Files
RenewCMS/features/runtime_settings_injection.qa.md
T

6.5 KiB

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).
  1. Build a mockUserRepo (reuse test/user_test.go's fake) seeded with one verified user and a known bcrypt hash via hashing.NewBcryptHasher().
  2. Build auth.NewHandler(token, getUseCase, listUseCase, createUseCase, authenticateUseCase, <injected production flag>) — 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().
  3. 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.
  4. 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

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

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

  1. Construct mailer.NewMailRepository(config.SmtpSettings{Email: "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_EMAIL/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.