# QA procedure: article_title_rules Package: `test` (black-box, alongside `article_test.go` / `use_case_baseline_test.go` / `not_found_errors_test.go` / the hasher-domain-interface tests). ## Fakes needed 1. Extend `mockArticleRepo` (`test/article_test.go`) / its shared `failSwitch` (`test/fakes_common.go`) so a configured failure can return a specific error, not only the generic `errRepositoryFailure`. Today `failSwitch.trigger` always returns the same sentinel; add a way to say "the next call to `Create` returns `article.ErrInvalidTitle`" specifically, distinct from "the next call to `Create` returns a generic repository error" (e.g. a `failNextWith(method string, err error)` on `failSwitch`, keeping `errRepositoryFailure` as the default so existing generic-failure tests don't change). 2. Add a `createCalled` flag (or equivalent) to `mockArticleRepo` if not already present, so a test can assert `Create` was never reached when request-layer validation rejects a title first. ## Use-case-level checks (fake repo, no HTTP) 3. `useCases.NewCreateArticleUseCase(repo)`, `CreateArticle(...)` with an empty fake repo: - Title `strings.Repeat("😀", 2)` (2 runes, 8 bytes) -> `errors.Is(err, article.ErrInvalidTitle)`, nothing stored. - Title `strings.Repeat("😀", 3)` (3 runes, 12 bytes) -> no error, created, `is_online` false, one article stored. - Title `strings.Repeat("😀", 200)` (200 runes, 800 bytes) -> no error, created. Proves the check counts runes (`utf8.RuneCountInString`), not `len()` bytes. - Title `strings.Repeat("😀", 201)` (201 runes, 804 bytes) -> `errors.Is(err, article.ErrInvalidTitle)`, nothing stored. - Keep the existing ASCII boundary cases in `TestCreateArticleTitleLengthLimit` (0/2/3/200/201) passing unchanged, as a regression check that byte length and rune length coincide for ASCII. ## HTTP checks (real CreateArticleUseCase over the fake repo, mirroring api/router.go) 4. Call `validator.InitValidator()`. 5. Build `useCases.NewCreateArticleUseCase(fakeRepo)` plus `GetArticleUseCase`, `ListArticlesUseCase`, `DeleteArticleUseCase` (real, over the same fake repo). Build `article.NewHandler(create, get, list, delete)`. Mount its routes inside a group wrapped by `jwtauth.Verifier(token)` + `jwtauth.Authenticator(token)` on a fresh `chi.NewRouter()`, with a test `jwtauth.New("HS256", []byte("test-secret"), nil)`, as in `api/router.go`. 6. Mint a valid jwt cookie for user id 1 (`token.Encode(map[string]any{"user_id": uint32(1)})`, set as the `jwt` cookie on each `httptest.NewRequest`). 7. `POST /v1/article/` with a 2-ASCII-character title (`"ab"`) and a valid body -> status `400`, `fakeRepo.createCalled` is false. Assert the body is neither `apperr.ArticleTitleErrorMessage` nor `"Failed to create article"`, to keep the two 400 sources (request-layer vs. domain-layer) distinguishable in the test. 8. `POST /v1/article/` with a 3-ASCII-character title -> status `201`, response body decodes to the created article JSON, `is_online` false. 9. `POST /v1/article/` with a 200-ASCII-character title (`strings.Repeat("a", 200)`) -> status `201`, created. 10. `POST /v1/article/` with a 201-ASCII-character title -> status `400`, `Create` never called. 11. `POST /v1/article/` with a title of 200 repetitions of "😀" (200 runes, 800 bytes) and a valid body -> status `201`, created, response JSON's `title` equals the sent title. 12. Configure the fake repo (per step 1) so its next `Create` call returns `article.ErrInvalidTitle` directly. `POST /v1/article/` with title "Valid Title" and a valid body (both well inside 3-200 runes, so `validator.Validate.Struct` passes and `CreateArticleUseCase`'s own check would also pass) -> status `400`, body `apperr.ArticleTitleErrorMessage + "\n"`. This scenario is deliberately artificial: with the rule aligned, no real HTTP input can make `CreateArticleUseCase.CreateArticle`'s own check return `article.ErrInvalidTitle` once `validator.Validate.Struct` has already accepted the same title, so the only way to exercise `Handler.PostArticle`'s mapping branch today is to have the fake repository hand back that sentinel directly, standing in for a future or repository-level source of the same error. 13. Configure the fake repo to fail on its next `Create` with the generic `errRepositoryFailure` (existing `failNext("Create")`). `POST /v1/article/` with title "Valid Title" and a valid body -> status `500`, body `"Failed to create article\n"` -- regression proof a non-title failure still answers 500. 14. `POST /v1/article/` with no jwt cookie -> status `401` (regression: the route is still gated). ## Manual verification (not exercised by the Go test) - None. This slice has no SPA-visible behaviour and no SMTP involvement.