# QA procedure: account_verification Package: `test` (black-box), extending `test/user_test.go` (`mockUserRepo`, `stubHasher`). ## Test infrastructure additions needed first 1. Add a `ClearVerificationCode(userId uint32) (user.User, error)` method to `mockUserRepo` mirroring the existing `UpdateVerificationStatus`/`UpdatePasswordResetCode` pattern: honor `m.fail.trigger("ClearVerificationCode")`, look the user up (return `user.ErrUserNotFound` if absent), set `IsVerified = true` and `VerificationCode = ""`, store it back, return it. Track a call counter/last-argument on the mock (or reuse the existing fail-switch style) so scenarios can assert it was or wasn't invoked. 2. `stubHasher.Compare(hashed, value)` already returns `nil` only when `hashed == "hashed:"+value` and an error otherwise, so seed `VerificationCode` in fixtures as `"hashed:abc123"` to represent "the bcrypt hash of abc123", and submit `"abc123"` as the matching code. ## Setup shared by every case 3. Call `validator.InitValidator()` once. 4. Use-case-level checks build `useCases.NewVerifyUserUseCase(repo, &stubHasher{})` where `repo` is a `mockUserRepo` pre-populated directly via `repo.users[id] = user.User{...}`. 5. HTTP-level checks wire `mockUserRepo`, `useCases.NewVerifyUserUseCase(repo, &stubHasher{})` and the other existing use cases into `auth.NewHandler(...)` (extended with the new `VerifyUseCase` parameter) with a test `jwtauth.New("HS256", []byte("test-secret"), nil)`. Mount `POST /v1/auth/verify` on a fresh `chi.NewRouter()` wrapped in `jwtauth.Verifier(token)` then `jwtauth.Authenticator(token)`, mirroring the protected group `api/router.go` adds inside `NewAuthRouter`. No live server. Mint jwt cookies with `token.Encode(map[string]any{"user_id": uint32(7)})` for the valid-token cases, following the convention in `features/article_publishing.qa.md`. ## Use-case-level checks (no HTTP) 6. Repo has user id 3 with `VerificationCode: "hashed:right-code"`, `VerificationExpiration: time.Now().Add(time.Hour)`. Call `Verify(3, "wrong-code")`. Assert `errors.Is(err, user.ErrInvalidVerificationCode)`. Assert `ClearVerificationCode` was never called on the repo. 7. Same repo, `VerificationExpiration: time.Now().Add(-time.Hour)`. Call `Verify(3, "right-code")`. Assert `errors.Is(err, user.ErrVerificationCodeExpired)`. Assert `ClearVerificationCode` was never called. 8. Same repo, `VerificationExpiration: time.Now().Add(time.Hour)`. Call `Verify(3, "right-code")`. Assert `err == nil`, `result.IsVerified == true`, `result.VerificationCode == ""`. Assert the repo's stored record for id 3 now has `IsVerified == true` and `VerificationCode == ""`. 9. Repo has user id 3 with `VerificationCode: ""` (already verified/used). Call `Verify(3, "anything")`. Assert `errors.Is(err, user.ErrInvalidVerificationCode)`. ## HTTP-level checks (fakes, per the shared setup above) 10. Seed `mockUserRepo` with user id 7, username "alice", `IsVerified: false`, `VerificationCode: "hashed:abc123"`, `VerificationExpiration: time.Now().Add(time.Hour)`. Mint a valid jwt cookie for user id 7. `POST /v1/auth/verify` with `{"code":"abc123"}` → assert status `200`, response body decodes to a user JSON with `is_verified: true` and `verification_code: ""`, and the repo's record for id 7 now has `IsVerified == true` and `VerificationCode == ""`. 11. Same seed, wrong code `{"code":"wrong-code"}` → assert status `403`, body exactly `"The verification code is invalid or has expired."`, and the repo's record for id 7 is unchanged (`IsVerified == false`, `VerificationCode == "hashed:abc123"`). 12. Same seed but `VerificationExpiration: time.Now().Add(-time.Hour)`, correct code `{"code":"abc123"}` → assert status `403`, same body text as above, and the repo's record for id 7 unchanged. 13. Replay: repeat case 10's successful call, then immediately `POST /v1/auth/verify` again with the same body `{"code":"abc123"}` on the same router/repo → assert the second response status is `403` with the same body text (the cleared code no longer matches). 14. Table-driven over three "no valid user_id" cases, each against the case-10 seed and asserting status `401` and a response body equal to the new `apperr.UnauthorizedErrorMessage` constant, without the handler panicking (a panic inside `httptest`/chi would fail the test run directly, so simply completing with the asserted status is sufficient proof): - mint the jwt with `token.Encode(map[string]any{"role": "user"})` (no `user_id` key at all); - mint the jwt with `token.Encode(map[string]any{"user_id": "seven"})` (string, not a number); - mint the jwt with `token.Encode(map[string]any{"user_id": uint32(999)})` (well-formed but no such user in the repo). After each sub-case, assert the repo's record for id 7 is still `IsVerified == false`. 15. A request with no `jwt` cookie at all against the same router → assert status `401` (produced by `jwtauth.Authenticator` itself, before the handler runs); do not assert an exact body for this one case since it is the library's own default text, not `apperr.UnauthorizedErrorMessage`. ## Manual verification (not exercised by the Go test) - The real SMTP-delivered verification link and the SPA's `/register/validate?c=...` page actually calling `POST /v1/auth/verify` with the code parsed from the URL, and redirecting or showing a message on success/failure, are out of scope for this slice's automated test and should be checked by hand against a running dev build. - The pre-existing bare type assertion in `auth.Handler.IsVerified` (`token.PrivateClaims()["user_id"].(float64)`) is untouched by this slice and remains a latent panic if that helper is ever wired into a route; it is not exercised here.