# QA procedure: image_variants_per_image_api Go only, no SPA. Package `test` (black-box). New files: `test/image_variants_per_image_http_test.go`, `test/image_variants_generate_on_upload_test.go`, `test/media_variants_list_http_test.go`, `test/image_variants_per_image_audit_test.go`. Extend `test/image_variants_support_test.go`; fake type names must stay unique in the package. No live server, no real SQLite file, no real SMTP, no `time.Sleep`. ## Setup shared by every case 1. `validator.InitValidator()` once. `jwtauth.New("HS256", []byte("test-secret"), nil)` stands in for `JWT_SECRET`. 2. Reuse the fakes of `test/image_variants_support_test.go`: `mockVariantRepo`, `mockVariantFileRepo`, `mockVariantStorage`, `countingRenderer`. Add to `mockVariantFileRepo` the per-source totals method (rows and bytes by source stored name) with a call counter. Reuse `mockImageRepo` (width, height, size, content type, SHA256, created_at seeded), `mockArticleRepo`, the user fake and the audit fakes of `test/activity_support_test.go`. 3. Users: owner 1, admin 2, verified plain user 3 (role other than owner/admin), unverified user 4. Real signed `jwt` cookies with `user_id` and `token_version`; a stale cookie with a lower `token_version`; a valid API key in the key fake. 4. Router: a fresh `chi.NewRouter()` mirroring `NewMediaRouter` in `api/router.go`: `jwtauth.Verifier`, the API-key middleware, `AuditContext`, `Authenticator`, `RequireCurrentToken`, `RequireVerifiedUser`, then the owner/admin group holding the new routes beside the existing purge routes and the ungrouped media routes. Mount at `/v1/media`. Mount the real public handler `staticimages.NewHandler(...)` at `/static/uploadedImages` over the same storage and variant use cases so a generated file is observed through the public url. 5. Images are generated in the test with `image/png`, `image/jpeg`, `image/draw` (as in the other variant tests). The SVG is a real string. Seed the Listing library of features/media_library.feature for the list cases and the numbers of the Rule's table. ## A. Guards 6. Table of the feature: each method, path and credential gives the status and the exact body constant; assert no variant, index, storage or audit fake was touched. API key gets 401. Owner and admin both get 200. Malformed and unknown ids: 400 `apperr.IdUint32ErrorMessage`, 404 `Image not found`. ## B. Listing the variants of one image 7. Seed the Rule's Background. Decode the body and assert order (name), the absence of the disabled variant, `width`/`height` as JSON `null` on the raw body, output size and format, `generated`, `size` (null when not generated), `url`. Stale fingerprint row: `generated` false. Disabled variant's row ignored. SVG: raw body `{"unsupported":"svg","items":[]}`. No enabled variant: raw `"items":[]`. 8. Output format Examples: `countingRenderer.EncodableFormats()` configured for webp on and off; a webp source with `original` gives `png` without an encoder. 9. Geometry table: for each row assert the body's `output_width`/`output_height` and then render the same source and settings with the real renderer and assert the decoded size equals them (one source of truth). 10. Listing renders nothing: renderer count 0, storage `Open`/`Put` counts 0, no audit event. ## C. Generating one 11. First generation: 200, item fields, exactly one `Put`, one index row whose size equals the item `size`, original blob untouched, no audit event. A public GET afterwards returns the same bytes with the renderer count still 1. A second POST: 200, renderer count unchanged. 12. Concurrency (run with `-race`): a renderer blocking on a channel; one admin POST and one public GET for the same pair released together; assert renderer count 1 and `Put` count 1. 13. Refusals: unknown, disabled and wrongly-cased variant names give 404 `Variant not found` with renderer count 0. SVG gives 422 with the constant. Byte cap and pixel cap give 413 (construct the use case with the low cap). Undecodable bytes give 415 with the constant. A renderer failure gives 500 `Internal server error`, no blob, no row, and the next call is 200. ## D. Generating all 14. Fresh image: 200, `generated` 5, `failed` 0, `failures` raw `[]`, five items all generated, five blobs and rows, `print` untouched. Pre-generated subset: counts only the newly generated, renderer count equals the missing ones. 15. Failure Examples: make the renderer fake return `ErrSourceTooLarge`, `ErrVariantSourceUnreadable` and a plain error for `hero` only; assert the status 200, `failures`, the causes and the four others generated. Nothing to generate, no enabled variant and SVG (422) rows. ## E. Purging one 16. 200 with `purged_files` 1 and `purged_bytes` equal to the seeded size; blob `Delete` called for that name only; other variant and the other image untouched; a following public GET renders again. Not generated and disabled-variant rows; unknown variant 404. Storage `Delete` failing gives 500 and the row is kept. ## F. Generate on upload 17. Upload through the real upload path with the Rule's four variants: after the 201, assert rows for `thumb` and `hero` only, `variant_count` 2 and `variant_size` equal to the sum, exactly one audit event (`media.upload`). Renderer failing for `hero`: 201, one row, the log line (capture with a test logger handed to the use case, assert it names the variant and the image id). Pixel cap: 201 and zero rows. SVG, no flagged variant, and 415 upload: renderer count 0. Creating a variant or a PUT with the flag on does not render for existing images. Same bytes uploaded twice: separate rows per stored name. ## G. Library list 18. Seed the Rule's table. `variant_count`/`variant_size` per item (raw JSON integers, 0 not null). Per-source totals fake call count is 1 per list call. `variants=with|without` sets; `counts` unchanged; combination rows; `sort=total_size` order `1,4,2,3,5`, `sort=largest` order `1,2,3,4,5`; tie breaks by id (make two totals equal); cursor walk with `limit=2`; empty cache with `variants=with`; every 400 row with `apperr.MediaQueryErrorMessage`. Rename response carries the fields. 19. Delete consistency: seed four rows and four blobs for image 1, DELETE, assert exactly those four `Delete` calls and rows. Usage and merge rows with a `?v=hero` suffix in the article body. ## H. Audit 20. Generate-all success, partial failure (`causes`), all-failed (`failed` outcome and exact reason), the no-op rows (all generated, no enabled variant, SVG, single generate, unknown image, GET, public generation, upload generation). Purge-by-image success params, the zero rows, the storage-failure row (500 plus failed event with the generic reason). A failing audit repository leaves response and files unchanged. `GET /v1/activity?category=media` lists both new events. Extend the mapping test of `failureReasons` for the three new sentences. ## I. Mutation guard 21. `task mutate` scope is `./internal/infrastructure/useCases`, scoped with `MUTATE_FILES` to the new use case files and `MUTATE_RUN` to the new test names. Each new branch needs a killing assertion: the generated-flag fingerprint comparison (equal and differing), the `generate_on_upload` AND `enabled` condition (each alone false), skipped already-generated variants, the failure-cause mapping order, `generated > 0` versus `failed > 0` for the outcome, the `purged_files > 0` guard, the total-size sum (original plus variants, not variants alone) and its id tie-break. Bare-bool conditions produce no mutant, so cover both branches anyway. ## J. Layering and hygiene 22. Guard test (`go list -deps` or grep): the domain imports only the stdlib and domain packages; the new use cases import no `persistence`, `mailer`, `models`, `gorm`, `net/http`, chi or `api`; the media handler imports no `persistence`, `mailer`, `models` or `gorm`. `CGO_ENABLED=0 go build -tags nofrontend ./...` succeeds. No comment in new or edited Go files, tests or docs. `docs/api.md` lists the new routes, shapes, params and errors. ## Manual (not automated) 23. `task dev:api` over a scratch SQLite file and `UPLOAD_DIR`: upload an image with a flagged variant and check the file lands in `UPLOAD_DIR`; then use the generate-all and purge routes with curl and a real cookie and check the files appear and disappear. 24. S3 driver (`STORAGE_DRIVER=s3`, MinIO): per-image generate and purge, upload with a flagged variant. 25. Latency of an upload of a 10 MB image with five flagged variants. 26. Read the two new events in the real Activity screen against section 8 copy.