94 lines
6.5 KiB
Markdown
94 lines
6.5 KiB
Markdown
# 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
|
|
|
|
1. `mockUserRepo` needs a `TokenVersion uint32` field on its stored `user.User` values, and its
|
|
`ResetPassword` fake must increment it by 1 alongside setting the password hash and clearing the
|
|
reset code, mirroring the production contract this slice adds.
|
|
2. 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 into `auth.NewHandler(...)` and
|
|
`article.NewHandler(...)` with a test `jwtauth.New("HS256", []byte("test-secret"), nil)`.
|
|
3. Build two `chi.NewRouter()` instances (or one router mounting both) that mirror
|
|
`api/router.go`:
|
|
- the article protected group: `jwtauth.Verifier(token)` → (mounted once, top-level, per
|
|
`InitBackendRoutes`) → 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 /register` mounted
|
|
unprotected, as today.
|
|
4. Call `validator.InitValidator()` once per test.
|
|
5. Mint jwt cookies directly with `token.Encode(map[string]any{"user_id": uint32(7), "token_version":
|
|
uint32(0)})` for "pre-reset" cookies; decode a `Set-Cookie` response header's jwt with
|
|
`jwtauth.New(...).Decode(tokenString)` or by parsing the JWT payload to assert its
|
|
`token_version` claim, mirroring how `secureAttributeFromResponse` in
|
|
`test/runtime_settings_injection_http_test.go` already parses `Set-Cookie`.
|
|
|
|
## HTTP-level checks
|
|
|
|
6. Seed user id 7 "alice", `is_verified: true`, `TokenVersion: 0`, `PasswordResetCode` = bcrypt hash
|
|
of "reset-code-1", `PasswordResetExpiration` one hour ahead. Mint a jwt cookie encoding
|
|
`{"user_id": 7, "token_version": 0}`.
|
|
- `POST /v1/article` with that cookie and a valid body → assert `200`.
|
|
- `POST /v1/auth/password-reset/confirm` with
|
|
`{"email":"alice@example.com","code":"reset-code-1","password":"newpassword1"}` → assert `200`
|
|
and `repo.users[7].TokenVersion == 1`.
|
|
- `POST /v1/article` again with the *same* pre-reset cookie → assert `401`, body equals
|
|
`apperr.UnauthorizedErrorMessage`, and the article use case's create path was not invoked
|
|
(article count in the fake repo unchanged).
|
|
- `POST /v1/auth/login` with `{"username":"alice","password":"newpassword1"}` → assert `200`, a
|
|
`jwt` `Set-Cookie` is present, and decoding it shows `token_version == 1`.
|
|
- `POST /v1/article` with that new cookie → assert `200`.
|
|
7. Same seed as case 6 minus the article calls. After the password reset:
|
|
- `POST /v1/auth/verify` with the pre-reset cookie and `{"code":"anything"}` → assert `401`, body
|
|
equals `apperr.UnauthorizedErrorMessage`.
|
|
- `DELETE /v1/auth/register` with the pre-reset cookie → assert `401`, body equals
|
|
`apperr.UnauthorizedErrorMessage`, and `repo.Get(7)` still succeeds.
|
|
8. Double-reset case: seed as in 6, reset once (`TokenVersion` → 1), log in (capture "interim"
|
|
cookie, `token_version: 1`), re-seed `PasswordResetCode` to bcrypt hash of "reset-code-2" with a
|
|
fresh expiration, reset again with `{"email":"alice@example.com","code":"reset-code-2","password":"newerpassword1"}`
|
|
→ assert `TokenVersion == 2`. Assert both the original (`token_version: 0`) and interim
|
|
(`token_version: 1`) cookies now get `401` on `POST /v1/article`, and a jwt minted after logging
|
|
in with `newerpassword1` (`token_version: 2`) gets `200`.
|
|
9. 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 encoding `token_version: 0`. Reset alice's
|
|
password. Assert `repo.users[9].TokenVersion` is still `0`, and `POST /v1/article` with bob's
|
|
cookie still returns `200`.
|
|
10. Cancelled-registration regression, table-driven over `POST /v1/auth/verify` (body
|
|
`{"code":"anything"}`) and `DELETE /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/register` with it → assert `200`. 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) → assert `401` and body `apperr.UnauthorizedErrorMessage` for both.
|
|
11. Missing-claim case: seed user id 7, `is_verified: true`, `TokenVersion: 0`. Mint a jwt with
|
|
`token.Encode(map[string]any{"user_id": uint32(7)})` (no `token_version` key at all).
|
|
`POST /v1/article` → assert `401`, body `apperr.UnauthorizedErrorMessage`.
|
|
12. 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` → assert `401`, body `apperr.UnauthorizedErrorMessage`, and confirm
|
|
the test process does not panic (no `recover` needed — a crash would fail the test run outright).
|
|
|
|
## Use-case-level checks (no HTTP)
|
|
|
|
13. `mockUserRepo` seeded with user id 3, email "alice@example.com", `TokenVersion: 5`,
|
|
`PasswordResetCode` = bcrypt hash of "reset-code-1", expiration one hour ahead. Build
|
|
`ResetPasswordUseCase` directly over the fake. Call
|
|
`ResetPassword("alice@example.com", "reset-code-1", "newpassword1")` → assert `err == nil` and
|
|
`repo.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 `401`
|
|
after 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 `TokenVersion` persists correctly through
|
|
`gorm.AutoMigrate` for existing rows (defaulting to `0`) is a manual check against a real
|
|
migration, not re-verified by the in-memory fakes here.
|