Appearance
Testing
Worker tests (Vitest with the Workers pool)
The Worker is tested inside the Workers runtime, not under Node. vitest.config.ts uses defineWorkersConfig from @cloudflare/vitest-pool-workers, so each test file runs in a Miniflare-backed Worker with real D1, R2, and Web Crypto implementations. Tests call the Worker through SELF.fetch(...) and read bindings through env from cloudflare:test.
Configuration
| Setting | Value |
|---|---|
| Wrangler config | ./wrangler.jsonc (supplies the Worker entry src/index.ts and the assets binding) |
| Emulated bindings | D1 DB, R2 FILES |
| Injected test secrets | HERMES_LINK_TOKEN, STRIPE_WEBHOOK_SECRET, an empty STRIPE_SECRET_KEY, and the price bindings STRIPE_PRICE_MONTHLY=price_monthly_test, STRIPE_PRICE_QUARTERLY=price_quarterly_test, STRIPE_PRICE_ANNUAL=price_annual_test (in vitest.config.ts). The empty key leaves billing calls to Stripe unconfigured, so checkout answers 503 billing_not_configured and the webhook neither re-fetches subscriptions nor cancels duplicates. The price bindings let tests exercise price-based plan detection |
| Schema | readD1Migrations("./migrations") is passed in as the binding TEST_MIGRATIONS, and the setup file test/apply-migrations.ts applies them with applyD1Migrations before the tests run |
| Types | test/env.d.ts extends ProvidedEnv with the Env interface and TEST_MIGRATIONS |
Tests create fresh users with random email addresses, so they do not depend on each other's rows.
What is covered
test/api.test.ts (family API):
| Test | Covers |
|---|---|
Sign up, /api/me, login, logout | Email normalization, HttpOnly and SameSite=Lax on the cookie, 401 invalid_credentials, cookie cleared with Max-Age=0, session no longer valid after logout |
| Duplicate email | 409 email_taken for the same email in a different case and with whitespace |
| Inmate validation and locking | 400 invalid_field naming the field, state normalized to uppercase, 409 inmate_locked for a number change and for deletion while a subscription is active |
| Plans and checkout prerequisites | GET /api/plans contents, 400 inmate_required, 503 billing_not_configured |
| Usage and route errors | Zero usage without a subscription, 404 not_found, 405 method_not_allowed, 401 without a cookie |
| Secure cookie | Secure is set on an https:// request and absent on an http:// request, independent of APP_URL |
| Checkout conflicts | 409 already_subscribed for an account with a live subscription and 409 inmate_taken when another account is subscribed for the same inmate number |
| Usage attribution | Hermes usage for a shared inmate_number lands on the account with the live subscription, the other account sees none, and an unknown number gives 404 unknown_inmate |
| Monthly window usage | A quarterly subscription's /api/usage returns the current monthly period_start/period_end, billing_period_end, and counts only events inside the monthly window |
| Expired session cleanup | Logging in deletes the user's expired sessions and, opportunistically, expired sessions of other users |
test/stripe_webhook.test.ts (Stripe and Hermes):
| Test | Covers |
|---|---|
| Bad signatures and stale timestamps | 400 invalid_signature for a wrong signature and for a timestamp 600 seconds old |
| Signed subscription events | Event accepted, inmate moves to pending, raw event archived to R2 at stripe/events/<id>.json, replay answered with duplicate: true and only one stripe_events row |
| Subscription item periods | Period dates are taken from the first subscription item when the object has no top-level periods |
| Hermes endpoints | 401 unauthorized without the bearer token on all four endpoints, pending connection listing with messages_per_month, the monthly-window period_start/period_end and the billing_period_start/billing_period_end, acknowledgement, usage posting with duplicate false then true, and the family's /api/usage reflecting the posted stamps |
| Renewals and duplicate subscriptions | A renewal of an already connected inmate keeps the connection connected; events for a different Stripe subscription than the live one leave the stored subscription id, status, and connection unchanged and are recorded at stripe/duplicates/<subscription_id>.json (with cancellation_requested: false and canceled: false, since no Stripe key is set in tests) |
| Plan from price | A customer.subscription.updated event whose item price is the quarterly price changes plan_id to quarterly even though the metadata still says monthly; a late checkout.session.completed naming monthly does not overwrite the live plan |
test/period.test.ts (monthly window calculation, src/period.ts):
| Test | Covers |
|---|---|
| January 31 anchor | A quarterly period anchored on 2025-01-31T12:30Z clamps February's boundary to the 28th (2025-02-28T12:30Z to 2025-03-31T12:30Z) |
| Quarterly end clamp | The third month of a quarterly period starts on the 31st and ends at the billing period end |
| Annual end clamp | The twelfth month of an annual period anchored on 2024-02-29 ends at the billing period end |
| February-to-March monthly period | A monthly period from 2025-02-28 to 2025-03-31 is exactly one window, with no short extra window |
| April-to-July quarterly period | A quarterly period from 2025-04-30 to 2025-07-31 has exactly three windows; on 2025-07-30 the window is 2025-06-30 to 2025-07-31 |
Running
From cloud/reform-assistant/:
sh
npm ci
npm test # vitest run, one pass
npm run typecheck # tsc --noEmit, strict, includes src/, test/, vitest.config.tsUse npx vitest for watch mode, and npx vitest run test/api.test.ts to run one file. No Stripe account, network access, or deployed Cloudflare resources are needed.
Bridge tests (Python)
The Hermes bridge has its own tests in the repository's root test suite at tests/cloud/test_reform_assistant_portal_sync.py. They import portal_sync directly, replace the portal HTTP calls and the Securus CLI with fakes, and cover:
- a pending connection adds the contact in portal mode (
--portal-allowance), grants the plan allowance, and acknowledgesconnected; - an already-added, enabled contact is acknowledged
connectedwithout a second welcome; - a Securus CLI failure or timeout is acknowledged as
failedwhile later connections continue; - a pending connection with a null or blank
period_startis acknowledgedfailedand nothing is stored for it, and an allowance with a null period is skipped (not saved, not refilled until a later window has a valid start); - an invalid previous period in the state file is recovered from, and an invalid candidate period is rejected;
- reconnecting an inmate grants only
max(0, messages_per_month - used)using the matching allowances record, never a negative amount, and the full allowance when no matching record exists; after connecting, the refill marker holds the allowance's monthly-windowperiod_start, so the same run does not top the balance back up; - the
revokedmarker is kept when reactivating the allowance fails, and cleared only once a retry succeeds; - a refill happens only once for each monthly window, and a subscription missing from the allowances response has its remaining stamps revoked once;
- usage is posted once per message across runs, with the
<entry_id>:<message_id>key (including repeated reply ids and entries without reply ids); - the Securus CLI runs with a 600-second timeout, non-interactive and UTF-8;
mainexits non-zero after an operation failure, and the cron entry point does not print the token.
The portal path of the Securus skill (add_contact --portal-allowance) is covered in tests/skills/test_securus_emessage_skill.py: the plan allowance is set before the welcome, no admin notice and no reply mark are sent, and an already-added enabled contact succeeds without a second welcome.
The repository's contributor guide (AGENTS.md) requires running Python tests through scripts/run_tests.sh, never with bare pytest. Run both files:
sh
scripts/run_tests.sh tests/cloud/test_reform_assistant_portal_sync.py tests/skills/test_securus_emessage_skill.pyEnd-to-end
The end-to-end runner exercises the running Worker over HTTP, then invokes the real Hermes portal bridge with a temporary Hermes home and a stateful stub Securus skill. It does not start Wrangler or install dependencies. Start wrangler dev separately, with development bindings configured (including the Hermes token and Stripe webhook secret), then from cloud/reform-assistant/ run:
sh
npm run e2eThe runner uses Node.js 22 and Python 3.11+ with built-in modules only. Set E2E_BASE_URL to the running Worker URL (default http://127.0.0.1:8787), E2E_HERMES_TOKEN to the same value configured for HERMES_LINK_TOKEN, and E2E_STRIPE_WEBHOOK_SECRET to the same value configured for STRIPE_WEBHOOK_SECRET. The local development environment must have no STRIPE_SECRET_KEY so checkout reports billing_not_configured and subscription events are processed from their signed payloads. The runner creates a unique email and inmate number per run. It prints PASS|FAIL|SKIP E2E-Fxx ... lines and exits non-zero if a step fails.
The steps check:
- F01–F02: landing and legal pages plus the unknown-route 404 page.
- F03–F04: account creation, duplicate and short-password rejection, HTTP session-cookie flags, authenticated session access, logout, and generic bad-password handling.
- F07–F09: invalid and valid inmate data, active-subscription inmate locking, checkout's missing-inmate and unconfigured-billing responses, and the plan catalog.
- F11–F13: signed subscription creation, replay and signature rejection, renewal and cancellation, pending connections and monthly allowance fields, bearer authorization, and family usage reporting.
- F14–F15: a real
hermes_bridge/portal_sync.pyrun against the Worker and a temporary fake Securus CLI/store. It verifies the portal-mode contact add with allowance 40, connection acknowledgement, one usage post across repeated runs, and allowance revocation after cancellation. - F05, F06, F10, F16, F17: print explicit
SKIPlines: redirect behavior and console rendering require browser QA; billing portal needs a configured Stripe customer; docs rendering and Cloudflare deployment are outside this local HTTP/bridge run.
What is not automated
- Interactive frontend behavior (responsive layouts, form interactions, client-side navigation, and console rendering) still needs browser QA; the end-to-end runner checks only public-page responses and headings.
- The Stripe calls in
src/billing.ts(Checkout and Billing Portal sessions) are not exercised against Stripe by the test suite; verify them in the sandbox as described in Stripe testing. - The Stripe subscription re-fetch and the cancellation of duplicate subscriptions in
src/stripe_webhook.ts(including the re-fetch before theDELETEand the re-check after a failed one), and the lookup and expiry of an open Checkout Session insrc/billing.ts, are not exercised: the test suite runs with an emptySTRIPE_SECRET_KEY, so subscription events are applied from their payloads, duplicates are only recorded, and no Stripe call is made. Verify them in the sandbox. - The end-to-end path from a real Checkout payment through the bridge to a connected inmate needs a manual run with
stripe listen,wrangler dev, and the bridge pointed athttp://localhost:8787.