7.9 KiB
7.9 KiB
QA procedure: user_display_name
Package: test (black-box), extending test/user_test.go (mockUserRepo, stubHasher) and
following the router wiring pattern in test/get_current_user_http_test.go. No live server, no real
SQLite file, no real SMTP. Files: test/user_display_name_test.go (use case and HTTP).
Test infrastructure additions needed first
- Add
UpdateDisplayName(userId uint32, displayName string) (user.User, error)tomockUserRepo: callm.fail.trigger("UpdateDisplayName")first, returnuser.ErrUserNotFoundwhen the id is absent, otherwise storeDisplayNameon the map entry and return it. Add fieldsupdateDisplayNameCalls intandlastUpdateDisplayName string. - Use
failNext("UpdateDisplayName")for the 500 case. For the "deleted between auth and save" case, addfailNextWith("UpdateDisplayName", user.ErrUserNotFound)(as planned infeatures/article_title_rules.qa.mdstep 1), or seed the user, then delete it from the fake after the middleware step via a hook. - Any other type implementing
user.Repositoryintest/must gain the method so the package compiles.
Setup shared by every HTTP case
- Call
validator.InitValidator(). getUser := useCases.NewGetUserUseCase(userRepo),update := useCases.NewUpdateUserProfileUseCase(userRepo),userHandler := user.NewHandler(update)(importRenewCMS/api/handlers/userunder an alias that does not collide with the domainuserpackage),meHandler := auth.NewMeHandler(getUser).- Fresh
chi.NewRouter()mirroringapi/router.go, withtoken := jwtauth.New("HS256", []byte("test-secret"), nil):Add the publicr.Route("/v1/user", func(r chi.Router) { r.Use(jwtauth.Verifier(token)) r.Use(jwtauth.Authenticator(token)) r.Use(middleware.RequireCurrentToken(getUser)) r.Use(middleware.RequireVerifiedUser(getUser)) r.Patch("/me", userHandler.UpdateMe) }) r.Route("/v1/auth", func(r chi.Router) { r.Use(jwtauth.Verifier(token)) r.Use(jwtauth.Authenticator(token)) r.Use(middleware.RequireCurrentToken(getUser)) r.Get("/me", meHandler.Me) })/v1/auth/loginroute (realauth.NewLoginHandlerover the fakes, as in the existing login tests) for the login case. The handler method names above are indicative; use whatever the handlers expose. - Mint cookies with
token.Encode(map[string]any{"user_id": uint32(id), "token_version": uint32(v)})set as thejwtcookie. - Decode success bodies into
map[string]json.RawMessage(notuser.User) so key sets can be asserted. Error bodies are plain text fromhttp.Errorand carry a trailing"\n".
Use-case checks (fake repo, no HTTP)
useCases.NewUpdateUserProfileUseCase(repo)with user id 7 seeded,DisplayName: "Old":" Marta "->DisplayName "Marta"returned and stored.""and" "->DisplayName ""stored (cleared).strings.Repeat("😀", 60)-> success.strings.Repeat("😀", 61)->errors.Is(err, user.ErrInvalidDisplayName),updateDisplayNameCalls == 0, stored value still"Old".- Unknown id 99 ->
errors.Is(err, user.ErrUserNotFound). - Repository failure via
failNext("UpdateDisplayName")-> the error is returned unchanged.
HTTP checks
- Seed user 7 (
marta,marta@renew.example,IsVerified: true,DisplayName: "",TokenVersion: 0) with a non-emptyPasswordhash.PATCH /v1/user/me{"display_name":"Marta Alves"}->200. Body keys are exactly{id, username, email, is_verified, created_at, updated_at, display_name}.display_nameis"Marta Alves",usernameis"marta". The repo holds"Marta Alves". The storedPasswordandTokenVersionare unchanged. The same cookie then gets200fromGET /v1/auth/me. - Trim table:
" Marta Alves "and"\tMarta Alves\n"->"Marta Alves";"Marta Alves"keeps its interior spaces. - Clear table (seed
DisplayName: "Marta Alves"):""," ","\t \n"->200, response and repodisplay_name "",usernamestill"marta". - Length table (seed
""): 60 x"a"->200; 61 x"a"->400with bodyapperr.DisplayNameErrorMessage + "\n"; 60 x"😀"->200; 61 x"😀"->400; 60 x"é"->200. AssertupdateDisplayNameCalls == 0on every400and that the stored value is unchanged. - Trim-before-count:
strings.Repeat(" ", 5) + strings.Repeat("a", 60) + strings.Repeat(" ", 5)->200, returned value is the 60"a". - Control characters:
{"display_name":"Ma\nrta"}->200, returned and stored value is"Ma\nrta". - Malformed table (seed
DisplayName: "Marta Alves"): raw textnot json,{},{"display_name":null},{"display_name":42},{"displayName":"Marta"}-> each400,updateDisplayNameCalls == 0, stored value unchanged. Do not assert an exact body for these. GET /v1/auth/mefor user 7 withDisplayName "Marta Alves"->200,display_nameis"Marta Alves". For user 8 (IsVerified: false,DisplayName "") -> key set is exactly the seven keys anddisplay_nameis"", not absent.- Login: seed verified user
martawith a stub-hashed password andDisplayName: "Ines Duarte".POST /v1/auth/loginwith username"Ines Duarte"->403, bodyapperr.LogsErrorMessage + "\n". With username"marta"->200. Confirm the login handler's real status for an unknown username before asserting403. - Unverified user 8 with matching cookie ->
PATCH->403, bodyapperr.VerificationRequiredErrorMessage + "\n",updateDisplayNameCalls == 0. - No cookie ->
401(do not assert the body; produced byjwtauth.Authenticator),updateDisplayNameCalls == 0. - User 7 with
TokenVersion: 1, cookie encodingtoken_version 0->401, bodyapperr.UnauthorizedErrorMessage + "\n",updateDisplayNameCalls == 0. - Cookie for user 999 that does not exist ->
401, same body. failNext("UpdateDisplayName")(generic error) with a valid verified user ->500, body"Internal server error\n".- Repository returns
user.ErrUserNotFoundfromUpdateDisplayNameonly (middleware passes) ->401, bodyapperr.UnauthorizedErrorMessage + "\n". - Sweep for secrets: on every
200PATCH body and onGET /v1/auth/me, reuseassertNoForbiddenKeysfromtest/user_response_field_hygiene_test.go. Addtoken_versionto that helper's forbidden keys if it does not check it yet.
Edits to existing tests required by the six to seven field change
test/user_response_field_hygiene_test.go(expectedKeysin the verify test, around line 127): add"display_name". The count check then expects seven. The seeded user 7 has no display name, so also assertdisplay_namedecodes to"".test/get_current_user_http_test.godecodes into the public user struct; update its import to the shared package. Step 17 is the authoritative shape check for/me.
Manual verification (not exercised by the Go test)
- Start an existing dev SQLite file created before this slice, boot the server so
AutoMigrateruns, and confirmGET /v1/auth/mefor the pre-existing user returnsdisplay_name: ""and that login still works. ThenPATCH, restart, and confirm the value persisted. - Confirm by inspection that the
internal/domain/userstruct has nogormtag onDisplayName, the GORM tag with the empty-string default sits inpersistence/models,persistence/mappersmaps both directions, and the model list inapi/dependecyInjection.goneeds no new entry (same model, new column). - Confirm that the new handler package constructs nothing and only receives
UpdateUserProfileviaNewHandler(...), thatdependecyInjection.gowires the use case into the*UseCasescontainer, and that neither handler package imports the other. - No SMTP or real embedded frontend is involved. The SPA is out of scope.