64 lines
7.8 KiB
Markdown
64 lines
7.8 KiB
Markdown
# QA procedure: Hardening after the Sites, Authors and SEO slices
|
|
|
|
Spec: `features/tech_debt_hardening.feature`. The Go part is in `test/` (package `test`, `task test`); the web part is Vitest (`task test:web`). Constraints: no commits; no comments in code; scoped `task mutate` only for the Go files of items 3, 8 and 9 (and of items 5 and 6 per their own QA files); `task visual:check` only on `articles`, `sites` and `integration` to confirm no regression, with no new `approvedBy` exception and the waived screens untouched; `docs/design` read-only.
|
|
|
|
## Item 3: GET /v1/media/{id} (Go, `test/`, `task test`)
|
|
|
|
Build `media.NewHandler(...)` over the real media use cases and the existing fake image repository and storage fakes used by the media tests (unique fake type names across files). Mount on a fresh `chi.NewRouter()` that mirrors `NewMediaRouter` in `api/router.go`, behind `jwtauth.Verifier`, the API key authentication path, `middleware.RequireSignedIn`, `jwtauth.Authenticator` and `RequireVerifiedUser`, with a test `jwtauth.New("HS256", ...)`. Requests carry signed `jwt` cookies with the `user_id` claim for an owner, an admin, a verified account with another role and an unverified account. Call `validator.InitValidator()`.
|
|
|
|
1. Credentials table on an existing image 5: owner, admin, other verified role and a valid API key give 200; an unverified account gives 403; no credentials give 401.
|
|
2. Shape: decode `GET /v1/media/5` and the matching element of `items` from `GET /v1/media/` into maps; assert the key sets and values are equal.
|
|
3. `GET /v1/media/999` gives 404 with the exact plain-text body "Image not found" (`apperr.ImageNotFoundErrorMessage`).
|
|
4. Malformed ids `abc`, `-1`, `4294967296`, `1.5` give 400 with `apperr.IdUint32ErrorMessage`.
|
|
5. Regression: `GET /{id}/usage`, `GET /{id}/variants`, `PATCH /{id}` and `DELETE /{id}` still answer as before (the existing tests stay green without edits), and the variant routes keep their `RejectAPIKey` and owner/admin rule.
|
|
6. No activity event is recorded by the read (the fake audit repository stays empty).
|
|
7. Docs, read by hand: `docs/api.md` documents `GET /v1/media/{id}` (auth, 200 shape, 404, 400); `web/src/data/apiCatalogue.ts` lists it in the Media group, and its spec and `IntegrationView.spec.ts` counts are updated; `docs/release-notes.md` line.
|
|
8. Scoped mutation through `task mutate` only, for the media files touched by the change: `MUTATE_FILES=<the GetMedia use case file and any media file touched> MUTATE_RUN='TestMedia' MUTATE_WORKERS=2 task mutate -- ./internal/infrastructure/useCases`. Report LIVED and NOT COVERED; re-check filtered suspects without `MUTATE_RUN`. Then `task lint:go` and `task fmt`.
|
|
|
|
## Item 3: media names (Vitest, `web/`)
|
|
|
|
Mock `useApi`'s `request` for `GET /v1/media/{id}` per case.
|
|
|
|
9. Open the SEO panel with cover id 150 and social id 151 (neither in any first page of the library). Mock `GET /v1/media/150` and `GET /v1/media/151` to return items. Assert the panel shows their names and thumbnails, and that no `GET /v1/media/?page=1&size=100&sort=newest` request is made.
|
|
10. Re-render with the same selection: assert no new request (the mocked call count stays).
|
|
11. `GET /v1/media/150` answers 404: the fallback is shown, no notice. A 500 and a network error: panel usable, fallback, no notice.
|
|
12. A library of 12 items: the same names as before.
|
|
13. The existing `useMediaNames` and SEO-panel specs pass; the assertions that depended on the list lookup are amended only where they mock the old request, and that diff is listed in the report.
|
|
|
|
## Item 4: SEO check cache (Vitest)
|
|
|
|
14. Same fields, different key order: one POST (assert the mocked call count).
|
|
15. Table, one case per result-affecting field (title, body, description, excerpt, slug, cover image, social image, noindex, site, canonical URL): a second POST is sent.
|
|
16. A field the check ignores changes: no second POST.
|
|
17. A 500 is not cached: the retry POSTs.
|
|
18. 200 distinct inputs: the cache size stays at its existing limit and the most recent input is a hit.
|
|
19. The existing editor SEO specs pass unchanged.
|
|
|
|
## Item 8: chunked lookups (Go, `test/`)
|
|
|
|
20. Temp-dir SQLite (never the real database file): seed 1,200 articles; load all ids; assert 1,200 rows, no duplicates, requested order.
|
|
21. Boundary table 0, 1, 499, 500, 501, 1000, 1001 ids.
|
|
22. 600 ids with 100 missing gives 500 rows. 700 articles with 3 ordered credits each come back in stored order across batches (including the `position ASC, id ASC` rule). Soft-deleted rows match the single-query path.
|
|
23. Name the MySQL and Postgres variants so the existing filters match them: `TestMySQLChunkedLookups` (run by `-run MySQL`) and `TestPostgresChunkedLookups` (run by `-run Postgres`). They skip when `RENEWCMS_TEST_MYSQL_DSN` or `RENEWCMS_TEST_POSTGRES_DSN` is unset, like the existing contract tests.
|
|
|
|
## Item 9: slug backfill (Go, `test/`, temp-dir SQLite)
|
|
|
|
24. Seed 5,000 articles with empty slugs across two sites; run `database.BackfillArticleSlugs`; assert unique non-empty slugs within each site; assert the writes ran in several bounded transactions (the batch size is a named constant chosen by the coder; assert through a counting hook or a GORM callback counter in the test, not a log).
|
|
25. Second boot: record every row's `slug` and `updated_at` and the DB write count (callback), run again, and assert the values are identical and the write count is 0. Also call `database.Migrate` twice.
|
|
26. Interrupted run: stop after the first batch (failure injection on the second batch), assert the first batch is committed, rerun, and assert the rest are filled and the first batch's slugs unchanged.
|
|
27. Suffix table (3 same-title articles in one site plus 1 in another), tombstones for soft-deleted rows written once, owner-set slugs kept, index `idx_articles_site_slug` present and a repeated `ensureArticleSlugIndex` returns nil.
|
|
28. Existing slug migration tests (`planSlugAssignments`) stay green.
|
|
|
|
## Quality gates
|
|
|
|
29. Layering guard: use cases import no persistence, mailer, models, gorm, net/http or chi; the domain package imports no GORM or HTTP; handlers import no persistence; no comment added.
|
|
30. Scoped mutation for items 3, 8 and 9 only, through `task mutate`: `MUTATE_FILES=articleRepository.go,articleSlugBackfill.go,<item 3 media files> MUTATE_RUN='TestChunked|TestBackfill|TestMedia' MUTATE_WORKERS=2 task mutate -- ./internal/infrastructure/persistence/repositories ./internal/infrastructure/persistence/database ./internal/infrastructure/useCases`. Report LIVED and NOT COVERED; re-check filtered suspects without `MUTATE_RUN`. Then `task lint:go`, `task fmt`, `task test` and `task test:web`.
|
|
31. `task visual:check -- articles`, `-- sites` and `-- integration`: all captures pass at 10 %; no exception added.
|
|
|
|
## Manual
|
|
|
|
- MySQL and PostgreSQL (item 8, and the repository cases of items 5 and 6): run `task test:mysql` (mysql:8.4 then mariadb:11) and `task test:postgres` (postgres:16 then 17). Docker is required; the tasks start and remove the containers. Extra arguments go after `--`, for example `task test:postgres -- -run Postgres.*Chunked -v`. To use your own server instead: `RENEWCMS_TEST_MYSQL_DSN='user:pass@tcp(host:3306)/db' go test -tags nofrontend ./test/... -run MySQL -count=1` and `RENEWCMS_TEST_POSTGRES_DSN='postgres://user:pass@host:5432/db?sslmode=disable' go test -tags nofrontend ./test/... -run Postgres -count=1`. Use a scratch database. No CI exists, so this is run by hand.
|
|
- Item 3 on a live app: with more than 100 media items, open an article whose cover is an old image; the SEO panel shows its name. `curl -i -b <cookie> http://localhost:8080/v1/media/<id>` returns the item, 404 for a missing id and 400 for `abc`.
|
|
- Slug backfill on a real file: copy an old database, boot with `task dev`, confirm slugs and that a second boot changes no row (compare `updated_at` before and after).
|
|
- Item 7 (the `?seo=` measurement) is a measurement with its result recorded in `docs/`, so it has no Gherkin.
|