Files
RenewCMS/features/account_verification.qa.md

85 lines
5.7 KiB
Markdown

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