Skip to content

test: pin consent, session-key injection, and the contact rate limit - #114

Draft
cursor[bot] wants to merge 5 commits into
productionfrom
cursor/missing-test-coverage-5b82
Draft

test: pin consent, session-key injection, and the contact rate limit#114
cursor[bot] wants to merge 5 commits into
productionfrom
cursor/missing-test-coverage-5b82

Conversation

@cursor

@cursor cursor Bot commented Sep 4, 2026

Copy link
Copy Markdown

Coverage for production paths that shipped without tests in CI, or whose tests lived under tests/ (which the check job cannot type-check) or skipped whenever typst/qpdf were absent.

Risky behavior now covered

  • Delivery consent (POST /api/responses/deliver): only the literal form value yes mails a copy; anything else — absent, empty, mis-cased, or an array — is treated as no. The assembled bundle is canonical 0–34, fills gaps rather than shortening the document, and refuses duplicate or out-of-range indices. A declined copy never looks up a session.
  • Session-key push (lib/session-keys.ts): a hostile session id (shell metacharacters, traversal, newlines) is refused before any transport runs — that id is interpolated into a remote shell command on the box that holds every respondent's private key. A transport failure is IdentityPushFailed with ambiguous === true and carries no key material. shredRemoteIdentity is best-effort and uses the same allowlist. Unset BREAKGLASS_AGE_RECIPIENT is fatal.
  • Gate-submit provisioning: when break-glass is unset, the submission is 503 and no answer text leaves the process. The fresh_gate_responses INSERT still binds NULL for both plaintext answer columns.
  • Contact rate limit: forged X-Forwarded-For does not mint a new identity when TRUST_PROXY is unset — the bug that made the 5/hour cap bypassable. PGP-marked plaintext is rejected. Unset CONTACT_AGE_RECIPIENT is 503, and the response never echoes the message.
  • Answer recipients: a legacy session (null pubkey) gets [] (gate default) and must not consult break-glass; a keyed session pairs the session key with break-glass and fails closed if that env var is missing.
  • Questionnaire next: a session_id or email in the query string is not a resume token.
  • PDF password: empty passwords and passwords containing CR/LF are refused before qpdf runs (the @argfile injection). These checks used to skip on every CI machine that lacks typst/qpdf.
  • Age encryption: both recipients open the same ciphertext; an empty recipient list is refused; AGE_RECIPIENT has no baked-in fallback.
  • Resume tokens: hashing without RESUME_TOKEN_SECRET throws; rotating the secret unlinks sessions.

Test files added/updated

  • routes/api/responses/deliver_test.ts (new)
  • lib/session-keys_test.ts (new)
  • lib/age-encrypt_test.ts (new)
  • lib/crypto_resume_test.ts (new)
  • routes/api/questions/answer_test.ts (new)
  • routes/api/questions/next_test.ts (new)
  • routes/api/contact_test.ts (new)
  • routes/api/newsletter/subscribe_test.ts (new)
  • routes/api/health_test.ts (new)
  • romania/tests/protect_guard_test.ts (new)
  • routes/api/gate-submit_test.ts — retargeted at the current provision-first ordering
  • routes/api/auth/magic-link_test.ts — assertion now matches the handler (Valid email required)
  • .github/workflows/ci.yml — these suites, plus existing lib/audience_test.ts, lib/client-ip_test.ts, lib/qr-scans_test.ts, lib/email_test.ts, romania/tests/, privacy, and WillyStCo-op tests, now run on the check job (--allow-write --allow-run)

No production behavior changes.

Why this reduces regression risk

The session-id allowlist is the difference between a filename and remote code execution on the key box; it was only tested in a directory CI cannot run. The contact form's rate limit was bypassable by setting X-Forwarded-For — that exact forgery is now a test, against the handler, not a copy of getClientIp. Consent fail-closed is the last check before mailing someone's questionnaire. PDF newline rejection is an argument-injection guard that previously skipped green in CI. Gate-submit now provisions keys before answers leave; the old suite still assumed the reverse and would have gone red for the wrong reason, or not run at all.

Hermetic: no Postgres, no SMTP (email_test aims at a closed loopback port), no live key box. fetch is stubbed. 223 passed locally on the expanded CI command (13 romania PDF tests ignored where typst/qpdf are absent — the password-injection guards now run anyway).

Open in Web View Automation 

cursoragent and others added 3 commits September 3, 2026 20:34
…WT that must not carry an address

The delivery confirmation route, the key-box callback, and questionnaire
JWTs shipped without tests. Pin fail-closed auth, write-once pdf_delivered_at,
GPC/DNT opt-out, and session tokens that verify as hashes not mailboxes.

Read KEYBOX_RENDER_* and RENDER_CALLBACK_* on each call (same boot-order
trap jwt.ts already documents) so the tests can set them, and so a missing
.env at import cannot lock the consent path into "unavailable" forever.

Co-authored-by: Ralphie B. <nyagrodha@users.noreply.github.com>
… nowhere

These suites already existed or just landed; the check job never invoked
them. Add --allow-write for keystore temp dirs. Leave tests/ out: it still
carries the pre-existing type errors that fail before any test executes.

Co-authored-by: Ralphie B. <nyagrodha@users.noreply.github.com>
…dentity

These contracts lived under tests/ (which CI cannot type-check) or skipped
whenever typst/qpdf were absent. Colocate them next to the production code
and run them on the check job so a hostile session id, a forged
X-Forwarded-For, or a guessed "yes" cannot regress silently.

Co-authored-by: Ralphie B. <nyagrodha@users.noreply.github.com>
cursoragent and others added 2 commits September 7, 2026 12:31
Keep production's lib/gate_encrypt_test.ts on the check job alongside the
delivery, auth, and audience suites this branch already added. The two
edits collided on the same deno test path list after #113 landed.

Co-authored-by: rbauer <rbauer@colorado.edu>
Union the CI test path lists so this branch (consent, session-key
injection, contact rate-limit) no longer conflicts with the delivery/JWT
suites from #112 or with production's gate_encrypt_test.ts from #113.

Co-authored-by: rbauer <rbauer@colorado.edu>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant