111 lines
8.3 KiB
Markdown
111 lines
8.3 KiB
Markdown
# 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 `<b>`.
|
|
|
|
## 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.
|