1bc2de655a
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
102 lines
8.1 KiB
Bash
102 lines
8.1 KiB
Bash
#!/usr/bin/env bash
|
|
# The catalogue as the phone drives it: create a garment, edit its card, add a size, set par,
|
|
# bind a barcode, archive and reinstate — plus the guards that stop a non-admin doing any of it
|
|
# and stop size history being rewritten.
|
|
set -u
|
|
B=${BASE:-http://127.0.0.1:3111}
|
|
# Refuses early, with the fix, when the server under test is in production mode with
|
|
# Turnstile refusing every auth route — otherwise the first signup fails and every check
|
|
# after it reports a security-check error instead of what it was testing.
|
|
. "$(dirname "$0")/e2e-preflight.sh"; e2e_preflight "$B"
|
|
T=${TMP:-/tmp}; J="$T/tc-cat-cj.txt"; K="$T/tc-cat-cj2.txt"; rm -f "$J" "$K"
|
|
PASS=0; FAIL=0
|
|
ok() { PASS=$((PASS+1)); echo " ✓ $1"; }
|
|
fail() { FAIL=$((FAIL+1)); echo " ✗ $1 :: $2"; }
|
|
mut() { curl -s -b "$J" -c "$J" -X POST "$B/api/mutate" -H 'content-type: application/json' -d "{\"op\":\"$1\",\"payload\":$2}"; }
|
|
mut2() { curl -s -b "$K" -c "$K" -X POST "$B/api/mutate" -H 'content-type: application/json' -d "{\"op\":\"$1\",\"payload\":$2}"; }
|
|
check(){ local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$(echo "$out" | head -c 300)"; fi; }
|
|
py() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; }
|
|
bk() { curl -s -b "$J" "$B/api/backup"; }
|
|
res() { py "print(d['result']$1)"; }
|
|
|
|
TS=$(date +%s)
|
|
echo "== setup"
|
|
check "signup" "$(curl -s -c "$J" -X POST "$B/api/auth/signup" -H 'content-type: application/json' \
|
|
-H "x-forwarded-for: 10.11.$((RANDOM%250)).$((RANDOM%250))" \
|
|
-d "{\"first\":\"Cat\",\"last\":\"Admin\",\"facility\":\"Cat Hospital $TS\",\"email\":\"cat$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
|
|
check "the facility names its staff groups" "$(e2e_groups "$B" "$J")" '"ok":true'
|
|
|
|
echo "== creating a garment from the phone"
|
|
check "a name is required" "$(mut catalog.add '{"item":"","sizes":["S"]}')" 'Item name required'
|
|
check "at least one size is required" "$(mut catalog.add '{"item":"Theatre Gown","sizes":[]}')" 'At least one size'
|
|
R=$(mut catalog.add '{"item":"Theatre Gown","type":"Gown","group":"Theatre","supplier":"Alpha Supply","sku":"TG1","cost":41.5,"sizes":["S","M","L"]}')
|
|
check "garment created" "$R" '"id"'
|
|
ID=$(echo "$R" | res "['id']")
|
|
# The order of the run is the assertion, not its length: every "si":N payload below addresses a
|
|
# size by its position, so a run that came back as M,S,L would still be three sizes and would still
|
|
# let every later check read back the index it just wrote.
|
|
check "the three sizes are in the order they were typed" "$(bk | py "print([i['sizes'] for i in d['items'] if i['id']=='$ID'][0])")" "^\['S', 'M', 'L'\]"
|
|
check "the supplier was created with it" "$(bk | py "print(any(s['name']=='Alpha Supply' for s in d['suppliers']))")" 'True'
|
|
|
|
echo "== editing the product card"
|
|
check "rename and reprice" "$(mut catalog.update "{\"id\":\"$ID\",\"item\":\"Theatre Gown (Blue)\",\"cost\":44}")" '"ok":true'
|
|
check "the name stuck" "$(bk | py "print([i['item'] for i in d['items'] if i['id']=='$ID'][0])")" 'Theatre Gown (Blue)'
|
|
check "the cost stuck" "$(bk | py "print([i['cost'] for i in d['items'] if i['id']=='$ID'][0])")" '^44'
|
|
check "a negative cost is refused" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":-1}")" 'Invalid cost'
|
|
|
|
echo "== sizes"
|
|
check "add a size" "$(mut catalog.update "{\"id\":\"$ID\",\"addSize\":\"XL\"}")" '"ok":true'
|
|
# addSize is the append path, and the item has no history yet, so nothing in the product stops it
|
|
# prepending or inserting. A count of four would not notice; the wrong run only surfaces later as a
|
|
# baffling failure in "but appending still works".
|
|
check "XL is appended to the end" "$(bk | py "print([i['sizes'] for i in d['items'] if i['id']=='$ID'][0])")" "^\['S', 'M', 'L', 'XL'\]"
|
|
check "a duplicate size is refused" "$(mut catalog.update "{\"id\":\"$ID\",\"addSize\":\"XL\"}")" 'already on the item'
|
|
|
|
echo "== par and barcode, the two things you set at a shelf"
|
|
check "set par on size M" "$(mut stock.reorder "{\"itemId\":\"$ID\",\"si\":1,\"reorder\":6}")" '"ok":true'
|
|
check "par stored" "$(bk | py "print([x['reorder'] for x in d['stock'] if x['itemId']=='$ID' and x['sizeIndex']==1][0])")" '^6$'
|
|
check "bind a barcode to size M" "$(mut barcode.bind "{\"code\":\"CAT$TS\",\"itemId\":\"$ID\",\"si\":1}")" '"ok":true'
|
|
check "barcode points at the size" "$(bk | py "print([b['sizeIndex'] for b in d['barcodes'] if b['code']=='CAT$TS'][0])")" '^1$'
|
|
check "the same code can't be bound twice" "$(mut barcode.bind "{\"code\":\"CAT$TS\",\"itemId\":\"$ID\",\"si\":2}")" 'is already on'
|
|
# bind ends in an upsert, so a refusal that never happened moves the label to size L rather than
|
|
# leaving it where it was. Reading it back is the half that proves the shelf is still right.
|
|
check "and it stayed on size M" "$(bk | py "print([b['sizeIndex'] for b in d['barcodes'] if b['code']=='CAT$TS'][0])")" '^1$'
|
|
|
|
echo "== history protects the size order"
|
|
# The import matches on sku, then resolves the size by name to a position. "created" is a
|
|
# processed-row counter — it says 1 whether the row landed on M or on L — so the quantity is read
|
|
# back off the position instead. (The history the next check leans on is already there: the par
|
|
# level and the barcode above both count towards it.)
|
|
check "an opening import finds the size by name" "$(mut import.rows "{\"kind\":\"opening\",\"rows\":[{\"sku\":\"TG1\",\"size\":\"M\",\"opening\":\"5\"}]}")" '"errors":\[\]'
|
|
check "opening stock lands on size M" "$(bk | py "print([x['opening'] for x in d['stock'] if x['itemId']=='$ID' and x['sizeIndex']==1][0])")" '^5$'
|
|
check "sizes can't be reordered once there is history" "$(mut catalog.update "{\"id\":\"$ID\",\"sizes\":[\"M\",\"S\",\"L\",\"XL\"]}")" "can't be removed or reordered"
|
|
check "but appending still works" "$(mut catalog.update "{\"id\":\"$ID\",\"sizes\":[\"S\",\"M\",\"L\",\"XL\",\"2XL\"]}")" '"ok":true'
|
|
|
|
echo "== archive and reinstate"
|
|
check "archive" "$(mut catalog.update "{\"id\":\"$ID\",\"archived\":true}")" '"ok":true'
|
|
check "it reads as archived" "$(bk | py "print([i['archived'] for i in d['items'] if i['id']=='$ID'][0])")" 'True'
|
|
check "reinstate" "$(mut catalog.update "{\"id\":\"$ID\",\"archived\":false}")" '"ok":true'
|
|
# A garment that archives but never comes back is the failure this section exists to catch, and
|
|
# "ok":true is what catalog.update says to any patch it accepted — including one that ignored this.
|
|
check "it reads as live again" "$(bk | py "print([i['archived'] for i in d['items'] if i['id']=='$ID'][0])")" '^False$'
|
|
|
|
echo "== an issuer can look but not touch"
|
|
check "invite an issuer" "$(mut users.add "{\"email\":\"catiss$TS@example.com\",\"first\":\"Ines\",\"last\":\"Issuer\",\"role\":\"ISSUER\",\"password\":\"password123\"}")" '"id"'
|
|
check "issuer signs in" "$(curl -s -c "$K" -X POST "$B/api/auth/login" -H 'content-type: application/json' -d "{\"email\":\"catiss$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
|
|
check "issuer cannot create a garment" "$(mut2 catalog.add '{"item":"Sneaky Coat","sizes":["M"]}')" 'Admin only'
|
|
check "issuer cannot edit the card" "$(mut2 catalog.update "{\"id\":\"$ID\",\"item\":\"Renamed\"}")" 'Admin only'
|
|
check "issuer cannot archive" "$(mut2 catalog.update "{\"id\":\"$ID\",\"archived\":true}")" 'Admin only'
|
|
# Every field the issuer's three attempts reached for: the name, and the archive flag — a garment
|
|
# left discontinued by a broken guard is invisible on the catalogue screen, and the name alone
|
|
# would never see it.
|
|
check "the garment is untouched" "$(bk | py "print([[i['item'], i['archived']] for i in d['items'] if i['id']=='$ID'][0])")" "^\['Theatre Gown (Blue)', False\]"
|
|
check "and nothing was created" "$(bk | py "print(any(i['item']=='Sneaky Coat' for i in d['items']))")" '^False$'
|
|
|
|
echo "== the phone routes exist and need a session"
|
|
check "/m/catalogue needs a session" "$(curl -s -o /dev/null -w '%{http_code} %{redirect_url}' "$B/m/catalogue")" '307.*/m/login'
|
|
check "/m/catalogue renders for a signed-in admin" "$(curl -s -b "$J" "$B/m/catalogue")" 'Catalogue'
|
|
check "the product card renders" "$(curl -s -b "$J" "$B/m/catalogue/$ID")" 'Sizes'
|
|
check "the new-garment form renders" "$(curl -s -b "$J" "$B/m/catalogue/new")" 'New garment'
|
|
|
|
echo; echo "PASS=$PASS FAIL=$FAIL"; [ "$FAIL" -eq 0 ]
|