Files
RenewCMS/features/runtime_settings_injection.feature
T

122 lines
6.7 KiB
Gherkin

Feature: Runtime settings are read once at startup
As a maintainer of RenewCMS
I want the environment, CORS origins, public URL, upload dir and SMTP
settings to be read from the process environment exactly once, in the
composition root, and handed down explicitly from there
So that no handler, router or repository reads an environment variable at
request or construction time, and changing the process environment after
startup can never change already-built behaviour
Background:
Given before this slice, os.Getenv is called outside cmd/server/main.go
in four places: api/router.go's enableLoggingIfDev reads ENVIRONMENT
on every router build, api/router.go's enableCorsIfConfigured reads
CORS_ALLOWED_ORIGINS on every router build,
api/handlers/auth.shouldCookieBeSecure reads ENVIRONMENT on every
Login/Register/Logout call, internal/infrastructure/mailer.MailRepository.Send
reads SMTP_LOGIN, SMTP_SENDER, SMTP_PASSWORD, SMTP_HOST and SMTP_PORT
on every call, and internal/infrastructure/persistence.ImageRepository.Create
and .Delete read UPLOAD_DIR on every call
And this slice introduces internal/infrastructure/config.Settings, a
plain struct with fields Environment, CorsAllowedOrigins ([]string),
PublicURL, UploadDir, JWTSecret, DBFile, Port and an Smtp sub-struct
(Login, Sender, Password, Host, Port)
And cmd/server/main.go's initEnvVariables still loads the .env file and
applies the same defaults and required-var checks as before, then
builds exactly one config.Settings value from the process environment,
before api.InitContainer or api.InitRoutes is ever called
And api.InitContainer and api.InitRoutes now take that config.Settings
value, and every downstream constructor receives its own slice of it
explicitly instead of reading os.Getenv: auth.NewHandler receives
whether the environment is production, mailer.NewMailRepository
receives the Smtp settings, persistence.NewImageRepository receives
the upload dir, and the router's logging and CORS setup receive the
environment string and the CORS origins list as parameters
And after this slice, os.Getenv (and os.LookupEnv) for ENVIRONMENT,
CORS_ALLOWED_ORIGINS, SMTP_LOGIN, SMTP_SENDER, SMTP_PASSWORD,
SMTP_HOST, SMTP_PORT and UPLOAD_DIR appears only inside
cmd/server/main.go and internal/infrastructure/config
And a component built from a config.Settings value keeps behaving
according to the values it was built with even if the process
environment changes afterwards, because it holds the captured values,
not the environment
And GET /healthz is mounted directly on the outer API router
(api/router.go), not under /v1, so the CORS middleware wraps it the
same way it wraps every route
Scenario Outline: The jwt cookie's Secure flag follows the injected environment, not the process environment
Given a config.Settings value with Environment "<injected environment>"
And the auth handler is built from that settings value
And the process environment variable ENVIRONMENT is set to "<process environment>"
When the client sends POST /v1/auth/login with valid credentials for an
existing, verified user
Then the response's Set-Cookie header for the "jwt" cookie has its
Secure attribute set to <secure expected>
Examples:
| injected environment | process environment | secure expected |
| production | production | true |
| production | development | true |
| development | production | false |
| development | development | false |
Scenario: CORS allows a request from an injected allowed origin
Given a config.Settings value with CorsAllowedOrigins
["https://cms.example.com"]
And the API router is built from that settings value
When the client sends GET /healthz with header
Origin: https://cms.example.com
Then the response has header Access-Control-Allow-Origin:
https://cms.example.com
Scenario: CORS does not allow a request from an origin outside the injected list
Given a config.Settings value with CorsAllowedOrigins
["https://cms.example.com"]
And the API router is built from that settings value
When the client sends GET /healthz with header
Origin: https://attacker.example.com
Then the response has no Access-Control-Allow-Origin header
Scenario: An empty injected origins list disables CORS entirely
Given a config.Settings value with CorsAllowedOrigins as an empty list
And the API router is built from that settings value
When the client sends GET /healthz with header
Origin: https://cms.example.com
Then the response has no Access-Control-Allow-Origin header
Scenario: An uploaded image is stored under the injected upload directory
Given a config.Settings value with UploadDir pointing at a fresh
directory created for this scenario
And the image repository is built from that settings value, over a
fresh in-memory database
And the process environment variable UPLOAD_DIR is unset or points at a
different directory
When internal/infrastructure/persistence.ImageRepository.Create is
called with a PNG file
Then the image file is written inside the injected upload directory,
not inside the process environment's UPLOAD_DIR
And the returned image's Path still starts with "/static/uploadedImages/"
Scenario: Changing the process environment after construction does not change already-built behaviour
Given a config.Settings value with Environment "production"
And the auth handler is built from that settings value
When the process environment variable ENVIRONMENT is then changed to
"development"
And the client sends POST /v1/auth/login with valid credentials for an
existing, verified user
Then the response's Set-Cookie header for the "jwt" cookie still has its
Secure attribute set to true
Scenario: The mailer is constructed from injected SMTP settings, not the environment
Given a config.Settings value with Smtp.Login "smtp-login@example.com",
Smtp.Sender "notify@example.com", Smtp.Host "smtp.example.com" and
Smtp.Port 587
And internal/infrastructure/mailer.NewMailRepository is called with that
Smtp settings value and no SMTP_* environment variable set
Then constructing the MailRepository does not read any SMTP_*
environment variable
And the maintainer verifies by manual test (see QA procedure) that an
actual send still uses those injected values, since a real SMTP
dialog is never exercised by the automated suite