143 lines
4.7 KiB
Go
143 lines
4.7 KiB
Go
package config
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// DM-06: config.go used to default JWT_SECRET_KEY, the NATS URL and its
|
|
// credentials, and the AI/optimiser hosts to the REAL production values. Two
|
|
// consequences: anyone holding the repository could mint a valid token for any
|
|
// account against a deployment that had not overridden the secret, and any
|
|
// local run silently joined the live NATS stream.
|
|
|
|
// The literals that must never come back. Written out so a revert is a test
|
|
// failure rather than something noticed in review.
|
|
func TestProductionValuesAreNotDefaults(t *testing.T) {
|
|
for _, key := range []string{
|
|
"JWT_SECRET_KEY", "NATS_URL", "NATS_USER", "NATS_PASSWORD",
|
|
"AI_LAYER_BASE_URL", "ROUTE_OPTIMIZER_URL", "DB_PASSWORD",
|
|
} {
|
|
setEnv(t, key, "")
|
|
}
|
|
setEnv(t, "ENV", "development")
|
|
|
|
cfg := Load()
|
|
|
|
banned := map[string]string{
|
|
"NatsURL": cfg.NatsURL,
|
|
"NatsUser": cfg.NatsUser,
|
|
"NatsPassword": cfg.NatsPassword,
|
|
"AILayerBaseURL": cfg.AILayerBaseURL,
|
|
"RouteOptimizerURL": cfg.RouteOptimizerURL,
|
|
"DBPassword": cfg.DBPassword,
|
|
}
|
|
for field, got := range banned {
|
|
if got != "" {
|
|
t.Errorf("%s defaulted to %q — production values must not be defaults", field, got)
|
|
}
|
|
}
|
|
|
|
// The old hardcoded secret must not be what we sign with.
|
|
if cfg.JWTSecret == "DoormileSuperSecretJWTKey2026!" {
|
|
t.Error("JWTSecret fell back to the literal that used to be in config.go")
|
|
}
|
|
}
|
|
|
|
// With no secret configured outside production the service still runs, but on
|
|
// a key that exists only for this process.
|
|
func TestUnsetSecretOutsideProductionIsEphemeralNotShared(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "")
|
|
setEnv(t, "ENV", "development")
|
|
|
|
first := Load().JWTSecret
|
|
second := Load().JWTSecret
|
|
|
|
if first == "" || second == "" {
|
|
t.Fatal("an unset secret produced an empty signing key; tokens would be forgeable")
|
|
}
|
|
if first == second {
|
|
t.Error("two loads produced the same generated key — it is not ephemeral")
|
|
}
|
|
if len(first) < 32 {
|
|
t.Errorf("generated key is %d chars, too short to be a signing key", len(first))
|
|
}
|
|
}
|
|
|
|
// In production an absent secret is NOT quietly replaced — it is left absent so
|
|
// Validate can refuse the boot with a message that says why. Silently
|
|
// generating one would invalidate every live session on each restart.
|
|
func TestProductionRefusesToStartWithoutASecret(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "")
|
|
setEnv(t, "ENV", "production")
|
|
|
|
cfg := Load()
|
|
if cfg.JWTSecret != "" {
|
|
t.Errorf("production generated a secret (%q); it must stay empty so Validate fails", cfg.JWTSecret)
|
|
}
|
|
err := cfg.Validate()
|
|
if err == nil {
|
|
t.Fatal("Validate accepted an empty JWT secret in production")
|
|
}
|
|
if !strings.Contains(err.Error(), "JWT_SECRET_KEY") {
|
|
t.Errorf("Validate error does not name the variable: %v", err)
|
|
}
|
|
}
|
|
|
|
// Outside production the ephemeral key is enough to pass validation, so local
|
|
// development needs no configuration at all.
|
|
func TestValidatePassesOutsideProductionWithNoSecret(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "")
|
|
setEnv(t, "ENV", "development")
|
|
|
|
if err := Load().Validate(); err != nil {
|
|
t.Errorf("development should boot without a configured secret: %v", err)
|
|
}
|
|
}
|
|
|
|
// A configured secret always validates, production or not.
|
|
func TestValidatePassesWithAConfiguredSecret(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "a-real-configured-secret")
|
|
setEnv(t, "ENV", "production")
|
|
|
|
if err := Load().Validate(); err != nil {
|
|
t.Errorf("a configured secret must validate: %v", err)
|
|
}
|
|
}
|
|
|
|
// An explicitly configured secret is always used verbatim.
|
|
func TestConfiguredSecretIsUsedVerbatim(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "a-real-configured-secret")
|
|
setEnv(t, "ENV", "production")
|
|
|
|
if got := Load().JWTSecret; got != "a-real-configured-secret" {
|
|
t.Errorf("JWTSecret = %q, want the configured value", got)
|
|
}
|
|
}
|
|
|
|
func TestIsProductionIsCaseAndSpaceInsensitive(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "x")
|
|
for _, env := range []string{"production", "Production", "PRODUCTION", " production "} {
|
|
setEnv(t, "ENV", env)
|
|
if !Load().IsProduction() {
|
|
t.Errorf("ENV=%q was not treated as production", env)
|
|
}
|
|
}
|
|
for _, env := range []string{"development", "staging", "test", ""} {
|
|
setEnv(t, "ENV", env)
|
|
if Load().IsProduction() {
|
|
t.Errorf("ENV=%q was treated as production", env)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The geocoder is a PUBLIC service, not a Doormile host, so it keeps its
|
|
// default — removing it would break place search for no security gain.
|
|
func TestGeocoderKeepsItsPublicDefault(t *testing.T) {
|
|
setEnv(t, "JWT_SECRET_KEY", "x")
|
|
setEnv(t, "GEOCODER_URL", "")
|
|
if got := Load().GeocoderURL; !strings.Contains(got, "nominatim") {
|
|
t.Errorf("GeocoderURL = %q, want the public Nominatim default", got)
|
|
}
|
|
}
|