Files
RenewCMS/features/verified_account_required.qa.md

130 lines
8.3 KiB
Markdown

# 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.