Files
RenewCMS/features/verified_account_required.qa.md

8.3 KiB

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

  1. Call validator.InitValidator() once.
  2. Build a test jwtauth.New("HS256", []byte("test-secret"), nil).
  3. 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.
  4. 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.
  5. 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.
  6. Seed mockUserRepo per scenario via repo.users[1] = user.User{ID: 1, IsVerified: ...}.

HTTP checks: protected routes reject an unverified caller

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

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

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

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

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

  1. 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.
  2. 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.
  3. 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).
  4. 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

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