#!/usr/bin/env bash # Plans, phase 1: the promise, the ceiling, read-only, and the console's billing desk. # # What it proves, in order: # - with plans OFF (the default) a new facility is grandfathered: no staff ceiling at all; # - an owner flips plans ON from the console, and a facility created after that is Hosted Small: # the 61st staff record is refused, by the form and by the import, with the same words; # - the console can set a room read-only: every write is refused with the one refusal, the # backup and the plan ops still work, and "make writable" undoes it; # - a trial and a payment record the dates, and "set free" clears them; # - plans are turned OFF again at the end, so the next suite still gets grandfathered rooms. # # Needs a server in dev mode (Turnstile advisory) and DATABASE_URL in the environment for the # operator seed, like e2e-ops.sh. Nothing here sends mail: the invoice request is a notice to the # owner that swallows a missing SMTP. set -u B=${BASE:-http://127.0.0.1:3111} . "$(dirname "$0")/e2e-preflight.sh"; e2e_preflight "$B" T=${TMP:-/tmp} OPS=ops.threadcount.tech OJ="$T/tc-plan-op.txt"; J1="$T/tc-plan-a.txt"; J2="$T/tc-plan-b.txt" rm -f "$OJ" "$J1" "$J2" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); echo " ✓ $1"; } fail() { FAIL=$((FAIL+1)); echo " ✗ $1 :: $(echo "$2" | head -c 240)"; } check(){ local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then ok "$name"; else fail "$name" "$out"; fi; } no() { local name=$1 out=$2 pat=$3; if echo "$out" | grep -q "$pat"; then fail "$name" "$out"; else ok "$name"; fi; } code() { curl -s -o /dev/null -w '%{http_code}' "$@"; } mut() { local jar=$1 op=$2 payload=$3; curl -s -b "$jar" -c "$jar" -X POST "$B/api/mutate" -H 'content-type: application/json' -d "{\"op\":\"$op\",\"payload\":$payload}"; } opost(){ curl -s -b "$OJ" -c "$OJ" -X POST -H "Host: $OPS" -H 'content-type: application/json' "$B$1" -d "$2"; } control(){ opost /api/ops/controls "$1"; } jget() { python3 -c "import sys,json; d=json.load(sys.stdin); print(eval('d$1'))"; } signup(){ local jar=$1 name=$2 email=$3; curl -s -c "$jar" -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.7.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"Plan\",\"last\":\"Tester\",\"facility\":\"$name\",\"email\":\"$email\",\"password\":\"password123\"}"; } # N staff rows as an import payload: numbers 1..N. rows() { python3 -c "import json,sys; n=int(sys.argv[1]); print(json.dumps({'kind':'staff','rows':[{'num':str(9000+i),'first':'P%d'%i,'last':'Row','group':'Kitchen','dept':'Kitchen'} for i in range(1,n+1)]}))" "$1"; } TS=$(date +%s) echo "== an owner at the console" OE="plan-ops-$TS@example.com" check "an operator is seeded" "$(OPERATOR_PASSWORD='e2e-operator-password-1' node scripts/create-operator.cjs "$OE" "Plan Ops" OWNER 2>&1)" "created operator $OE" check "and signs in" "$(opost /api/ops/auth/login "{\"email\":\"$OE\",\"password\":\"e2e-operator-password-1\"}")" '"ok":true' # Plans must start OFF for the first half; say so out loud if a previous run left them on. check "plans are off to begin with" "$(control '{"action":"switch","key":"plansLive","value":false}')" '"ok":true' echo "== plans off: the website still says free" R=$(curl -s "$B/pricing") check "pricing says that is the whole page" "$R" 'That is the whole page' no " and nothing about a price" "$R" '>\$129<' check "the home page states the price" "$(curl -s "$B/")" 'No licence, no per-device charge' check "the footer chip says free to use" "$(curl -s "$B/")" 'Free to use' no "the sign-in page asks nothing about plans" "$(curl -s "$B/auth?mode=signup")" 'Hosted Facility' check "the terms already carry the fees section" "$(curl -s "$B/terms")" '9. Fees' echo "== plans off: a new facility is grandfathered" check "signup A" "$(signup "$J1" "Plan Grandfathered $TS" "plan-a-$TS@example.com")" '"ok":true' check " groups" "$(e2e_groups "$B" "$J1")" '"ok":true' R=$(mut "$J1" import.rows "$(rows 65)") check " 65 staff import with no ceiling" "$R" '"created":65' no " and no refusal in the errors" "$R" 'register is full' check " the 66th by the form" "$(mut "$J1" staff.save '{"num":"9066","first":"Sixty","last":"Six","group":"Kitchen","dept":"Kitchen"}')" '"id"' echo "== plans on" check "the owner turns plans on" "$(control '{"action":"switch","key":"plansLive","value":true}')" '"ok":true' R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/controls") check " and the switches page says live" "$R" 'Live' echo "== plans on: the website tells the plans story" R=$(curl -s "$B/pricing") check "pricing says free to run" "$R" 'Free to run' check " and names the price" "$R" '>\$129<' check " and the health service bundle" "$R" '\$4,990' check " and keeps the founder's sentence" "$R" 'charging for it was never the point' no " and no longer says that is the whole page" "$R" 'That is the whole page' check " the title is still Pricing" "$R" 'Pricing' R=$(curl -s "$B/") check "the home page band changed" "$R" 'Paid to host' no " and no longer says no licence" "$R" 'No licence, no per-device charge' check " structured data carries the hosted offer" "$R" '"price":"1290"' check "the footer chip says free to run" "$R" 'Free to run</div>' R=$(curl -s "$B/faq") check "the FAQ answer changed" "$R" 'The software is. Run it on your own server' check "the about page's last line changed" "$(curl -s "$B/about")" 'Hosting it for you is what costs money' no "the support heading no longer says free" "$(curl -s "$B/support")" 'is free and maintained' check "the terms carry the fees section" "$(curl -s "$B/terms")" '9. Fees' check " and the grandfathering" "$(curl -s "$B/terms")" 'is grandfathered: hosted free' check "the sign-in page offers the plan choice" "$(curl -s "$B/auth?mode=signup")" 'Hosted Facility' check "the phone sign-up offers it too" "$(curl -s "$B/m/signup")" 'Hosted Small' echo "== plans on: a trial can be chosen at sign-up" J4="$T/tc-plan-d.txt"; rm -f "$J4" check "signup D on a Hosted Facility trial" "$(curl -s -c "$J4" -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.6.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"Plan\",\"last\":\"Trial\",\"facility\":\"Plan Trial $TS\",\"email\":\"plan-d-$TS@example.com\",\"password\":\"password123\",\"plan\":\"hosted_facility\"}")" '"ok":true' check " groups" "$(e2e_groups "$B" "$J4")" '"ok":true' check " no ceiling on a trial" "$(mut "$J4" import.rows "$(rows 61)")" '"created":61' FD=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities" | grep -o "href=\"/ops/facilities/[a-z0-9]\{20,40\}\"[^>]*>Plan Trial $TS<" | head -1 | sed 's/.*facilities\/\([a-z0-9]*\)".*/\1/') R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FD") check " the console shows Hosted Facility" "$R" 'Hosted Facility' check " on trial" "$R" '>Trial<' check " a nonsense plan lands on Hosted Small" "$(J5="$T/tc-plan-e.txt"; rm -f "$J5"; curl -s -c "$J5" -X POST "$B/api/auth/signup" -H 'content-type: application/json' -H "x-forwarded-for: 10.5.$((RANDOM%250)).$((RANDOM%250))" -d "{\"first\":\"Plan\",\"last\":\"Odd\",\"facility\":\"Plan Odd $TS\",\"email\":\"plan-e-$TS@example.com\",\"password\":\"password123\",\"plan\":\"gold\"}" >/dev/null; e2e_groups "$B" "$J5" >/dev/null; mut "$J5" import.rows "$(rows 61)")" '"created":60' echo "== plans on: a new facility is Hosted Small" check "signup B" "$(signup "$J2" "Plan Small $TS" "plan-b-$TS@example.com")" '"ok":true' check " groups" "$(e2e_groups "$B" "$J2")" '"ok":true' R=$(mut "$J2" staff.save '{"num":"8001","first":"First","last":"Person","group":"Kitchen","dept":"Kitchen"}') check " one record by the form" "$R" '"id"'; S1=$(echo "$R" | jget '["result"]["id"]') R=$(mut "$J2" import.rows "$(rows 65)") check " 65 rows import 59 more" "$R" '"created":59' check " and say why once" "$R" 'register is full for this plan' check " skipping the rest" "$R" '"skipped":6' check " the 61st by the form is refused" "$(mut "$J2" staff.save '{"num":"9099","first":"Sixty","last":"One","group":"Kitchen","dept":"Kitchen"}')" 'register is full' check " editing an existing record still works" "$(mut "$J2" staff.save "{\"id\":\"$S1\",\"num\":\"8001\",\"first\":\"First\",\"last\":\"Renamed\",\"group\":\"Kitchen\",\"dept\":\"Kitchen\"}")" "\"id\":\"$S1\"" check " a coordinator's other writes are untouched" "$(mut "$J2" dept.save '{"name":"Willow","cc":"CC-1"}')" '"ok":true' check " facility A is still grandfathered" "$(mut "$J1" staff.save '{"num":"9067","first":"Sixty","last":"Seven","group":"Kitchen","dept":"Kitchen"}')" '"id"' echo "== the console sees the plan" FB=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities" | grep -o "href=\"/ops/facilities/[a-z0-9]\{20,40\}\"[^>]*>Plan Small $TS<" | head -1 | sed 's/.*facilities\/\([a-z0-9]*\)".*/\1/') if [ -n "$FB" ]; then ok "facility B's id from the list"; else fail "facility B's id from the list" "not found"; fi R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FB") check " its page says Hosted Small" "$R" 'Hosted Small' check " and a ceiling of 60" "$R" '>60<' R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities") check " the list says free" "$R" 'Hosted Small · free' check " and grandfathered for A" "$R" 'Free · grandfathered' echo "== read-only" check "the console sets B read-only" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"readonly\",\"on\":true}")" '"ok":true' check " a write is refused" "$(mut "$J2" dept.save '{"name":"Maple","cc":"CC-2"}')" 'Read-only' check " with a 403" "$(curl -s -o /dev/null -w '%{http_code}' -b "$J2" -X POST "$B/api/mutate" -H 'content-type: application/json' -d '{"op":"dept.save","payload":{"name":"Oak","cc":"CC-3"}}')" '403' check " the backup still downloads" "$(code -b "$J2" "$B/api/backup")" '200' check " the billing contact can still be set" "$(mut "$J2" plan.billing '{"email":"accounts@example.com"}')" '"ok":true' check " and an invoice requested" "$(mut "$J2" plan.invoice '{"email":"accounts@example.com","plan":"hosted_facility"}')" '"ok":true' check " but not twice a day, four times" "$(mut "$J2" plan.invoice '{}' >/dev/null; mut "$J2" plan.invoice '{}' >/dev/null; mut "$J2" plan.invoice '{}')" 'already been requested' check " the coordinator's own password op stays open" "$(mut "$J2" me.password '{"current":"password123","next":"password1234"}')" '"ok":true' check " the list shows it in red" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities")" 'Hosted Small · read-only' check "and makes it writable again" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"readonly\",\"on\":false}")" '"ok":true' check " a write goes through" "$(mut "$J2" dept.save '{"name":"Maple","cc":"CC-2"}')" '"ok":true' check " the ceiling is back too" "$(mut "$J2" staff.save '{"num":"9098","first":"Sixty","last":"One","group":"Kitchen","dept":"Kitchen"}')" 'register is full' echo "== trial, payment, free" check "a 60-day trial starts" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"trial\",\"days\":60}")" '"ok":true' R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FB") check " the page says Hosted Facility" "$R" 'Hosted Facility' check " on trial" "$R" '>Trial<' check " the ceiling is lifted" "$(mut "$J2" staff.save '{"num":"9098","first":"Sixty","last":"One","group":"Kitchen","dept":"Kitchen"}')" '"id"' check "a payment is recorded" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"paid\",\"months\":12}")" '"ok":true' check " the page says Paid" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FB")" '>Paid<' check "set free clears it" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"free\"}")" '"ok":true' check " and the page says Free" "$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FB")" 'Recorded as</dt><dd[^>]*>free' check "an owner sets Health Service and grandfathers" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"set\",\"plan\":\"health_service\",\"planNote\":\"PO 1\",\"grandfathered\":true}")" '"ok":true' R=$(curl -s -b "$OJ" -H "Host: $OPS" "$B/ops/facilities/$FB") check " the page says Health Service" "$R" 'Health Service' check " and grandfathered" "$R" 'grandfathered' check " the note is shown" "$R" 'PO 1' check " a bad plan code is refused" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"set\",\"plan\":\"gold\",\"planNote\":\"\"}")" 'Not a plan' check " a grandfathered room has no ceiling even on read-only's neighbour, free" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"set\",\"plan\":\"hosted_small\",\"planNote\":\"\",\"grandfathered\":true}" >/dev/null; mut "$J2" staff.save '{"num":"9097","first":"Sixty","last":"Two","group":"Kitchen","dept":"Kitchen"}')" '"id"' check " taking the promise away brings the ceiling back" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"set\",\"plan\":\"hosted_small\",\"planNote\":\"\",\"grandfathered\":false}" >/dev/null; mut "$J2" staff.save '{"num":"9096","first":"Sixty","last":"Three","group":"Kitchen","dept":"Kitchen"}')" 'register is full' check " read-only beats the promise" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"set\",\"plan\":\"hosted_small\",\"planNote\":\"\",\"grandfathered\":true}" >/dev/null; control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"readonly\",\"on\":true}" >/dev/null; mut "$J2" dept.save '{"name":"Elm","cc":"CC-5"}')" 'Read-only' echo "== plans off again" check "the owner turns plans off" "$(control '{"action":"switch","key":"plansLive","value":false}')" '"ok":true' check " a facility created now is grandfathered" "$(J3="$T/tc-plan-c.txt"; rm -f "$J3"; signup "$J3" "Plan After $TS" "plan-c-$TS@example.com" >/dev/null; e2e_groups "$B" "$J3" >/dev/null; mut "$J3" import.rows "$(rows 61)")" '"created":61' check " facility B is still read-only from the owner's last act" "$(mut "$J2" dept.save '{"name":"Birch","cc":"CC-4"}')" 'Read-only' check " and made writable to finish" "$(control "{\"action\":\"plan\",\"facilityId\":\"$FB\",\"act\":\"readonly\",\"on\":false}")" '"ok":true' echo; echo "plan: $PASS passed, $FAIL failed" [ "$FAIL" -eq 0 ]