Files
RenewCMS/features/user_display_name.qa.md
T

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

  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

  1. Call validator.InitValidator().
  2. 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).
  3. Fresh chi.NewRouter() mirroring api/router.go, with token := jwtauth.New("HS256", []byte("test-secret"), nil):
    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.
  4. Mint cookies with token.Encode(map[string]any{"user_id": uint32(id), "token_version": uint32(v)}) set as the jwt cookie.
  5. 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)

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

  1. 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.
  2. Trim table: " Marta Alves " and "\tMarta Alves\n" -> "Marta Alves"; "Marta Alves" keeps its interior spaces.
  3. Clear table (seed DisplayName: "Marta Alves"): "", " ", "\t \n" -> 200, response and repo display_name "", username still "marta".
  4. 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.
  5. Trim-before-count: strings.Repeat(" ", 5) + strings.Repeat("a", 60) + strings.Repeat(" ", 5) -> 200, returned value is the 60 "a".
  6. Control characters: {"display_name":"Ma\nrta"} -> 200, returned and stored value is "Ma\nrta".
  7. 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.
  8. 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.
  9. 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.
  10. Unverified user 8 with matching cookie -> PATCH -> 403, body apperr.VerificationRequiredErrorMessage + "\n", updateDisplayNameCalls == 0.
  11. No cookie -> 401 (do not assert the body; produced by jwtauth.Authenticator), updateDisplayNameCalls == 0.
  12. User 7 with TokenVersion: 1, cookie encoding token_version 0 -> 401, body apperr.UnauthorizedErrorMessage + "\n", updateDisplayNameCalls == 0.
  13. Cookie for user 999 that does not exist -> 401, same body.
  14. failNext("UpdateDisplayName") (generic error) with a valid verified user -> 500, body "Internal server error\n".
  15. Repository returns user.ErrUserNotFound from UpdateDisplayName only (middleware passes) -> 401, body apperr.UnauthorizedErrorMessage + "\n".
  16. 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

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