Skip to content

test(e2e): wire three never-run specs into CI, and make the security one real - #377

Merged
catomean merged 3 commits into
mainfrom
ci/wire-e2e-specs
Aug 25, 2026
Merged

test(e2e): wire three never-run specs into CI, and make the security one real#377
catomean merged 3 commits into
mainfrom
ci/wire-e2e-specs

Conversation

@catomean

Copy link
Copy Markdown
Collaborator

Follow-up to the audit: the e2e-local job runs tests/e2e/*journey.spec.ts. Nine specs aren't named *journey, so nothing ran them — anywhere, ever.

I ran all nine against a locally seeded instance before wiring any. That's the only reason this PR is three specs and not nine.

Wired — 41 assertions that were running nowhere

spec assertions
security 4 tests — rewritten, see below
notification-hrefs 21, green as-is
user-admin-flows 16, green after one fix

security.spec.ts was a stub — wiring it as-written would have been worse than leaving it out

Two of its four tests had comment-only bodies ("Would need to fill and submit form 6 times"), a third asserted only inside an isVisible() branch, and every one called test.skip() when redirected to login — which /it-hilfe/create always does signed out. In CI it would have reported a green "security" check while exercising nothing.

It now asserts the closed side of the authorization boundary — the same boundary where #358 found a real privilege escalation:

  • no admin page renders to a signed-out visitor (all 37 routes, from the existing route SSOT)
  • no admin/money API answers 2xx signed out
  • a refused response carries no user data (a 401 that still serialises a row is a known fleet bug class)

It needs no credentials, so it can't be skipped for want of secrets. Proven by mutation: adding a public route to the protected list turns it red.

The one fix in user-admin-flows

It addressed routes as /de/x. The app 307-redirects those to /x, and when that redirect races the client router Playwright aborts the navigation (net::ERR_ABORTED) — indistinguishable from a broken page. It cost me three wrong hypotheses (concurrency → state residue → fresh database) before I stopped guessing and read the error context. Now uses the canonical paths, which is where the redirect lands and what users actually see.

Not wired, with reasons rather than silence

spec result why
marketplace 9 failed / 2 passed expects h1 = "Marketplace" and "gebrauchte IT-Geräte" — pre-rebrand Revamp-IT copy
it-hilfe 4 failed / 10 passed mixed — best next candidate
appointments 3 failed session-email mismatch vs seeded accounts
payment-return 1 failed / 1 passed
timecards 1 failed
dashboard-timecards 1 skipped skips its only test → inert if wired

Specs that nothing runs rot. These did. Recorded in docs/AUDIT_BACKLOG_2026-08.md with the evidence.

Verification

3 consecutive green runs of the bundle against a freshly migrated + seeded database (41/41 each), typecheck clean, lint 0 errors, docs gate green.

🤖 Generated with Claude Code

catomean and others added 3 commits August 25, 2026 16:04
…one real

The e2e-local job runs 'tests/e2e/*journey.spec.ts'. Nine specs are not
named *journey, so nothing ran them — anywhere, ever. I ran all of them
against a locally seeded instance before wiring any, which is the only
reason this PR is three specs and not nine.

Wired (41 assertions that were running nowhere):
  security            4 tests — rewritten, see below
  notification-hrefs  21 assertions, green as-is
  user-admin-flows    16 assertions, green after one fix

security.spec.ts was a STUB and wiring it as-written would have been
worse than leaving it out: two of its four tests had comment-only bodies
('Would need to fill and submit form 6 times'), a third asserted only
inside an isVisible() branch, and every one called test.skip() when
redirected to login — which /it-hilfe/create always does signed out. In
CI it would have reported a green 'security' check while exercising
nothing. It now asserts the closed side of the authorization boundary: no
admin page renders and no admin/money API answers 2xx to a signed-out
request, and a refused response carries no user data. Proven by mutation
— adding a public route to the protected list turns it red.

user-admin-flows addressed routes as /de/x. The app 307-redirects those
to /x, and when that races the client router Playwright aborts the
navigation (net::ERR_ABORTED) — indistinguishable from a broken page.
It cost three wrong hypotheses (concurrency, then state residue, then a
fresh database) before I read the error. Now uses the canonical paths,
which is where the redirect lands and what users actually see.

NOT wired, with reasons rather than silence: marketplace, it-hilfe,
appointments, payment-return and timecards fail against the current app —
marketplace expects an h1 of 'Marketplace' and the text 'gebrauchte
IT-Geräte', which is pre-rebrand Revamp-IT copy. dashboard-timecards
skips its only test. Specs nothing runs rot; these did. Recorded in
docs/AUDIT_BACKLOG_2026-08.md.

Verified: 3 consecutive green runs of the bundle, typecheck, lint, docs gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three are now wired; the other six are not, and the table says why rather
than leaving a silent gap. marketplace expects pre-rebrand copy, and
dashboard-timecards skips its only test. it-hilfe is the best next
candidate — 10 of its 14 tests already pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
CI rejected the previous commit and it was right to. playwright.config.ts
sets storageState globally, so every context — the page AND the request
fixture — inherits a saved login unless a spec says otherwise. My
'authorization boundary (signed out)' tests therefore ran authenticated:
/api/invoices, /api/admin/users and /api/admin/refunds all answered 200,
and the spec reported the doors were open.

They passed locally only because no saved-session file exists there. That
is the worst kind of green: right answer, wrong reason, and dependent on
which machine you run it on.

Both specs now force an empty session explicitly. For security.spec.ts
that line IS the test — a signed-out check carrying a session asserts
nothing about the closed side of the boundary.

Same root cause fixed notification-hrefs: it probes whether a ROUTE
exists using a dummy UUID matching no record. Signed out, an auth
redirect answers that without touching the database. Signed in, the admin
page renders and correctly 404s on the missing record — so it reported
'notification deep link is broken' when nothing was broken.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@catomean
catomean merged commit 90bb2a8 into main Aug 25, 2026
9 checks passed
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