85 lines
5.7 KiB
Markdown
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.
|