7.8 KiB
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().
- 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.
- Shape: decode
GET /v1/media/5and the matching element ofitemsfromGET /v1/media/into maps; assert the key sets and values are equal. GET /v1/media/999gives 404 with the exact plain-text body "Image not found" (apperr.ImageNotFoundErrorMessage).- Malformed ids
abc,-1,4294967296,1.5give 400 withapperr.IdUint32ErrorMessage. - Regression:
GET /{id}/usage,GET /{id}/variants,PATCH /{id}andDELETE /{id}still answer as before (the existing tests stay green without edits), and the variant routes keep theirRejectAPIKeyand owner/admin rule. - No activity event is recorded by the read (the fake audit repository stays empty).
- Docs, read by hand:
docs/api.mddocumentsGET /v1/media/{id}(auth, 200 shape, 404, 400);web/src/data/apiCatalogue.tslists it in the Media group, and its spec andIntegrationView.spec.tscounts are updated;docs/release-notes.mdline. - Scoped mutation through
task mutateonly, 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 withoutMUTATE_RUN. Thentask lint:goandtask fmt.
Item 3: media names (Vitest, web/)
Mock useApi's request for GET /v1/media/{id} per case.
- Open the SEO panel with cover id 150 and social id 151 (neither in any first page of the library). Mock
GET /v1/media/150andGET /v1/media/151to return items. Assert the panel shows their names and thumbnails, and that noGET /v1/media/?page=1&size=100&sort=newestrequest is made. - Re-render with the same selection: assert no new request (the mocked call count stays).
GET /v1/media/150answers 404: the fallback is shown, no notice. A 500 and a network error: panel usable, fallback, no notice.- A library of 12 items: the same names as before.
- The existing
useMediaNamesand 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)
- Same fields, different key order: one POST (assert the mocked call count).
- 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.
- A field the check ignores changes: no second POST.
- A 500 is not cached: the retry POSTs.
- 200 distinct inputs: the cache size stays at its existing limit and the most recent input is a hit.
- The existing editor SEO specs pass unchanged.
Item 8: chunked lookups (Go, test/)
- Temp-dir SQLite (never the real database file): seed 1,200 articles; load all ids; assert 1,200 rows, no duplicates, requested order.
- Boundary table 0, 1, 499, 500, 501, 1000, 1001 ids.
- 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 ASCrule). Soft-deleted rows match the single-query path. - Name the MySQL and Postgres variants so the existing filters match them:
TestMySQLChunkedLookups(run by-run MySQL) andTestPostgresChunkedLookups(run by-run Postgres). They skip whenRENEWCMS_TEST_MYSQL_DSNorRENEWCMS_TEST_POSTGRES_DSNis unset, like the existing contract tests.
Item 9: slug backfill (Go, test/, temp-dir SQLite)
- 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). - Second boot: record every row's
slugandupdated_atand the DB write count (callback), run again, and assert the values are identical and the write count is 0. Also calldatabase.Migratetwice. - 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.
- 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_slugpresent and a repeatedensureArticleSlugIndexreturns nil. - Existing slug migration tests (
planSlugAssignments) stay green.
Quality gates
- 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.
- 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 withoutMUTATE_RUN. Thentask lint:go,task fmt,task testandtask test:web. task visual:check -- articles,-- sitesand-- 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) andtask test:postgres(postgres:16 then 17). Docker is required; the tasks start and remove the containers. Extra arguments go after--, for exampletask 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=1andRENEWCMS_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 forabc. - Slug backfill on a real file: copy an old database, boot with
task dev, confirm slugs and that a second boot changes no row (compareupdated_atbefore and after). - Item 7 (the
?seo=measurement) is a measurement with its result recorded indocs/, so it has no Gherkin.