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 2d04e45 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/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 ]
|
||||
Reference in New Issue
Block a user