# QA procedure: upload_article_image Package: `test` (black-box, extending `article_test.go` / `image_test.go`). ## Setup shared by every case 1. Call `validator.InitValidator()` once. 2. Reuse `mockArticleRepo` (`test/article_test.go`) and `mockImageRepo` (`test/image_test.go`) as-is; both already implement `AddImage` / `Create` returning `article.ErrArticleNotFound` / `image.ErrImageNotFound` for an unknown id, per `features/write_not_found_errors.feature`. 3. Wire the real use cases over the fakes: `useCases.NewCreateArticleUseCase`, `useCases.NewGetArticleUseCase`, `useCases.NewListArticlesUseCase`, `useCases.NewUpdateArticleUseCase(articleRepo)`, `useCases.NewDeleteArticleUseCase(articleRepo, imageRepo)`, `useCases.NewCreateImageUseCase(imageRepo)`. 4. Build `article.NewHandler(...)` with its existing constructor shape, and `image.NewHandler(createImageUseCase, updateArticleUseCase)` (`api/handlers/image`). 5. Build a test `jwtauth.New("HS256", []byte("test-secret"), nil)`. 6. Mount the routes on a fresh `chi.NewRouter()` mirroring `api/router.go`'s shape after this slice: `GET /` under `jwtauth.Verifier` only; `GET /{id}`, `POST /`, `DELETE /{id}`, `PATCH /{id}`, `PATCH /{id}/publish`, `PATCH /{id}/unpublish` and the new `POST /{id}/image` (routed to `imageHandler.PostImage`) under `jwtauth.Verifier` + `jwtauth.Authenticator`. 7. Mint a valid jwt cookie for user id 1: `token.Encode(map[string]any{"user_id": uint32(1)})`, set as the `jwt` cookie on the relevant `httptest.NewRequest`. 8. Build multipart request bodies with `mime/multipart.Writer`, field name `"file"`, setting the part's `Content-Type` header explicitly (Go's multipart writer defaults to sniffing otherwise, so set it via `CreatePart` with an explicit MIME header rather than `CreateFormFile`), and set the request's `Content-Type` header to the writer's `FormDataContentType()`. ## HTTP checks 9. Seed `articleRepo.articles[1] = article.Article{ID: 1, Title: "Backyard Renovation Update"}`. Send `POST /v1/article/1/image` with the valid cookie and a multipart `"file"` part named `"before.png"`, content type `"image/png"`, some non-empty byte content. Assert status `201`; decode the JSON body and assert `location` equals `"/static/uploadedImages/before.png"`; assert `imageRepo.images` gained exactly one entry whose `Path` matches that location; assert `articleRepo.articles[1].Images` now contains that image's id. 10. Empty `articleRepo`, empty `imageRepo`. Send `POST /v1/article/9999/image` with the valid cookie and the same `"before.png"` / `"image/png"` part. Assert status `404`, body `"Article not found\n"`; assert `imageRepo.images` is still empty (the article's existence is confirmed before any image is created, so nothing is left orphaned). 11. Seed `articleRepo.articles[1] = article.Article{ID: 1, Title: "Backyard Renovation Update"}` with an empty `Images` slice. Send `POST /v1/article/1/image` with the valid cookie and a multipart `"file"` part named `"notes.pdf"`, content type `"application/pdf"`. Assert status `400`, body `"the file must be a PNG, JPEG, WEBP, or SVG image\n"`; assert `imageRepo.images` is still empty; assert `articleRepo.articles[1].Images` is still empty. 12. Seed the same article id 1 as step 11. Send `POST /v1/article/1/image` with **no** cookie and the `"before.png"` / `"image/png"` part. Assert status `401`; assert `imageRepo.images` is still empty; assert `articleRepo.articles[1].Images` is still empty. ## Manual verification (not exercised by the Go test) - Real upload against a real SQLite file and real `UPLOAD_DIR`: confirm that after `POST /v1/article/{id}/image` on a real running instance, the file actually lands on disk under `UPLOAD_DIR`, the `images` row's `path` resolves to a URL the SPA can load, and the `article_images` join table gets the new row. Not exercised by `task test` (fakes only); requires `task dev` or a built `task build` binary with a scratch DB/upload dir. - SPA-visible behaviour: confirm the back-office article editor (if/when an image-upload UI exists) shows the uploaded image at that location. No such UI exists yet as of this slice. - No SMTP involvement.