test(e2e): mail-sink helper + un-fixme reset/magic-link/change-email (BUNYIP-150) #220

Merged
nrupard merged 3 commits from feat/bunyip-150-e2e-mail-sink into main 2026-06-24 21:55:31 +02:00
Owner

What

Un-fixme the three email-driven E2E specs by reading their token-links out of the staging Mailpit sink. Part 3 (final) of BUNYIP-150.

How

  • e2e/lib/mail-sink.ts: clearMailbox() + waitForLink(toAddress, linkRe) over Mailpit's HTTP API (/api/v1/search?query=to:<addr> then /api/v1/message/<id>), with the basicAuth credentials parsed out of E2E_MAIL_SINK_URL into an Authorization header. Filters by the link pattern so the async welcome mail to the same address is ignored.
  • e2e/lib/accounts.ts: register a throwaway, run-tagged account via POST /v1/auth/register (no email confirmation gate), and self-delete via DELETE /v1/users/me.
  • Specs rewritten to drive over the JSON API + the sink rather than browser forms (robust against the BUNYIP-168 fill() flakiness, and no shared-storageState risk):
    • auth/magic-link.spec.ts: request /magic-link, read the token, verify on a fresh context, assert it reads /v1/auth/memberships.
    • auth/password-reset.spec.ts: request /password-reset, read the token, confirm a new password, assert the new password logs in.
    • account/change-email.spec.ts: request the change, read the verification link sent to the NEW address, confirm the token, assert the new email. Pins storageState: undefined so it can never mutate the shared login.
  • env.ts gains optional mailSinkURL; .env.example, e2e/README.md, and dev-docs/e2e.md document it; e2e.yml resolves E2E_MAIL_SINK_URL to the staging secret on staging and empty on production.

Each spec test.skips when env.mailSinkURL is unset, so production and an unprovisioned staging skip these rather than fail. Disposable accounts self-delete in a finally and carry the run tag.

Dependencies / merge order

  1. psa-systems/bunyip #219 (plaintext SmtpTls::None) on main + rebuilt to :latest.
  2. niceguyit/docker #143 (the bunyip-mailpit staging deployment + SMTP repoint) deployed on c-01.
  3. Operator sets the Forgejo secret E2E_STAGING_MAIL_SINK_URL.

Until the secret is set the three specs SKIP, so this PR is safe to merge at any point without breaking CI; coverage activates once the sink is live.

Verification

  • tsc --noEmit clean.
  • playwright test --list: the three specs now collect as real tests (no longer test.fixme); 18 tests total.
  • Full green run requires the live staging sink (operator steps above), so this PR is verified structurally here and end-to-end on staging once the secret lands.

Follow-ups (not blocking)

  • auth/signup.spec.ts remains test.fixme (separate follow-up under BUNYIP-150's umbrella).
  • Disposable-account cleanup is per-spec self-delete (covers normal + assertion-failure paths). A teardown sweep of orphaned tagged accounts from a hard crash would need admin credentials wired into the suite; deferred.

🤖 Generated with Claude Code

## What Un-fixme the three email-driven E2E specs by reading their token-links out of the staging Mailpit sink. Part 3 (final) of BUNYIP-150. ## How - `e2e/lib/mail-sink.ts`: `clearMailbox()` + `waitForLink(toAddress, linkRe)` over Mailpit's HTTP API (`/api/v1/search?query=to:<addr>` then `/api/v1/message/<id>`), with the basicAuth credentials parsed out of `E2E_MAIL_SINK_URL` into an `Authorization` header. Filters by the link pattern so the async welcome mail to the same address is ignored. - `e2e/lib/accounts.ts`: register a throwaway, run-tagged account via `POST /v1/auth/register` (no email confirmation gate), and self-delete via `DELETE /v1/users/me`. - Specs rewritten to drive over the JSON API + the sink rather than browser forms (robust against the BUNYIP-168 `fill()` flakiness, and no shared-storageState risk): - `auth/magic-link.spec.ts`: request `/magic-link`, read the token, verify on a fresh context, assert it reads `/v1/auth/memberships`. - `auth/password-reset.spec.ts`: request `/password-reset`, read the token, confirm a new password, assert the new password logs in. - `account/change-email.spec.ts`: request the change, read the verification link sent to the NEW address, confirm the token, assert the new email. Pins `storageState: undefined` so it can never mutate the shared login. - `env.ts` gains optional `mailSinkURL`; `.env.example`, `e2e/README.md`, and `dev-docs/e2e.md` document it; `e2e.yml` resolves `E2E_MAIL_SINK_URL` to the staging secret on staging and empty on production. Each spec `test.skip`s when `env.mailSinkURL` is unset, so production and an unprovisioned staging skip these rather than fail. Disposable accounts self-delete in a `finally` and carry the run tag. ## Dependencies / merge order 1. psa-systems/bunyip #219 (plaintext `SmtpTls::None`) on main + rebuilt to `:latest`. 2. niceguyit/docker #143 (the `bunyip-mailpit` staging deployment + SMTP repoint) deployed on c-01. 3. Operator sets the Forgejo secret `E2E_STAGING_MAIL_SINK_URL`. Until the secret is set the three specs SKIP, so this PR is safe to merge at any point without breaking CI; coverage activates once the sink is live. ## Verification - `tsc --noEmit` clean. - `playwright test --list`: the three specs now collect as real tests (no longer `test.fixme`); 18 tests total. - Full green run requires the live staging sink (operator steps above), so this PR is verified structurally here and end-to-end on staging once the secret lands. ## Follow-ups (not blocking) - `auth/signup.spec.ts` remains `test.fixme` (separate follow-up under BUNYIP-150's umbrella). - Disposable-account cleanup is per-spec self-delete (covers normal + assertion-failure paths). A teardown sweep of orphaned tagged accounts from a hard crash would need admin credentials wired into the suite; deferred. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
test(e2e): mail-sink helper + un-fixme reset/magic-link/change-email (BUNYIP-150)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 16s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m37s
e95ce8de9f
Read email token-links out of the staging Mailpit sink so the email-driven E2E flows can run. Adds lib/mail-sink.ts (clearMailbox + waitForLink over Mailpit's HTTP API, with the basicAuth credentials parsed out of E2E_MAIL_SINK_URL into a header) and lib/accounts.ts (register a throwaway account via POST /v1/auth/register, which needs no email confirmation, and self-delete via DELETE /v1/users/me).

Un-fixme the three specs, driven over the JSON API + the sink for robustness (no browser form-filling, which is fragile on the CI runner per BUNYIP-168, and no shared-storageState risk):

- magic-link: register a disposable account, request /magic-link, read the token from the sink, verify it on a fresh context, assert that context reads /v1/auth/memberships.
- password-reset: register a disposable account, request /password-reset, read the token, confirm a new password, assert the new password logs in.
- change-email: register a disposable account, request the change, read the verification link sent to the NEW address, confirm the token, assert the account now reports the new email. Pins storageState: undefined so it can never mutate the shared login.

Each spec guards on env.mailSinkURL with test.skip, so production (no sink) and an unprovisioned staging skip these rather than fail. The e2e.yml env block resolves E2E_MAIL_SINK_URL to the staging secret on staging and empty on production. Disposable accounts self-delete in a finally and carry this run's tag for recognisability.

Depends on the plaintext SMTP arm (SmtpTls::None) and the staging bunyip-mailpit deployment; until E2E_STAGING_MAIL_SINK_URL is set these specs skip, so this can merge before the sink is live without breaking CI.

#BUNYIP-150

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(e2e): read the mail sink via Stalwart JMAP instead of Mailpit (BUNYIP-150)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 25s
Check / fmt + clippy + build + tests (pull_request) Successful in 2m4s
f1e26def05
Reuse the existing Stalwart relay (mail.a8n.run) as the E2E mail sink rather than standing up a Mailpit service. Staging bunyip-api keeps sending through Stalwart over TLS unchanged, so this drops both the separate Mailpit deployment and the plaintext SmtpTls::None app change (those PRs are closed).

lib/mail-sink.ts now reads over JMAP: it fetches the JMAP session from E2E_MAIL_SINK_URL (the sink mailbox credentials embedded as basicAuth userinfo, e.g. https://e2e%40a8n.run:pass@mail.a8n.run), then Email/query + Email/get filtered by recipient, extracts the token-link from the body, and destroys the message. Disposable accounts now address a unique plus-subaddress of the sink mailbox (e2e+<run-tag>@a8n.run), which Stalwart delivers into the one mailbox the suite reads; the unique recipient isolates each test's mail, so no mailbox clearing is needed.

The three specs are otherwise unchanged (disposable account, API-driven, test.skip when E2E_MAIL_SINK_URL is unset). Docs and the e2e.yml comment updated; the CI secret selector (E2E_STAGING_MAIL_SINK_URL) is unchanged.

#BUNYIP-150

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
test(e2e): use nate@a8n.run sink mailbox + guard destroy to exact subaddress (BUNYIP-150)
All checks were successful
E2E / Playwright against deployment (pull_request) Successful in 47s
Check / fmt + clippy + build + tests (pull_request) Successful in 4m33s
Create release / Create release from merged PR (pull_request) Has been skipped
4085b4c663
The dedicated e2e@a8n.run mailbox does not exist yet, so use nate@a8n.run for now. The mailbox is taken from the E2E_MAIL_SINK_URL userinfo, so this is a secret/docs change, not a code change - switching to e2e@a8n.run later is just a new secret value.

Because nate@a8n.run is a personal mailbox, harden the reader: Email/get now fetches the `to` header and waitForLink only matches (and only destroys) a message whose To is the EXACT unique plus-subaddress the test used. A JMAP `to` filter that normalised the subaddress to the base mailbox can therefore never cause real mail to be matched or deleted.

#BUNYIP-150

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/bunyip-150-e2e-mail-sink 2026-06-24 21:55:31 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!220
No description provided.