# QA procedure: runtime_settings_injection Package: `test` (black-box). ## Setup shared by every case 1. Call `validator.InitValidator()` once. 2. For the secure-cookie and upload-directory cases, build only the pieces under test directly (no real SQLite file), mirroring `test/article_publishing_http_test.go` and siblings. 3. For the CORS cases, build the **real** router construction path — see "CORS" section below. A test-local reimplementation of the CORS wiring (a helper that copies `enableCorsIfConfigured`'s logic instead of calling it) is not acceptable: the point of these scenarios is to prove the actual `api/router.go` wiring, driven by injected settings, behaves correctly, not that some parallel logic does. 4. Never set `t.Setenv` before constructing a component under test in a way that the component is expected to read; the whole point of this suite is that env is captured once elsewhere. Where a scenario needs to prove the environment has no effect, set it with `t.Setenv` explicitly *after* construction, and say so in the test's own scenario name (not as a code comment). ## Secure cookie flag (Scenario Outline + regression scenario) 5. Build a `mockUserRepo` (reuse `test/user_test.go`'s fake) seeded with one verified user and a known bcrypt hash via `hashing.NewBcryptHasher()`. 6. Build `auth.NewHandler(token, getUseCase, listUseCase, createUseCase, authenticateUseCase, )` — this slice adds a constructor parameter carrying the environment/production flag in place of `shouldCookieBeSecure`'s `os.Getenv` call. Mount only `POST /login` on a fresh `chi.NewRouter()`. 7. For each row of the outline: optionally `t.Setenv("ENVIRONMENT", ...)` with the process value from the table (this must have zero effect), then send `POST /login` with valid JSON credentials for the seeded user via `httptest.NewRequest` + `httptest.NewRecorder()`. Read the `Set-Cookie` response header value with `rec.Result().Header.Values("Set-Cookie")`, parse it with `http.ParseSetCookie(...)`, and assert `.Secure` equals the table's expected value. 8. For the "changing the process environment after construction" scenario, build the handler once with the production flag fixed at construction time, then call `t.Setenv("ENVIRONMENT", "development")`, then perform the same login request and assert `Secure` is still `true`. ## CORS 9. Build a `*api.UseCases` (or whatever the composition-root container is named after this slice) backed by the existing in-memory fakes (`mockArticleRepo`, `mockUserRepo`, `mockImageRepo`, `mockMailRepo`, a test `jwtauth.New("HS256", []byte("test-secret"), nil)`), then call the real `api.InitRoutes(container, settings)` (or whichever exported constructor this slice ends up exposing for the outer router, e.g. an exported `NewApiRouter(container, settings)`) with a `config.Settings{CorsAllowedOrigins: ...}` value. Do not hand-roll a second `cors.Handler` call in the test; call the same construction path `cmd/server/main.go` calls. 10. For the allowed-origin case, build the router with `CorsAllowedOrigins: []string{"https://cms.example.com"}`, send `GET /healthz` with header `Origin: https://cms.example.com`, and assert the response header `Access-Control-Allow-Origin` equals `"https://cms.example.com"`. 11. For the disallowed-origin case, same router, send the request with `Origin: https://attacker.example.com`, and assert the response has no `Access-Control-Allow-Origin` header (`rec.Result().Header.Get(...)` returns `""`). 12. For the empty-list case, build the router with `CorsAllowedOrigins: nil` (or `[]string{}`), send a request to `GET /healthz` with any `Origin` header, and assert the header is again absent. ## Upload directory 13. Reuse `openInMemoryDb(t)` and `models.Image{}` migration as `test/image_extension_mapping_test.go` does, but construct the repository as `persistence.NewImageRepository(db, uploadDir)` where `uploadDir := t.TempDir()` — no `t.Setenv("UPLOAD_DIR", ...)` at all. Additionally call `t.Setenv("UPLOAD_DIR", t.TempDir())` with a *different* temp directory right before `Create`, to prove it has no effect. 14. Call `repo.Create(imageDomain.ImageInput{...})` with a PNG payload. Assert no error; assert the returned `Path` starts with `/static/uploadedImages/`; assert, via `os.Stat`, that the file exists under the constructor's `uploadDir` and does **not** exist under the `t.Setenv` directory from step 13. 15. Update `test/image_extension_mapping_test.go` in the same slice to pass `t.TempDir()` straight into `persistence.NewImageRepository(db, uploadDir)` instead of `t.Setenv("UPLOAD_DIR", ...)`, since the constructor signature changes. ## Mailer 16. Construct `mailer.NewMailRepository(config.SmtpSettings{Login: "smtp-login@example.com", Sender: "notify@example.com", Host: "smtp.example.com", Port: 587, Password: "unused-in-test"})` with no `SMTP_*` environment variable set (do not call `t.Setenv` for any `SMTP_*` name in this test). Assert construction does not panic and does not error — this is the only automated assertion available, since exercising `Send` requires a real SMTP dialog that this suite deliberately never performs (matches `test/mail_test.go`, which already tests `SendMailUseCase` against `mockMailRepo`, not the real `MailRepository`). ## Manual verification (not exercised by the Go test) - Real SMTP send: run `task dev` (or a built binary) with a real `.env` populated with SMTP_LOGIN/SMTP_SENDER/SMTP_PASSWORD/SMTP_HOST/SMTP_PORT, trigger a flow that calls `SendMailUseCase.SendMail`, and confirm the message actually leaves via the injected host/port/credentials, and that changing those env vars requires a process restart to take effect (there is no live-reload). - Dev-only request logging: run the binary with `ENVIRONMENT=development` vs `ENVIRONMENT=production` and confirm `httplog` output is present only in the development case; this is not response-observable so it isn't asserted by `httptest`. - Confirm `api/dependecyInjection.go`'s `getDb` and `getUseCases` consume `DB_FILE` and `JWT_SECRET` from the same `config.Settings` value built in `cmd/server/main.go`, rather than calling `os.Getenv` themselves — this stays inside the composition root per CLAUDE.md and is not a behaviour change, so it has no dedicated scenario, but review the diff to confirm no stray `os.Getenv` remains there either. - SPA-visible behaviour: none: this slice has no SPA-facing change.