# QA: Paginated article listing (API) ## HTTP-level (Go, `test/`, `task test`) Build `article.NewHandler(...)` over the real `ListArticlesUseCase` and a fake `article.Repository` (extend `mockArticleRepo` in `test/article_test.go`, keeping type names unique across files). The fake implements the interface contract: paging (Page/Size 0 = unpaged), q/status filtering, ordering updated_at DESC then id DESC, total, and the visibility counts. Mount on a fresh `chi.NewRouter()` mirroring `NewArticleRouter` in `api/router.go` (GET / under `jwtauth.Verifier` only, test `jwtauth.New("HS256", ...)`). Call `validator.InitValidator()`. Use a user fake for the verified, unverified and missing cases, as in `test/public_article_listing_test.go`. No live server, no SQLite file. Helper: decode the body into {items []map, total, page, size int, counts {all, online, draft}} and also assert the raw JSON contains `"items":[` (never null). 1. Bare request: 30 online articles, GET /v1/article/ -> object (not array), page 1, size 25, total 30, 25 items, counts 30/30/0. 2. Defaults table (page=2, page=3, size=5, size=50) on 60 articles. 3. Order: ids 3,2,1 for the equal-timestamp case with no sort parameter (default updated_at:desc). Sort table: the 8 whitelisted values on the mixed-case fixture (apple/Banana/Cherry), asserting the item ids. The fake repo implements the same contract: key, direction, then id desc. 3a. Equal titles with sort=title:asc -> ids 3,2,1. 3b. Sort before paging: 12 articles, ?sort=title:desc&size=5, three pages, no overlap. 3c. Sort + q + status combination: ordered titles, total 3, counts unchanged. 3d. Use-case test: ListArticlesUseCase passes the typed sort through to Filters and defaults to updated_at desc when absent. Only whitelisted values reaching Order in the repository is a code-review item; the handler test covers the 400 side. 4. Partition: 12 articles, pages 1-3 of size 5 -> 5,5,2 and 12 distinct ids. 5. Public: 5 online + 3 offline -> total 5, counts 5/5/0, no offline item. 6. Verified: same data -> total 8, counts 8/5/3, three offline items. 7. Malformed cookie and unverified user -> same as public. 8. Out of range: ?page=99&size=5 -> 200, raw `"items":[]`, total 12, counts.all 12. 9. Empty repository -> items [], total 0, counts 0/0/0. 10. Status table (verified): online -> total 4, draft -> total 2, counts always 6/4/2. 11. Public status=draft -> items [], total 0, counts 4/4/0. 12. q=PLÂSTER (accent and case insensitive) -> total 1, counts.all 2. q=zzzz -> empty, counts unchanged. 13. q+status+page combination: total 7, 2 items on page 2, counts.draft 7. 14. Invalid table (all rows incl. `page=`, `page=1.5`, `status=archived`, `status=`, 201-char q, and the 7 sort rows: `sort=title`, `sort=title:up`, `sort=body:asc`, `sort=`, `sort=updated_at`, `sort=title:asc;DROP TABLE`, `sort=TITLE:ASC`): each -> 400 with the exact BodyErrorMessage text, and the fake's FindByFilters/count counters stay 0. 15. ?page=0&format=pdf -> 400 with the format message. 16. ?size=5&format=html -> every item body_format "html". 17. Fake repo failure (existing `failNext("FindByFilters")`) -> 500 "Failed to retrieve articles". 18. Use-case tests: the paged list method returns items, total and counts, propagates a repo error (errors.Is on a sentinel), and the unpaged `Filters{}` call used by `purgePlan.go` and `ListMedia.go` still returns every article (existing tests keep passing). 19. Existing tests that decode `GET /v1/article/` as an array (`test/public_article_listing_test.go`, `test/article_test.go`, `test/use_case_baseline_test.go`, features/articles_list.qa.md item 1) are amended to the envelope. List them in the diff. ## counts.suggestions (Go, `test/`, `task test`) Build `article.NewHandler(...)` over the real use cases and the extended `mockArticleRepo`, as in the section above. The fixture articles carry bodies that yield suggestions, hidden (noindex) or ready SEO status and a `seo_dismissed` list. Mount on a fresh `chi.NewRouter()` that mirrors `NewArticleRouter` and call `validator.InitValidator()`. 20. Counts table: 4 online and 2 draft with 3 suggestions gives `counts` exactly `{all:6, online:4, draft:2, suggestions:3}`. Decode into a map and assert the key set is exactly those four. 21. Zero case: 5 clean articles gives `suggestions` 0. Empty library gives all four at 0. 22. Equality: 30 articles, 12 with suggestions; `GET /?seo=suggestions` gives `total` 12 and `counts.suggestions` 12. 23. Independence table: `q=tile`, `status=draft`, `site=journal`, `seo=ready`, `seo=hidden`, `sort=title:asc`, `size=5&page=99` all give 3. 24. Exclusions: a noindex article with suggestions and an article whose only suggestion is dismissed are not counted; one with two suggestions of which one is dismissed is counted once. 25. Visibility: a verified caller counts an online and a draft article (2); an anonymous caller and an unverified caller count the online one only, with `draft` 0. 26. Repository failure while evaluating gives 500 "Failed to retrieve articles" and no partial body. 27. Regression: the existing assertions on `total`, `page`, `size`, `counts.all/online/draft` and items stay green without edits. 28. Docs, read by hand: `docs/api/articles.md` (example JSON gets `"suggestions"`, and the `counts` bullet says global, visibility-scoped, noindex and dismissed excluded, equal to the unfiltered `seo=suggestions` total); `web/src/data/apiCatalogue.ts` list response fields with its spec and `IntegrationView.spec.ts` counts; `docs/release-notes.md` line. 29. Scoped mutation through `task mutate` only: `MUTATE_FILES=ListArticles.go MUTATE_RUN='TestArticle.*Count' MUTATE_WORKERS=2 task mutate -- ./internal/infrastructure/useCases`, plus any other file touched by the change. Re-check LIVED or NOT COVERED suspects without `MUTATE_RUN`. Then `task lint:go` and `task fmt`. ## Docs (verified by reading the diff) - `docs/api/articles.md`: new "Pagination, search, sort and counts" section (parameters including `sort` values, default and id tie-break, envelope, ordering, errors) and a "Breaking change" note, like the existing format note: GET /v1/article/ no longer returns a bare array; read `items`. - `web/src/data/apiCatalogue.ts` `art-list`: add `page`, `size`, `q`, `status`, `sort` query entries, the 400 error, and a paged JSON example; the Integration specs that assert this catalogue are amended. - `docs/design/screens/integration.md` is read-only for agents: flag that its text "GET /v1/article/" example needs the user's update. - A release note under `docs/releases/` is the maintainer's call: flag it. ## Manual verification - Real SQLite file via `task dev`: seed 60 articles, check ?page=2&size=25, ordering, q accent-insensitivity, total and counts. MySQL and PostgreSQL parity of the COUNT / LIMIT-OFFSET / LIKE queries is manual (no live DB in tests). - Sort by `title` on a live SQLite file, and on MySQL/PostgreSQL if available: title collation differs (binary, case-insensitive, locale), so mixed-case and accented ordering may differ between databases. Accepted; note any difference. - An anonymous curl against a running server returns the envelope.