# QA procedure: account_change_email Package: `test` (black-box), extending `test/user_test.go` (`mockUserRepo`, `stubHasher`) and `test/mail_test.go` (`mockMailRepo`, `recordedMail`, `failSwitch`). Reuse the existing recording audit fake. Fake type names must be unique across the package. ## Test infrastructure additions needed first 1. `mockUserRepo`: add the new repository methods for setting, clearing and confirming a pending change. - Each honours `fail.trigger` and returns `user.ErrUserNotFound` for an unknown id. - Confirm checks "email not held by another user" and returns `user.ErrEmailUnavailable`. - Add call counters, in the style of `clearVerificationCodeCalls`. 2. Seed fixtures with `stubHasher` hashes: `EmailChangeCode: "hashed:abc123"` stands for the raw code `abc123`. The password "limewash2026" is seeded as `"hashed:limewash2026"`. 3. A reusable helper builds the real `RequestEmailChangeUseCase`, `ResendEmailChangeUseCase`, `CancelEmailChangeUseCase` and `ConfirmEmailChangeUseCase` over `mockUserRepo`, `stubHasher`, `mockMailRepo`, the audit fake and `publicURL "https://cms.example.com"`. ## Setup shared by every case 4. Call `validator.InitValidator()` once. 5. Handler checks build the handler from the use cases via its `NewHandler`/named constructor. Mount the four routes on a fresh `chi.NewRouter()` that mirrors the `NewAuthRouter` session group: `middleware.RequireSession`, `authtoken.Verifier`, `AuditContext`, `RequireSignedIn`, `authtoken.Authenticator`, `RequireCurrentToken`, `RequireVerifiedUser`. Add the shared `middleware.NewUserRateLimiter(5, 15*time.Minute)` in front of change and resend only. 6. Use a test `authtoken` instance and mint jwt cookies carrying `user_id` and `token_version`. Seed user 7 "marta" (`IsVerified: true`). 7. No live server, real SQLite file or SMTP. ## Use-case checks 8. Request, valid. Assert: - nil error. - `PendingEmail` set, and `users.email` unchanged. - Stored code equals `"hashed:"+raw`, and the expiration is within 5 s of now+2h. - `SentEmails` has exactly two entries. - Entry 1 is `verify-new-email` to the new address; its data (type-assert `useCases.MailVerifyNewEmailData`) carries Host, Username and the raw code. - Entry 2 is `email-change-notice` to the old address, with NewEmail in its data. - One audit event `user.request_email_change`, success, `Security` true, with no `@` in its Params or Changes. 9. Request errors are table-driven, each asserted with `errors.Is`: - Wrong password gives `ErrWrongPassword`, with no mail, nothing stored and one failed security event. - Same address in different case gives `ErrSameEmail`. 10. Taken address, matched case-insensitively. Assert the same return value and the same stored `PendingEmail` as case 8, only the notice mail sent (none to the other user), and the same audit event shape. 11. Mail failure via `mailRepo.fail.failNext("Send")`. Capture the log (`log.SetOutput(&buf)` with a cleanup). Assert nil error, the pending change stored, a log line naming the failure, and neither the raw code nor its hash in the log. 12. Resend. Assert: - A new code and expiry, and the old raw code now fails `stubHasher.Compare`. - One `verify-new-email` mail and no notice. - `ErrNoPendingEmailChange` when nothing is pending. - Nothing sent for a taken address. 13. Cancel. Assert fields cleared, nil error when nothing is pending, and an audit event only when something was cancelled. 14. Confirm, success. Assert: - `users.email` is the new address, the pending fields are cleared, and `TokenVersion` is unchanged. - A second confirm returns `ErrInvalidEmailChangeCode`. - The `user.confirm_email_change` event is recorded. 15. Confirm, failures. Assert: - Expired, wrong, other user's, cancelled and none pending all return `ErrInvalidEmailChangeCode`. - Taken returns `ErrEmailUnavailable` and clears the pending change. ## HTTP checks 16. Request, valid and refused. Using the examples table of the feature, assert the status and exact body and that `SentEmails` is empty on refusals. The text for the 400 cases is the validator message; assert it equals `validator.Validate.Struct` output. 17. Taken-address response parity. Compare status, body and headers except Date between case 8 and case 10 using `reflect.DeepEqual` on the header map minus Date. 18. Auth gates, for each of the four routes: - No cookie gives 401 "You must be logged in to access this resource.". - A bearer API key gives 403 "This action needs a real session.". - An unverified user gives 403 "You must verify your account to access this resource.". 19. Rate limit. Send 5 valid-or-wrong-password requests mixed with resends, then assert the sixth is 429 with a numeric `Retry-After` and the body "Too many requests. Please try again later.". Assert DELETE and confirm still answer. 20. Resend, cancel and confirm statuses and bodies exactly as in the feature (200 JSON, 404 `no_pending_email_change`, 204 empty, 403 `invalid_email_change_code`, 409 `email_unavailable`). 21. `GET /auth/me` returns `pending_email` as null, then as the address after a request. ## Mailer checks (no SMTP) 22. Render `verify-new-email` and `email-change-notice` with `MailRepository.Render` and fake data. - Assert the link strings, the subjects, and that no `{{` remains. - Assert the HTML escapes a username such as ``. ## Vitest (SPA), `web/src/**/__tests__/` Mock `/v1` with the response in the second column. One spec per state, named with the design state name. Strings live in `messages.ts` in English and French under the same keys. I18n checks (extend the existing `web/src/i18n/*.spec.ts` style, e.g. `spa.spec.ts`): - Key parity: every new key exists in both `en` and `fr`, with no empty value and the same interpolation placeholders (new address, old address, countdown). - French render: for each state with `setLocale('fr')`, assert the French text is shown and no English string remains; switching `en`, `fr`, `en` restores the English copy. - English values equal the design copy verbatim. Flagged assumption: the French wording is authored by the coder because docs/design has none. French strings to author, for the user's review: the "Change email" link, the form labels and hint, the four field errors, "Sending…", "Waiting for verification" and its text, "Resend link", "Cancel change", "Resend in {time}", the three toast pairs (sent, resent, cancelled), the 429 and server notices, and all seven landing states (title, text, button). | State | Mocked /v1 | |---|---| | Default, Form open | `GET /auth/me` with pending_email null | | Invalid format, Same as current, Password missing | none (no request asserted) | | Wrong password (403) | `POST /auth/email/change` 403 "wrong_password" | | Sending | POST pending forever | | Pending, then reload | POST 200; `GET /auth/me` with pending_email | | Pending: resending / resent / resend rate limited | `POST /auth/email/resend` unresolved / 200 / 429 with Retry-After 270 | | Cancelled | `DELETE /auth/email/change` 204 | | Rate limited (429), Server or network error | POST 429; POST 500 and a rejected fetch | | Landing: all seven states | `POST /auth/email/confirm` per the feature table; the 401 state uses `GET /auth/me` 401 and then an inline login | Also assert: - The 403 `wrong_password` is not treated as a global unverified redirect. - Typed values are kept in the 429 and 500 states. - The code is read from `?c=` only. ## Visual Run `task visual:check -- account` and `task visual:check -- auth`. Every capture must be within 10 % at desktop and mobile, light and dark. No `exception` without the user's `approvedBy`. - Board data to reuse in `web/visual/states.ts` setups: username marta, old email marta@renew.example, new email marta.alves@atelier-renew.example, resend countdown 4:30 (Retry-After 270). - States: the 14 "Change email" states and the 7 "Verify new email" states of the Auth board. - Toast states (Pending, Pending: resent, Cancelled) capture the toast. - None of these is clock-sensitive. ## Manual verification - Real SMTP delivery and rendering of both mails in a real client and in dark mode. - Opening the link from a different browser to see the inline login, then the confirmation. - Keyboard and focus order: the email field on open, password focus after 403, focus return after Cancel. - Hover and motion of the toasts. - Real embedded frontend: open `/account/email/confirm?c=...` on the built binary.