Files
threadcount-community/scripts/e2e-cost.sh
T
ThreadCount a6f1059ddf ThreadCount Community edition
Uniform stock management for healthcare linen rooms: the coordinator app, the phone counter and the staff app, for your own server. Built from a113353 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
2026-09-15 22:54:09 +10:00

79 lines
4.8 KiB
Bash

#!/usr/bin/env bash
# Cost history: that a price is remembered, that it records what it moved from, and that a change
# which isn't a change doesn't produce a row.
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-cost-cj.txt"; rm -f "$J"
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}"; }
check(){ local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$(echo "$out" | head -c 250)"; fi; }
py() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; }
bk() { curl -s -b "$J" "$B/api/backup"; }
costs(){ curl -s -b "$J" "$B/m/catalogue" > /dev/null; bk; }
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.16.$((RANDOM%250)).$((RANDOM%250))" \
-d "{\"first\":\"Cora\",\"last\":\"Cost\",\"facility\":\"Cost Hospital $TS\",\"email\":\"cost$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
check "the facility names its staff groups" "$(e2e_groups "$B" "$J")" '"ok":true'
R=$(mut catalog.add '{"item":"Priced Gown","sku":"PG1","supplier":"Alpha Supply","cost":30,"sizes":["S","M"]}')
check "garment created" "$R" '"id"'
ID=$(echo "$R" | py "print(d['result']['id'])")
# The whole history for this garment, oldest first, each row as "cost<-previous". Counting rows can
# only ever say that a row appeared; the figures in it are what finance actually reads back.
hist() { bk | py "print(' | '.join('%g<-%s' % (c['cost'], 'none' if c['previous'] is None else '%g' % c['previous']) for c in d.get('costs',[]) if c['itemId']=='$ID'))"; }
itemcost() { bk | py "print('%g' % [i['cost'] for i in d['items'] if i['id']=='$ID'][0])"; }
echo "== the opening price is remembered"
check "the price landed on the garment" "$(itemcost)" '^30$'
# previous is null rather than a figure, which is what makes this row the start of the history
# instead of an edit somebody made on day one.
check "one opening row, at the opening price" "$(hist)" '^30<-none$'
echo "== a change records what it moved from"
check "raise the price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":34.5}")" '"ok":true'
N1=$(bk | py "print(len([c for c in d.get('costs',[]) if c['itemId']=='$ID']))")
check "two rows now" "$N1" '^2$'
check "the latest is the new price" "$(bk | py "print([c['cost'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" '34.5'
check "and remembers the old one" "$(bk | py "print([c['previous'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" '^30'
check "and who changed it" "$(bk | py "print([c['byName'] for c in d.get('costs',[]) if c['itemId']=='$ID'][-1])")" 'Cora Cost'
echo "== setting the same price again is not a change"
check "save the identical price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":34.5}")" '"ok":true'
check "no row for the identical price" "$(hist)" '^30<-none | 34\.5<-30$'
echo "== editing other fields leaves the history alone"
check "rename it" "$(mut catalog.update "{\"id\":\"$ID\",\"item\":\"Priced Gown (Blue)\"}")" '"ok":true'
check "no row for a rename" "$(hist)" '^30<-none | 34\.5<-30$'
echo "== a price drop is recorded as a drop"
check "lower the price" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":29}")" '"ok":true'
check "a third row, 34.5 down to 29" "$(hist)" '^30<-none | 34\.5<-30 | 29<-34\.5$'
echo "== a negative price is refused on an edit"
check "negative refused" "$(mut catalog.update "{\"id\":\"$ID\",\"cost\":-5}")" 'Invalid cost'
# Refused has to mean nothing was written. An error message sent after the update had already gone
# through would read exactly the same from out here, with the garment left priced at -5.
check "the price is untouched" "$(itemcost)" '^29$'
check "and the history is untouched" "$(hist)" '^30<-none | 34\.5<-30 | 29<-34\.5$'
echo "== a negative price on creation is clamped, not refused"
# catalog.add takes Math.max(0, cost) where catalog.update throws, so the two doors disagree about
# the same bad figure. Pinned here so that whichever way it is settled, it is settled deliberately.
R2=$(mut catalog.add '{"item":"Odd Gown","sku":"OG1","supplier":"Alpha Supply","cost":-5,"sizes":["S"]}')
check "creation accepted" "$R2" '"id"'
ID=$(echo "$R2" | py "print(d['result']['id'])")
check "the price is zero, not negative" "$(itemcost)" '^0$'
check "and a zero price opens no history" "$(hist)" '^$'
echo; echo "PASS=$PASS FAIL=$FAIL"; [ "$FAIL" -eq 0 ]