Files
RenewCMS/features/account_verification.qa.md

5.7 KiB

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

  1. Call validator.InitValidator() once.
  2. Use-case-level checks build useCases.NewVerifyUserUseCase(repo, &stubHasher{}) where repo is a mockUserRepo pre-populated directly via repo.users[id] = user.User{...}.
  3. 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)

  1. 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.
  2. Same repo, VerificationExpiration: time.Now().Add(-time.Hour). Call Verify(3, "right-code"). Assert errors.Is(err, user.ErrVerificationCodeExpired). Assert ClearVerificationCode was never called.
  3. 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 == "".
  4. 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)

  1. 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 == "".
  2. 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").
  3. 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.
  4. 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).
  5. 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.
  6. 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.