6.5 KiB
6.5 KiB
QA procedure: stale_token_invalidation
Package: test (black-box), extending test/user_test.go (mockUserRepo) and reusing the
router/handler wiring pattern from test/account_verification_http_test.go and
test/reset_password_http_test.go (or equivalent).
Test infrastructure changes needed
mockUserReponeeds aTokenVersion uint32field on its storeduser.Uservalues, and itsResetPasswordfake must increment it by 1 alongside setting the password hash and clearing the reset code, mirroring the production contract this slice adds.- HTTP-level checks wire
mockUserRepo,useCases.NewResetPasswordUseCase(repo, hasher),useCases.NewAuthenticateUserUseCase(repo, hasher),useCases.NewGetUserUseCase(repo), the article use cases, and the rest of the existing container intoauth.NewHandler(...)andarticle.NewHandler(...)with a testjwtauth.New("HS256", []byte("test-secret"), nil). - Build two
chi.NewRouter()instances (or one router mounting both) that mirrorapi/router.go:- the article protected group:
jwtauth.Verifier(token)→ (mounted once, top-level, perInitBackendRoutes) → inside the group:jwtauth.Authenticator(token)→middleware.RequireCurrentToken(getUserUseCase)→middleware.RequireVerifiedUser(getUserUseCase)→POST /article; - the auth protected group:
jwtauth.Verifier(token)→jwtauth.Authenticator(token)→middleware.RequireCurrentToken(getUserUseCase)→POST /verify,DELETE /register; - the auth public routes
POST /login,POST /password-reset/confirm,POST /registermounted unprotected, as today.
- the article protected group:
- Call
validator.InitValidator()once per test. - Mint jwt cookies directly with
token.Encode(map[string]any{"user_id": uint32(7), "token_version": uint32(0)})for "pre-reset" cookies; decode aSet-Cookieresponse header's jwt withjwtauth.New(...).Decode(tokenString)or by parsing the JWT payload to assert itstoken_versionclaim, mirroring howsecureAttributeFromResponseintest/runtime_settings_injection_http_test.goalready parsesSet-Cookie.
HTTP-level checks
- Seed user id 7 "alice",
is_verified: true,TokenVersion: 0,PasswordResetCode= bcrypt hash of "reset-code-1",PasswordResetExpirationone hour ahead. Mint a jwt cookie encoding{"user_id": 7, "token_version": 0}.POST /v1/articlewith that cookie and a valid body → assert200.POST /v1/auth/password-reset/confirmwith{"email":"alice@example.com","code":"reset-code-1","password":"newpassword1"}→ assert200andrepo.users[7].TokenVersion == 1.POST /v1/articleagain with the same pre-reset cookie → assert401, body equalsapperr.UnauthorizedErrorMessage, and the article use case's create path was not invoked (article count in the fake repo unchanged).POST /v1/auth/loginwith{"username":"alice","password":"newpassword1"}→ assert200, ajwtSet-Cookieis present, and decoding it showstoken_version == 1.POST /v1/articlewith that new cookie → assert200.
- Same seed as case 6 minus the article calls. After the password reset:
POST /v1/auth/verifywith the pre-reset cookie and{"code":"anything"}→ assert401, body equalsapperr.UnauthorizedErrorMessage.DELETE /v1/auth/registerwith the pre-reset cookie → assert401, body equalsapperr.UnauthorizedErrorMessage, andrepo.Get(7)still succeeds.
- Double-reset case: seed as in 6, reset once (
TokenVersion→ 1), log in (capture "interim" cookie,token_version: 1), re-seedPasswordResetCodeto bcrypt hash of "reset-code-2" with a fresh expiration, reset again with{"email":"alice@example.com","code":"reset-code-2","password":"newerpassword1"}→ assertTokenVersion == 2. Assert both the original (token_version: 0) and interim (token_version: 1) cookies now get401onPOST /v1/article, and a jwt minted after logging in withnewerpassword1(token_version: 2) gets200. - Isolation case: seed user id 7 "alice" as in 6 and user id 9 "bob",
is_verified: true,TokenVersion: 0. Mint a cookie for user id 9 encodingtoken_version: 0. Reset alice's password. Assertrepo.users[9].TokenVersionis still0, andPOST /v1/articlewith bob's cookie still returns200. - Cancelled-registration regression, table-driven over
POST /v1/auth/verify(body{"code":"anything"}) andDELETE /v1/auth/register(no body): seed user id 7 "alice",is_verified: false,TokenVersion: 0. Mint a cookie encoding{"user_id":7,"token_version":0}.DELETE /v1/auth/registerwith it → assert200. Then send the pre-cancellation cookie against each of the two routes in turn (fresh repo/router state per sub-case, or re-running the cancel step) → assert401and bodyapperr.UnauthorizedErrorMessagefor both. - Missing-claim case: seed user id 7,
is_verified: true,TokenVersion: 0. Mint a jwt withtoken.Encode(map[string]any{"user_id": uint32(7)})(notoken_versionkey at all).POST /v1/article→ assert401, bodyapperr.UnauthorizedErrorMessage. - Wrong-type case: mint a jwt with
token.Encode(map[string]any{"user_id": uint32(7), "token_version": "0"})(string, not a number).POST /v1/article→ assert401, bodyapperr.UnauthorizedErrorMessage, and confirm the test process does not panic (norecoverneeded — a crash would fail the test run outright).
Use-case-level checks (no HTTP)
mockUserReposeeded with user id 3, email "alice@example.com",TokenVersion: 5,PasswordResetCode= bcrypt hash of "reset-code-1", expiration one hour ahead. BuildResetPasswordUseCasedirectly over the fake. CallResetPassword("alice@example.com", "reset-code-1", "newpassword1")→ asserterr == nilandrepo.users[3].TokenVersion == 6.
Manual verification (not exercised by the Go test)
- The SPA behaviour of being logged out / redirected to login when an API call comes back
401after a password reset or a cancelled registration (wherever that's handled globally, e.g. an axios/fetch interceptor) is out of scope for this slice's automated test and should be checked by hand against a running dev build. - Confirming against the real SQLite file that
TokenVersionpersists correctly throughgorm.AutoMigratefor existing rows (defaulting to0) is a manual check against a real migration, not re-verified by the in-memory fakes here.