# QA: S3 image storage All tests live in test/ (package test). No cloud account, no real SMTP, no real SQLite file. Test names carry "S3Storage". Settled decisions: boot backfill gets a log line only, wired in the composition root, with no cap and no use case change. No ETag is emitted; the staticimages handler is unchanged and conditional requests are validated by Last-Modified only. ## Automated 1. Config validation: table test over STORAGE_DRIVER and S3_* lookup functions (the MissingEnvVarError pattern, a `StorageRequiredEnvVars(driver)` helper like `DatabaseRequiredEnvVars`). Covers the validation table, UPLOAD_DIR ignored, unknown driver error. 2. Registry: storage.Open with "local" returns local storage, "s3" returns the S3 storage after HEAD bucket, anything else returns the "unsupported storage driver" error. 3. Fake S3: an httptest.Server on 127.0.0.1 only (never bind :port) implementing HEAD bucket, PUT, GET with Range, HEAD and DELETE object, with injectable statuses and recorded keys, headers and bodies. 4. Adapter tests against the fake: the Put, Open (found, 404, 500) and Delete scenarios, HEAD bucket 404/403/500, prefix handling, ModTime and Size from metadata, Content as io.ReadSeeker (seek then read issues a ranged GET), secret absent from error text. 5. Handler tests: staticimages.NewHandler over OpenMediaUseCase over the S3 storage on the fake server, mounted on a fresh chi.NewRouter() at /static/uploadedImages/*. Cover GET, HEAD, 304 via If-Modified-Since, 206, 416, SVG CSP, 404, 500, traversal. 6. Handler regression: a storage fake returning a non-seekable Blob. GET and HEAD behave exactly as today. 7. Backfill log: the boot log line for the number of images read, captured with a log writer, containing no credential. 8. Local regression: all existing image tests pass unchanged. 9. Gates: `task test` and `task lint:go` pass. `task mutate` is not part of this slice, but the new storage package becomes a target if the mutation scope is widened. The 404 to ErrBlobNotFound and idempotent-Delete branches need killing tests. ## Optional integration (task test:s3) Start a SeaweedFS container with Docker, create a bucket, set RENEWCMS_TEST_S3_ENDPOINT and run the adapter tests against it. Skip with a message when Docker is unavailable. Not part of `task test`. ## Manual - Run docker-compose.s3.yml, upload an image in the SPA, confirm the object is in the bucket and /static/uploadedImages/ serves it, including after the container is recreated. - Real AWS S3, R2, B2 or Scaleway smoke test, and an instance-role credential check. - Start with a wrong bucket and confirm a clear, secret-free startup error. - Sync an old UPLOAD_DIR with `aws s3 sync` or `mc mirror` and confirm old images load. - Confirm docs describe: variables (configuration.md, .env.example, docker.env.example, Docker docs), UPLOAD_DIR ignored with s3, manual migration, update backups covering the DB only and not the bucket, and the updated architecture.md "Storage and database seams" section.