Files
RenewCMS/features/tech_debt_hardening.qa.md
T

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().

  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.

  1. 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.
  2. Re-render with the same selection: assert no new request (the mocked call count stays).
  3. GET /v1/media/150 answers 404: the fallback is shown, no notice. A 500 and a network error: panel usable, fallback, no notice.
  4. A library of 12 items: the same names as before.
  5. 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)

  1. Same fields, different key order: one POST (assert the mocked call count).
  2. 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.
  3. A field the check ignores changes: no second POST.
  4. A 500 is not cached: the retry POSTs.
  5. 200 distinct inputs: the cache size stays at its existing limit and the most recent input is a hit.
  6. The existing editor SEO specs pass unchanged.

Item 8: chunked lookups (Go, test/)

  1. Temp-dir SQLite (never the real database file): seed 1,200 articles; load all ids; assert 1,200 rows, no duplicates, requested order.
  2. Boundary table 0, 1, 499, 500, 501, 1000, 1001 ids.
  3. 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.
  4. 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)

  1. 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).
  2. 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.
  3. 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.
  4. 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.
  5. Existing slug migration tests (planSlugAssignments) stay green.

Quality gates

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