5.7 KiB
5.7 KiB
QA procedure: account_verification
Package: test (black-box), extending test/user_test.go (mockUserRepo, stubHasher).
Test infrastructure additions needed first
- Add a
ClearVerificationCode(userId uint32) (user.User, error)method tomockUserRepomirroring the existingUpdateVerificationStatus/UpdatePasswordResetCodepattern: honorm.fail.trigger("ClearVerificationCode"), look the user up (returnuser.ErrUserNotFoundif absent), setIsVerified = trueandVerificationCode = "", 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. stubHasher.Compare(hashed, value)already returnsnilonly whenhashed == "hashed:"+valueand an error otherwise, so seedVerificationCodein fixtures as"hashed:abc123"to represent "the bcrypt hash of abc123", and submit"abc123"as the matching code.
Setup shared by every case
- Call
validator.InitValidator()once. - Use-case-level checks build
useCases.NewVerifyUserUseCase(repo, &stubHasher{})whererepois amockUserRepopre-populated directly viarepo.users[id] = user.User{...}. - HTTP-level checks wire
mockUserRepo,useCases.NewVerifyUserUseCase(repo, &stubHasher{})and the other existing use cases intoauth.NewHandler(...)(extended with the newVerifyUseCaseparameter) with a testjwtauth.New("HS256", []byte("test-secret"), nil). MountPOST /v1/auth/verifyon a freshchi.NewRouter()wrapped injwtauth.Verifier(token)thenjwtauth.Authenticator(token), mirroring the protected groupapi/router.goadds insideNewAuthRouter. No live server. Mint jwt cookies withtoken.Encode(map[string]any{"user_id": uint32(7)})for the valid-token cases, following the convention infeatures/article_publishing.qa.md.
Use-case-level checks (no HTTP)
- Repo has user id 3 with
VerificationCode: "hashed:right-code",VerificationExpiration: time.Now().Add(time.Hour). CallVerify(3, "wrong-code"). Asserterrors.Is(err, user.ErrInvalidVerificationCode). AssertClearVerificationCodewas never called on the repo. - Same repo,
VerificationExpiration: time.Now().Add(-time.Hour). CallVerify(3, "right-code"). Asserterrors.Is(err, user.ErrVerificationCodeExpired). AssertClearVerificationCodewas never called. - Same repo,
VerificationExpiration: time.Now().Add(time.Hour). CallVerify(3, "right-code"). Asserterr == nil,result.IsVerified == true,result.VerificationCode == "". Assert the repo's stored record for id 3 now hasIsVerified == trueandVerificationCode == "". - Repo has user id 3 with
VerificationCode: ""(already verified/used). CallVerify(3, "anything"). Asserterrors.Is(err, user.ErrInvalidVerificationCode).
HTTP-level checks (fakes, per the shared setup above)
- Seed
mockUserRepowith 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/verifywith{"code":"abc123"}→ assert status200, response body decodes to a user JSON withis_verified: trueandverification_code: "", and the repo's record for id 7 now hasIsVerified == trueandVerificationCode == "". - Same seed, wrong code
{"code":"wrong-code"}→ assert status403, 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"). - Same seed but
VerificationExpiration: time.Now().Add(-time.Hour), correct code{"code":"abc123"}→ assert status403, same body text as above, and the repo's record for id 7 unchanged. - Replay: repeat case 10's successful call, then immediately
POST /v1/auth/verifyagain with the same body{"code":"abc123"}on the same router/repo → assert the second response status is403with the same body text (the cleared code no longer matches). - Table-driven over three "no valid user_id" cases, each against the case-10 seed and asserting
status
401and a response body equal to the newapperr.UnauthorizedErrorMessageconstant, without the handler panicking (a panic insidehttptest/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"})(nouser_idkey 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 stillIsVerified == false.
- mint the jwt with
- A request with no
jwtcookie at all against the same router → assert status401(produced byjwtauth.Authenticatoritself, before the handler runs); do not assert an exact body for this one case since it is the library's own default text, notapperr.UnauthorizedErrorMessage.
Manual verification (not exercised by the Go test)
- The real SMTP-delivered verification link and the SPA's
/register/validate?c=...page actually callingPOST /v1/auth/verifywith 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.