# 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 1. Add `UpdateDisplayName(userId uint32, displayName string) (user.User, error)` to `mockUserRepo`: call `m.fail.trigger("UpdateDisplayName")` first, return `user.ErrUserNotFound` when the id is absent, otherwise store `DisplayName` on the map entry and return it. Add fields `updateDisplayNameCalls int` and `lastUpdateDisplayName string`. 2. Use `failNext("UpdateDisplayName")` for the 500 case. For the "deleted between auth and save" case, add `failNextWith("UpdateDisplayName", user.ErrUserNotFound)` (as planned in `features/article_title_rules.qa.md` step 1), or seed the user, then delete it from the fake after the middleware step via a hook. 3. Any other type implementing `user.Repository` in `test/` must gain the method so the package compiles. ## Setup shared by every HTTP case 4. Call `validator.InitValidator()`. 5. `getUser := useCases.NewGetUserUseCase(userRepo)`, `update := useCases.NewUpdateUserProfileUseCase(userRepo)`, `userHandler := user.NewHandler(update)` (import `RenewCMS/api/handlers/user` under an alias that does not collide with the domain `user` package), `meHandler := auth.NewMeHandler(getUser)`. 6. Fresh `chi.NewRouter()` mirroring `api/router.go`, with `token := jwtauth.New("HS256", []byte("test-secret"), nil)`: ```go r.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) }) ``` Add the public `/v1/auth/login` route (real `auth.NewLoginHandler` over the fakes, as in the existing login tests) for the login case. The handler method names above are indicative; use whatever the handlers expose. 7. Mint cookies with `token.Encode(map[string]any{"user_id": uint32(id), "token_version": uint32(v)})` set as the `jwt` cookie. 8. Decode success bodies into `map[string]json.RawMessage` (not `user.User`) so key sets can be asserted. Error bodies are plain text from `http.Error` and carry a trailing `"\n"`. ## Use-case checks (fake repo, no HTTP) 9. `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 10. Seed user 7 (`marta`, `marta@renew.example`, `IsVerified: true`, `DisplayName: ""`, `TokenVersion: 0`) with a non-empty `Password` hash. `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_name` is `"Marta Alves"`, `username` is `"marta"`. The repo holds `"Marta Alves"`. The stored `Password` and `TokenVersion` are unchanged. The same cookie then gets `200` from `GET /v1/auth/me`. 11. Trim table: `" Marta Alves "` and `"\tMarta Alves\n"` -> `"Marta Alves"`; `"Marta Alves"` keeps its interior spaces. 12. Clear table (seed `DisplayName: "Marta Alves"`): `""`, `" "`, `"\t \n"` -> `200`, response and repo `display_name ""`, `username` still `"marta"`. 13. Length table (seed `""`): 60 x `"a"` -> `200`; 61 x `"a"` -> `400` with body `apperr.DisplayNameErrorMessage + "\n"`; 60 x `"😀"` -> `200`; 61 x `"😀"` -> `400`; 60 x `"é"` -> `200`. Assert `updateDisplayNameCalls == 0` on every `400` and that the stored value is unchanged. 14. Trim-before-count: `strings.Repeat(" ", 5) + strings.Repeat("a", 60) + strings.Repeat(" ", 5)` -> `200`, returned value is the 60 `"a"`. 15. Control characters: `{"display_name":"Ma\nrta"}` -> `200`, returned and stored value is `"Ma\nrta"`. 16. Malformed table (seed `DisplayName: "Marta Alves"`): raw text `not json`, `{}`, `{"display_name":null}`, `{"display_name":42}`, `{"displayName":"Marta"}` -> each `400`, `updateDisplayNameCalls == 0`, stored value unchanged. Do not assert an exact body for these. 17. `GET /v1/auth/me` for user 7 with `DisplayName "Marta Alves"` -> `200`, `display_name` is `"Marta Alves"`. For user 8 (`IsVerified: false`, `DisplayName ""`) -> key set is exactly the seven keys and `display_name` is `""`, not absent. 18. Login: seed verified user `marta` with a stub-hashed password and `DisplayName: "Ines Duarte"`. `POST /v1/auth/login` with username `"Ines Duarte"` -> `403`, body `apperr.LogsErrorMessage + "\n"`. With username `"marta"` -> `200`. Confirm the login handler's real status for an unknown username before asserting `403`. 19. Unverified user 8 with matching cookie -> `PATCH` -> `403`, body `apperr.VerificationRequiredErrorMessage + "\n"`, `updateDisplayNameCalls == 0`. 20. No cookie -> `401` (do not assert the body; produced by `jwtauth.Authenticator`), `updateDisplayNameCalls == 0`. 21. User 7 with `TokenVersion: 1`, cookie encoding `token_version 0` -> `401`, body `apperr.UnauthorizedErrorMessage + "\n"`, `updateDisplayNameCalls == 0`. 22. Cookie for user 999 that does not exist -> `401`, same body. 23. `failNext("UpdateDisplayName")` (generic error) with a valid verified user -> `500`, body `"Internal server error\n"`. 24. Repository returns `user.ErrUserNotFound` from `UpdateDisplayName` only (middleware passes) -> `401`, body `apperr.UnauthorizedErrorMessage + "\n"`. 25. Sweep for secrets: on every `200` PATCH body and on `GET /v1/auth/me`, reuse `assertNoForbiddenKeys` from `test/user_response_field_hygiene_test.go`. Add `token_version` to that helper's forbidden keys if it does not check it yet. ## Edits to existing tests required by the six to seven field change 26. `test/user_response_field_hygiene_test.go` (`expectedKeys` in 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 assert `display_name` decodes to `""`. 27. `test/get_current_user_http_test.go` decodes 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 `AutoMigrate` runs, and confirm `GET /v1/auth/me` for the pre-existing user returns `display_name: ""` and that login still works. Then `PATCH`, restart, and confirm the value persisted. - Confirm by inspection that the `internal/domain/user` struct has no `gorm` tag on `DisplayName`, the GORM tag with the empty-string default sits in `persistence/models`, `persistence/mappers` maps both directions, and the model list in `api/dependecyInjection.go` needs no new entry (same model, new column). - Confirm that the new handler package constructs nothing and only receives `UpdateUserProfile` via `NewHandler(...)`, that `dependecyInjection.go` wires the use case into the `*UseCases` container, and that neither handler package imports the other. - No SMTP or real embedded frontend is involved. The SPA is out of scope.