Files
RenewCMS/features/account_change_email.qa.md
T

8.3 KiB

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

  1. Call validator.InitValidator() once.
  2. 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.
  3. Use a test authtoken instance and mint jwt cookies carrying user_id and token_version. Seed user 7 "marta" (IsVerified: true).
  4. No live server, real SQLite file or SMTP.

Use-case checks

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. Cancel. Assert fields cleared, nil error when nothing is pending, and an audit event only when something was cancelled.
  7. 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.
  8. 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

  1. 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.
  2. 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.
  3. 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.".
  4. 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.
  5. 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).
  6. GET /auth/me returns pending_email as null, then as the address after a request.

Mailer checks (no SMTP)

  1. 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.