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 f976bd5 on 2026-09-15. Licensed under the Functional Source License (FSL-1.1-ALv2).
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# Photos on disk: stored as files rather than base64, served only to the owning facility, and
|
||||
# still whole in a backup.
|
||||
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-ph-cj.txt"; K="$T/tc-ph-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}"; }
|
||||
check(){ local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$(echo "$out" | head -c 200)"; fi; }
|
||||
py() { python3 -c "import sys,json; d=json.load(sys.stdin); $1"; }
|
||||
|
||||
# A real 1x1 JPEG, so what is written is a genuine image rather than random bytes.
|
||||
JPEG_B64=$(python3 -c "
|
||||
import base64
|
||||
# smallest valid JPEG
|
||||
b = bytes.fromhex('ffd8ffe000104a46494600010100000100010000ffdb004300ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc00011080001000101011100ffc40014000100000000000000000000000000000009ffc40014100100000000000000000000000000000000ffda0008010100003f0054df')
|
||||
print(base64.b64encode(b).decode())
|
||||
")
|
||||
DATAURL="data:image/jpeg;base64,$JPEG_B64"
|
||||
|
||||
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.17.$((RANDOM%250)).$((RANDOM%250))" \
|
||||
-d "{\"first\":\"Pia\",\"last\":\"Photo\",\"facility\":\"Photo Hospital $TS\",\"email\":\"ph$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
|
||||
check "the facility names its staff groups" "$(e2e_groups "$B" "$J")" '"ok":true'
|
||||
|
||||
echo "== storing"
|
||||
R=$(mut photo.put "{\"kind\":\"sig\",\"data\":\"$DATAURL\"}")
|
||||
check "photo accepted" "$R" '"id"'
|
||||
PID=$(echo "$R" | py "print(d['result']['id'])")
|
||||
|
||||
echo "== it is on disk, not in the database"
|
||||
ROW=$(curl -s -b "$J" "$B/api/backup" | py "
|
||||
ph = [x for x in d['photos'] if x['id']=='$PID'][0]
|
||||
print('path=' + (ph.get('path') or '') + ' mime=' + (ph.get('mime') or '') + ' bytes=' + str(ph.get('bytes')))
|
||||
")
|
||||
# Named after the row id we generated, never after anything the request supplied — the trailing
|
||||
# space keeps the match inside the path field, so an empty path cannot borrow the slash in the mime.
|
||||
check "the row records a file path" "$ROW" "path=[A-Za-z0-9_-]\{1,\}/$PID\.jpg "
|
||||
check "and the mime type" "$ROW" 'mime=image/jpeg'
|
||||
# The decoded image is 143 bytes (192 as base64). Pinning the number is what catches bytes ever
|
||||
# recording the encoded length instead of the real size of the file on disk.
|
||||
check "and the size" "$ROW" 'bytes=143$'
|
||||
|
||||
echo "== serving"
|
||||
CT=$(curl -s -b "$J" -o /dev/null -w '%{content_type} %{http_code}' "$B/api/photo/$PID")
|
||||
check "served as a jpeg" "$CT" 'image/jpeg 200'
|
||||
check "signed out is refused" "$(curl -s -o /dev/null -w '%{http_code}' "$B/api/photo/$PID")" '401'
|
||||
|
||||
echo "== another facility cannot read it"
|
||||
check "second facility signs up" "$(curl -s -c "$K" -X POST "$B/api/auth/signup" -H 'content-type: application/json' \
|
||||
-H "x-forwarded-for: 10.18.$((RANDOM%250)).$((RANDOM%250))" \
|
||||
-d "{\"first\":\"Oth\",\"last\":\"Er\",\"facility\":\"Other Photo $TS\",\"email\":\"pho$TS@example.com\",\"password\":\"password123\"}")" '"ok":true'
|
||||
check "their request 404s" "$(curl -s -b "$K" -o /dev/null -w '%{http_code}' "$B/api/photo/$PID")" '404'
|
||||
|
||||
echo "== the backup is still whole"
|
||||
# Byte-identical to what went in, not merely a data URL with the right prefix: the prefix is built
|
||||
# from the mime column, so a truncated or wrong file on disk still produces one.
|
||||
check "backup embeds the image" "$(curl -s -b "$J" "$B/api/backup" | py "
|
||||
ph = [x for x in d['photos'] if x['id']=='$PID'][0]
|
||||
print('same' if ph.get('data') == '$DATAURL' else 'DIFFERENT')
|
||||
")" 'same'
|
||||
|
||||
echo "== rubbish is refused"
|
||||
check "not an image" "$(mut photo.put '{"kind":"sig","data":"data:text/html;base64,PGgxPmhp"}')" 'JPEG or PNG'
|
||||
check "empty" "$(mut photo.put '{"kind":"sig","data":""}')" 'JPEG or PNG'
|
||||
|
||||
echo; echo "PASS=$PASS FAIL=$FAIL"; [ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user