Skip to content

test(e2e): PKCE-on-the-wire, per-user scoping, and desk clash coverage - #477

Open
camreeves wants to merge 4 commits into
e2e/ci-verifyfrom
e2e/coverage
Open

test(e2e): PKCE-on-the-wire, per-user scoping, and desk clash coverage#477
camreeves wants to merge 4 commits into
e2e/ci-verifyfrom
e2e/coverage

Conversation

@camreeves

Copy link
Copy Markdown
Contributor

Stacked on #476 — please review that one first, this branches off it.

Adds three specs to the workplace e2e suite. All P0s in the coverage contract are now done.

Spec Covers Why it matters
pkce.spec.ts AUTH-E2E-01 Asserts the login handshake on the wire, not on the response. A client that leaked a secret or silently dropped PKCE would still get a valid token and a working app, so the response can't tell you the exchange was sound. Checks S256 (never plain), a real base64url challenge, a code_verifier, the expected client_id, no client_secret anywhere, and that the password never appears in a URL. Ported from tasks/PPT-2536/, where nothing ran it.
booking-scoping.spec.ts WP-E2E-08, AUTH-E2E-05 A second user can't see your booking in their listing and can't delete it. Locks down something we learned the hard way: GET /bookings is caller-scoped, and an early leak check written as an admin reported zero bookings while the database held one.
desk-clash.spec.ts REG-02 No double-booking. Maps to the shipped fix in 2607.1. Attempted as a second user, since a check that only consulted your own bookings would pass a single-user version.

13 specs, green locally and on the runner.

Every assertion is red-checked

Each was deliberately inverted and observed to fail against real data, because a test that has never been seen to fail is a guess rather than a guard:

  • clash → genuinely 409 Conflict
  • scoping → the other user's listing really is [] while the booking exists
  • PKCE → a real code_challenge_method=S256 and base64url challenge on the wire

Controls, so nothing passes for the wrong reason

  • A non-overlapping slot must still be accepted, or a backend that rejected everything would look like perfect clash detection
  • You can see your own booking, or "nobody sees anything" would read as a working privacy boundary
  • The desk frees up after deletion, since a cancelled booking that still blocks is harder to diagnose than a plain double-booking

Also corrects the contract

Lockers and parking were listed as following the desk metadata pattern. They don't. Lockers come from banks and then lockers within them; parking needs level zones tagged parking plus a separate spaces API. Both are materially more setup than desks, and the rows now say so rather than implying quick wins.

🤖 Generated with Claude Code

camreeves and others added 3 commits August 3, 2026 18:05
Closes the last outstanding P0. login.spec.ts proves a login works and that the
token is usable; this proves the handshake that produced it was safe, which is a
different question. A client that leaked a secret or quietly dropped PKCE would
still end up with a valid token and a working app, so nothing in the response
tells you the exchange was sound. You have to look at what the browser sent.

loginViaUI now records every /auth/* request it observes, and the new spec asserts
S256 (never "plain"), a real base64url challenge, a code_verifier, the expected
client_id, and no client_secret anywhere. Plus: the password never appears in a
URL, and only ever reaches /auth/signin.

Red-checked. These assertions previously lived in tasks/PPT-2536/, where nothing
ran them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers WP-E2E-08 and AUTH-E2E-05. A second seeded user cannot see the booking in
their listing, and cannot delete it; the owner still can. Includes a control
asserting you can see your own booking, otherwise "nobody sees anything" would
pass as success.

This locks down something we learned the hard way: GET /bookings is scoped to the
caller. An early leak check written as an admin reported zero bookings while the
database plainly held one. It is both a privacy boundary and a trap for anyone
writing tooling, and a regression would leak quietly rather than fail loudly.

Red-checked: inverting the assertion shows the other user's listing really is
empty while the booking exists.

Also corrects two rows in the contract. Lockers and parking do NOT follow the desk
metadata pattern as claimed - lockers come from banks then lockers within them,
parking needs level zones tagged `parking` plus a separate spaces API. Both are
more setup than desks, and the contract now says so rather than implying they are
quick wins.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Maps to a shipped fix, "Fix rejecting overlapping bookings on desk assignment"
(2607.1). Double-booking is the kind of regression that doesn't announce itself:
nothing errors, nobody notices, and two people turn up to the same desk on
Tuesday.

Attempted as a SECOND user deliberately, because that's the real scenario and
because a clash check that only consulted your own bookings would still pass a
single-user version of this. Covers the identical slot and a partial overlap,
which is the case a naive check misses.

Two controls, so the test can't pass for the wrong reason: a genuinely
non-overlapping slot must still be accepted (otherwise a backend that rejected
everything would look correct), and the desk must free up once the booking is
deleted (a cancelled booking that still blocks the desk is harder to diagnose than
a plain double-booking).

Red-checked: the API really does return 409.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
frontend-templates Ignored Ignored Preview Aug 5, 2026 12:06pm

@MrYuion MrYuion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

This is valuable coverage, and the controls around the happy paths are thoughtful. I found two false-positive paths that weaken the guarantees these specs claim to provide, plus one cleanup issue that can poison retries.

Changes requested

  1. P1 — Bind the PKCE challenge to its verifier
    pkce.spec.ts:44–62

    The current assertions only establish that a syntactically plausible challenge and some verifier were sent. If the client sent unrelated values and the backend stopped enforcing PKCE, the exchange could return 200 and this test would still pass—the security regression this spec is intended to catch.

    Please parse code_verifier from exchange.postData, calculate its SHA-256 base64url value, and compare it exactly with challenge. An S256 challenge should also be exactly 43 base64url characters.

  2. P1 — Assert 409 Conflict, not every error response
    desk-clash.spec.ts:100–112 · desk-clash.spec.ts:144–145

    The workflow already documents intermittent DB::ConnectionLost 500s on booking POSTs. With >= 400, one of those infrastructure failures is treated as successful clash detection. The PR notes that the real behavior was red-checked as 409, so these assertions should use toBe(409).

  3. P2 — Clean up unexpectedly accepted second-user bookings
    desk-clash.spec.ts:98–125

    If either exact or partial returns 2xx, its assertion throws before the returned booking ID is captured. The finally block deletes only the owner's first booking. Because booking listings are caller-scoped, the next attempt's releaseAsset(staffApi, ...) cannot see or delete the leaked second-user booking, so the retry fails during setup.

    Please retain IDs from any successful second-user responses and delete them through other before disposing the context.

The advisory E2E check is green, but these issues are specifically false-positive and failure-path problems, so a successful run does not exercise them.

Rules out client disconnects as the trigger for the connection poisoning:
aborting requests mid-flight strands nothing, while a burst with no aborts
at all does. Kept next to the burst reproducer because a diagnostic that
lives only in a ticket comment rots.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.

2 participants