8.7 KiB
8.7 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
mockUserRepo.Getalready honorsm.fail.trigger("Get")and already returnsuser.ErrUserNotFoundfor an absent id (test/user_test.go), so no new mock methods are required on the user side.mockArticleRepoalready supports seeding viarepo.articles[id] = article.Article{...}and afailNext(...)helper (test/article_test.go); no changes needed there either.- Build the middleware under test directly:
middleware.RequireVerifiedUser(getUser *useCases.GetUserUseCase) func(http.Handler) http.Handler, exported fromapi/middleware, andclaims.UserId(r *http.Request) (uint32, bool), exported fromapi/pkg/claims. ConstructuseCases.NewGetUserUseCase(repo)over amockUserRepo.
Setup shared by every case
- Call
validator.InitValidator()once. - Build a test
jwtauth.New("HS256", []byte("test-secret"), nil). - Wire
article.NewHandler(create, get, list, delete, update, getUser)(extended with the newGetUserUseCaseparameter) over amockArticleRepoand themockUserRepo/GetUserUseCasefrom step 3. - Mount the routes on a fresh
chi.NewRouter()mirroring the newapi/router.goshape:r.Use(jwtauth.Verifier(token))on the whole/articlegroup (parses, never rejects)r.Get("/", articleHandler.ListArticles)directly under that Verifier-only group- a nested
r.Groupaddingjwtauth.Authenticator(token)thenmiddleware.RequireVerifiedUser(getUser), containingr.Get("/{id}", ...),r.Post("/", ...),r.Delete("/{id}", ...),r.Patch("/{id}", ...),r.Patch("/{id}/publish", ...),r.Patch("/{id}/unpublish", ...),r.Post("/{id}/image", ...)No live server.
- Mint jwt cookies with
token.Encode(map[string]any{"user_id": uint32(1)})for the valid-token cases, following the convention infeatures/article_publishing.qa.md. - Seed
mockUserRepoper scenario viarepo.users[1] = user.User{ID: 1, IsVerified: ...}.
HTTP checks: protected routes reject an unverified caller
- Seed
mockUserRepowith user id 1,IsVerified: false; seedmockArticleRepowith article id- 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 status403and body exactlyapperr.VerificationRequiredErrorMessage. Assert the article repository was not mutated by any of the write calls.
- Table-driven over
- Same seed,
POST /v1/article/1/imagewith the valid cookie and a multipart form field"file"holdingbefore.png/image/png→ assert status403, bodyapperr.VerificationRequiredErrorMessage, and the image repository'sCreateis never called (mirroring the assertion style intest/upload_article_image_test.go).
HTTP checks: a verified caller is unaffected
- Seed
mockUserRepowith user id 1,IsVerified: true.POST /v1/article/with the valid cookie and body{"title":"Valid Title","body":"Some content"}→ assert status201and the response body decodes to the createdarticle.Article.
HTTP checks: an unresolvable caller still answers 401, not 403
- Table-driven over five token shapes, each against a fresh seed of user id 1,
IsVerified: false, sent asPOST /v1/article/with body{"title":"Valid Title","body":"Some content"}, asserting status401and body exactlyapperr.UnauthorizedErrorMessage:- mint the jwt with
token.Encode(map[string]any{"role": "user"})(nouser_idkey 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. SeedmockUserRepowith user id 7,IsVerified: true; seedmockArticleRepoempty. Mint a jwt withtoken.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 status401, body exactlyapperr.UnauthorizedErrorMessage, and that the article repository gained no new article (proving the fractional claim was rejected rather than silently accepted as user 7). 13b. Extendtest/claims_test.gowith aTestClaimsUserIdset of subtests mirroring the existingTestClaimsTokenVersionpattern: a well-formeduint32claim returnsok == truewith 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 pastmath.MaxUint32(4294967296) all returnok == false. This exercisesclaims.UserIddirectly, independent of the HTTP round-trip.
- mint the jwt with
HTTP checks: no session at all is still 401
GET /v1/article/1andPOST /v1/article/with nojwtcookie → assert status401for both (produced byjwtauth.Authenticatoritself, beforeRequireVerifiedUserruns); do not assert an exact body.
HTTP checks: a user-repository failure while resolving the caller
- Mint a valid jwt cookie for user id 1. Configure
mockUserRepo'sfailSwitchso the next call toGetreturns a generic error (notuser.ErrUserNotFound), via the existingfailNext("Get")-style helper.DELETE /v1/article/1with that cookie, article repository seeded with article id 1 → assert status500, 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
- Seed
mockUserRepowith user id 1,IsVerified: false; seedmockArticleRepowith an online article id 1 and an offline article id 2.GET /v1/article/with the valid cookie from step 8 → assert status200; decode the JSON array and assert it has exactly one element, id1. - Same article seed,
mockUserRepouser id 1 withIsVerified: true→GET /v1/article/with the same cookie → assert status200; decode the JSON array and assert both ids1and2are present. - Same article seed, no user with id 999 in
mockUserRepo, jwt cookie minted for user id 999 →GET /v1/article/with that cookie → assert status200; assert exactly id1is present (fallback to online-only onuser.ErrUserNotFound). - Same article seed, valid cookie for user id 1,
mockUserRepo'sfailSwitchset so the nextGetcall returns a generic error →GET /v1/article/with that cookie → assert status200; assert exactly id1is present (a user-lookup failure falls back to online-only rather than failing the request).
Regression check on existing fixtures
features/article_publishing.feature,features/edit_article_body.feature,features/upload_article_image.featureand the correspondingtest/*_test.gofiles mint a valid jwt cookie for "a user with id 1" without asserting anything about that user'sIsVerifiedstatus. After this slice, those HTTP tests'mockUserRepofixture must seed user id 1 withIsVerified: true(or the equivalent id used in each file), or every one of those protected-route assertions will start failing with403instead 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 surprisetask testfailure.
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/image-upload) 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.