130 lines
8.3 KiB
Markdown
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.
|