6.6 KiB
6.6 KiB
QA procedure: runtime_settings_injection
Package: test (black-box).
Setup shared by every case
- Call
validator.InitValidator()once. - 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.goand siblings. - 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 actualapi/router.gowiring, driven by injected settings, behaves correctly, not that some parallel logic does. - Never set
t.Setenvbefore 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 witht.Setenvexplicitly after construction, and say so in the test's own scenario name (not as a code comment).
Secure cookie flag (Scenario Outline + regression scenario)
- Build a
mockUserRepo(reusetest/user_test.go's fake) seeded with one verified user and a known bcrypt hash viahashing.NewBcryptHasher(). - Build
auth.NewHandler(token, getUseCase, listUseCase, createUseCase, authenticateUseCase, <injected production flag>)— this slice adds a constructor parameter carrying the environment/production flag in place ofshouldCookieBeSecure'sos.Getenvcall. Mount onlyPOST /loginon a freshchi.NewRouter(). - For each row of the outline: optionally
t.Setenv("ENVIRONMENT", ...)with the process value from the table (this must have zero effect), then sendPOST /loginwith valid JSON credentials for the seeded user viahttptest.NewRequest+httptest.NewRecorder(). Read theSet-Cookieresponse header value withrec.Result().Header.Values("Set-Cookie"), parse it withhttp.ParseSetCookie(...), and assert.Secureequals the table's expected value. - 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 assertSecureis stilltrue.
CORS
- 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 testjwtauth.New("HS256", []byte("test-secret"), nil)), then call the realapi.InitRoutes(container, settings)(or whichever exported constructor this slice ends up exposing for the outer router, e.g. an exportedNewApiRouter(container, settings)) with aconfig.Settings{CorsAllowedOrigins: ...}value. Do not hand-roll a secondcors.Handlercall in the test; call the same construction pathcmd/server/main.gocalls. - For the allowed-origin case, build the router with
CorsAllowedOrigins: []string{"https://cms.example.com"}, sendGET /healthzwith headerOrigin: https://cms.example.com, and assert the response headerAccess-Control-Allow-Originequals"https://cms.example.com". - For the disallowed-origin case, same router, send the request with
Origin: https://attacker.example.com, and assert the response has noAccess-Control-Allow-Originheader (rec.Result().Header.Get(...)returns""). - For the empty-list case, build the router with
CorsAllowedOrigins: nil(or[]string{}), send a request toGET /healthzwith anyOriginheader, and assert the header is again absent.
Upload directory
- Reuse
openInMemoryDb(t)andmodels.Image{}migration astest/image_extension_mapping_test.godoes, but construct the repository aspersistence.NewImageRepository(db, uploadDir)whereuploadDir := t.TempDir()— not.Setenv("UPLOAD_DIR", ...)at all. Additionally callt.Setenv("UPLOAD_DIR", t.TempDir())with a different temp directory right beforeCreate, to prove it has no effect. - Call
repo.Create(imageDomain.ImageInput{...})with a PNG payload. Assert no error; assert the returnedPathstarts with/static/uploadedImages/; assert, viaos.Stat, that the file exists under the constructor'suploadDirand does not exist under thet.Setenvdirectory from step 13. - Update
test/image_extension_mapping_test.goin the same slice to passt.TempDir()straight intopersistence.NewImageRepository(db, uploadDir)instead oft.Setenv("UPLOAD_DIR", ...), since the constructor signature changes.
Mailer
- 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 noSMTP_*environment variable set (do not callt.Setenvfor anySMTP_*name in this test). Assert construction does not panic and does not error — this is the only automated assertion available, since exercisingSendrequires a real SMTP dialog that this suite deliberately never performs (matchestest/mail_test.go, which already testsSendMailUseCaseagainstmockMailRepo, not the realMailRepository).
Manual verification (not exercised by the Go test)
- Real SMTP send: run
task dev(or a built binary) with a real.envpopulated with SMTP_LOGIN/SMTP_SENDER/SMTP_PASSWORD/SMTP_HOST/SMTP_PORT, trigger a flow that callsSendMailUseCase.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=developmentvsENVIRONMENT=productionand confirmhttplogoutput is present only in the development case; this is not response-observable so it isn't asserted byhttptest. - Confirm
api/dependecyInjection.go'sgetDbandgetUseCasesconsumeDB_FILEandJWT_SECRETfrom the sameconfig.Settingsvalue built incmd/server/main.go, rather than callingos.Getenvthemselves — 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 strayos.Getenvremains there either. - SPA-visible behaviour: none: this slice has no SPA-facing change.