# QA procedure: verified_account_required Package: `test` (black-box), extending `test/article_test.go` (`mockArticleRepo`) and `test/user_test.go` (`mockUserRepo`). This slice's router mount touches both the article and auth routers, so the HTTP-level checks live in a new `test/verified_account_required_http_test.go`. ## Test infrastructure additions needed first 1. `mockUserRepo.Get` already honors `m.fail.trigger("Get")` and already returns `user.ErrUserNotFound` for an absent id (`test/user_test.go`), so no new mock methods are required on the user side. 2. `mockArticleRepo` already supports seeding via `repo.articles[id] = article.Article{...}` and a `failNext(...)` helper (`test/article_test.go`); no changes needed there either. 3. Build the middleware under test directly: `middleware.RequireVerifiedUser(getUser *useCases.GetUserUseCase) func(http.Handler) http.Handler`, exported from `api/middleware`, and `claims.UserId(r *http.Request) (uint32, bool)`, exported from `api/pkg/claims`. Construct `useCases.NewGetUserUseCase(repo)` over a `mockUserRepo`. ## Setup shared by every case 4. Call `validator.InitValidator()` once. 5. Build a test `jwtauth.New("HS256", []byte("test-secret"), nil)`. 6. Wire `article.NewHandler(create, get, list, delete, update, getUser)` (extended with the new `GetUserUseCase` parameter) over a `mockArticleRepo` and the `mockUserRepo`/`GetUserUseCase` from step 3. 7. Mount the routes on a fresh `chi.NewRouter()` mirroring the new `api/router.go` shape: - `r.Use(jwtauth.Verifier(token))` on the whole `/article` group (parses, never rejects) - `r.Get("/", articleHandler.ListArticles)` directly under that Verifier-only group - a nested `r.Group` adding `jwtauth.Authenticator(token)` then `middleware.RequireVerifiedUser(getUser)`, containing `r.Get("/{id}", ...)`, `r.Post("/", ...)`, `r.Delete("/{id}", ...)`, `r.Patch("/{id}", ...)`, `r.Patch("/{id}/publish", ...)`, `r.Patch("/{id}/unpublish", ...)` No live server. 8. Mint jwt cookies with `token.Encode(map[string]any{"user_id": uint32(1)})` for the valid-token cases, following the convention in `features/article_publishing.qa.md`. 9. Seed `mockUserRepo` per scenario via `repo.users[1] = user.User{ID: 1, IsVerified: ...}`. ## HTTP checks: protected routes reject an unverified caller 10. Seed `mockUserRepo` with user id 1, `IsVerified: false`; seed `mockArticleRepo` with article id 1. Table-driven over `GET /v1/article/1`, `POST /v1/article/` (body `{"title":"Valid Title","body":"Some content"}`), `DELETE /v1/article/1`, `PATCH /v1/article/1` (body `{"body":"Updated"}`), `PATCH /v1/article/1/publish`, `PATCH /v1/article/1/unpublish`, each sent with the valid cookie from step 8 → assert status `403` and body exactly `apperr.VerificationRequiredErrorMessage`. Assert the article repository was not mutated by any of the write calls. ## HTTP checks: a verified caller is unaffected 12. Seed `mockUserRepo` with user id 1, `IsVerified: true`. `POST /v1/article/` with the valid cookie and body `{"title":"Valid Title","body":"Some content"}` → assert status `201` and the response body decodes to the created `article.Article`. ## HTTP checks: an unresolvable caller still answers 401, not 403 13. Table-driven over five token shapes, each against a fresh seed of user id 1, `IsVerified: false`, sent as `POST /v1/article/` with body `{"title":"Valid Title","body":"Some content"}`, asserting status `401` and body exactly `apperr.UnauthorizedErrorMessage`: - 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": "one"})` (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); - mint the jwt with `token.Encode(map[string]any{"user_id": -1.0})` (negative); - mint the jwt with `token.Encode(map[string]any{"user_id": float64(4294967296)})` (one past the uint32 maximum). 13a. Seed `mockUserRepo` with user id 7, `IsVerified: true`; seed `mockArticleRepo` empty. Mint a jwt with `token.Encode(map[string]any{"user_id": 7.5})` (a fractional value that would previously have truncated to the real user id 7). `POST /v1/article/` with that cookie and body `{"title":"Valid Title","body":"Some content"}` → assert status `401`, body exactly `apperr.UnauthorizedErrorMessage`, and that the article repository gained no new article (proving the fractional claim was rejected rather than silently accepted as user 7). 13b. Extend `test/claims_test.go` with a `TestClaimsUserId` set of subtests mirroring the existing `TestClaimsTokenVersion` pattern: a well-formed `uint32` claim returns `ok == true` with the matching value; a missing key, a non-numeric value (string), a negative value, a fractional value (e.g. `7.5`), and a value one past `math.MaxUint32` (`4294967296`) all return `ok == false`. This exercises `claims.UserId` directly, independent of the HTTP round-trip. ## HTTP checks: no session at all is still 401 14. `GET /v1/article/1` and `POST /v1/article/` with no `jwt` cookie → assert status `401` for both (produced by `jwtauth.Authenticator` itself, before `RequireVerifiedUser` runs); do not assert an exact body. ## HTTP checks: a user-repository failure while resolving the caller 15. Mint a valid jwt cookie for user id 1. Configure `mockUserRepo`'s `failSwitch` so the next call to `Get` returns a generic error (not `user.ErrUserNotFound`), via the existing `failNext("Get")`-style helper. `DELETE /v1/article/1` with that cookie, article repository seeded with article id 1 → assert status `500`, body exactly `"Internal server error"`, and that the article repository's article with id 1 still exists (the delete use case was never reached). ## HTTP checks: the offline-inclusive listing is gated by verification 16. Seed `mockUserRepo` with user id 1, `IsVerified: false`; seed `mockArticleRepo` with an online article id 1 and an offline article id 2. `GET /v1/article/` with the valid cookie from step 8 → assert status `200`; decode the JSON array and assert it has exactly one element, id `1`. 17. Same article seed, `mockUserRepo` user id 1 with `IsVerified: true` → `GET /v1/article/` with the same cookie → assert status `200`; decode the JSON array and assert both ids `1` and `2` are present. 18. Same article seed, no user with id 999 in `mockUserRepo`, jwt cookie minted for user id 999 → `GET /v1/article/` with that cookie → assert status `200`; assert exactly id `1` is present (fallback to online-only on `user.ErrUserNotFound`). 19. Same article seed, valid cookie for user id 1, `mockUserRepo`'s `failSwitch` set so the next `Get` call returns a generic error → `GET /v1/article/` with that cookie → assert status `200`; assert exactly id `1` is present (a user-lookup failure falls back to online-only rather than failing the request). ## Regression check on existing fixtures 20. `features/article_publishing.feature`, `features/edit_article_body.feature`, `features/upload_article_image.feature` and the corresponding `test/*_test.go` files mint a valid jwt cookie for "a user with id 1" without asserting anything about that user's `IsVerified` status. After this slice, those HTTP tests' `mockUserRepo` fixture must seed user id 1 with `IsVerified: true` (or the equivalent id used in each file), or every one of those protected-route assertions will start failing with `403` instead of the expected success/404/400 status. This is a required update to those test files' setup, not a new behavior; call it out explicitly when touching them so the regression is caught at review time rather than by a surprise `task test` failure. ## Manual verification (not exercised by the Go test) - The SPA-visible behaviour of an unverified, logged-in user landing on the back-office article screens (create/edit/publish) and seeing a 403 instead of the normal editing UI, and the front-office/back-office listing showing only published articles to that same unverified session, is out of scope for this slice's automated test and should be checked by hand against a running dev build with an unverified test account. - No SMTP involvement.