package config import ( "strings" "testing" "time" ) func TestProductionListsAllMissingMandatoryValues(t *testing.T) { values := map[string]string{"PULSE_ENV": "production"} _, err := LoadFrom(mapLookup(values)) if err == nil { t.Fatal("expected production validation error") } message := err.Error() for _, key := range []string{"PULSE_PUBLIC_URL", "PULSE_DATABASE_URL", "PULSE_OIDC_ISSUER", "PULSE_OIDC_CLIENT_ID", "PULSE_OIDC_CLIENT_SECRET", "PULSE_OIDC_REDIRECT_URL"} { if !strings.Contains(message, key) { t.Errorf("error %q does not mention %s", message, key) } } } func TestWebhookConfigurationIsSecureAndRedacted(t *testing.T) { secret := "webhook-runtime-secret" configuration, err := LoadFrom(mapLookup(map[string]string{ "PULSE_ENV": "development", "PULSE_NOTIFICATION_WEBHOOK_URL": "http://127.0.0.1:8080/pulse", "PULSE_NOTIFICATION_WEBHOOK_TOKEN": secret, "PULSE_NOTIFICATION_WEBHOOK_TIMEOUT": "3s", })) if err != nil { t.Fatal(err) } if configuration.NotificationWebhookTimeout != 3*time.Second { t.Fatalf("timeout = %s", configuration.NotificationWebhookTimeout) } if strings.Contains(configuration.String(), secret) || strings.Contains(configuration.Redacted().NotificationWebhookToken, secret) { t.Fatal("webhook credential leaked through config rendering") } for name, values := range map[string]map[string]string{ "missing token": {"PULSE_NOTIFICATION_WEBHOOK_URL": "https://receiver.example/hook"}, "query token": {"PULSE_NOTIFICATION_WEBHOOK_URL": "https://receiver.example/hook?token=value", "PULSE_NOTIFICATION_WEBHOOK_TOKEN": secret}, "production http": {"PULSE_ENV": "production", "PULSE_NOTIFICATION_WEBHOOK_URL": "http://receiver.example/hook", "PULSE_NOTIFICATION_WEBHOOK_TOKEN": secret}, "unbounded timeout": {"PULSE_NOTIFICATION_WEBHOOK_URL": "https://receiver.example/hook", "PULSE_NOTIFICATION_WEBHOOK_TOKEN": secret, "PULSE_NOTIFICATION_WEBHOOK_TIMEOUT": "31s"}, } { t.Run(name, func(t *testing.T) { if _, err := LoadFrom(mapLookup(values)); err == nil { t.Fatal("expected webhook configuration rejection") } }) } } func TestProductionRejectsMockAuth(t *testing.T) { values := map[string]string{ "PULSE_ENV": "production", "PULSE_PUBLIC_URL": "https://pulse.example", "PULSE_DATABASE_URL": "postgres://pulse@db/pulse", "PULSE_OIDC_ISSUER": "https://auth.example", "PULSE_OIDC_CLIENT_ID": "pulse", "PULSE_OIDC_CLIENT_SECRET": "secret-value", "PULSE_OIDC_REDIRECT_URL": "https://pulse.example/auth/callback", "PULSE_AUTH_MODE": "mock", } _, err := LoadFrom(mapLookup(values)) if err == nil || !strings.Contains(err.Error(), "mock") { t.Fatalf("expected mock-auth rejection, got %v", err) } } func TestValidationErrorsAndStringNeverExposeSecrets(t *testing.T) { secret := "super-secret-token" values := map[string]string{ "PULSE_ENV": "production", "PULSE_DATABASE_URL": "not-a-url", "PULSE_OIDC_CLIENT_SECRET": secret, "PULSE_UNRAID_API_TOKEN": secret, "PULSE_AUTH_MODE": "mock", } config, err := LoadFrom(mapLookup(values)) if err == nil { t.Fatal("expected validation error") } if strings.Contains(err.Error(), secret) { t.Fatal("validation error leaked a secret") } if strings.Contains(config.String(), secret) { t.Fatal("config String leaked a secret") } } func TestDevelopmentDefaultsAllowExplicitMockMode(t *testing.T) { values := map[string]string{"PULSE_ENV": "development", "PULSE_AUTH_MODE": "mock"} config, err := LoadFrom(mapLookup(values)) if err != nil { t.Fatalf("development defaults rejected: %v", err) } if config.Environment != Development || config.AuthMode != "mock" { t.Fatalf("unexpected config: %s", config) } if config.SessionIdleTTL != 8*time.Hour || config.SessionAbsoluteTTL != 7*24*time.Hour { t.Fatalf("unexpected session defaults: idle=%s absolute=%s", config.SessionIdleTTL, config.SessionAbsoluteTTL) } } func TestSessionLifetimeConfigurationIsBounded(t *testing.T) { configured, err := LoadFrom(mapLookup(map[string]string{ "PULSE_ENV": "development", "PULSE_AUTH_MODE": "mock", "PULSE_SESSION_IDLE_TTL": "20s", "PULSE_SESSION_ABSOLUTE_TTL": "2m", })) if err != nil { t.Fatal(err) } if configured.SessionIdleTTL != 20*time.Second || configured.SessionAbsoluteTTL != 2*time.Minute { t.Fatalf("unexpected session lifetimes: %#v", configured) } for name, values := range map[string]map[string]string{ "invalid duration": {"PULSE_SESSION_IDLE_TTL": "later"}, "idle too short": {"PULSE_SESSION_IDLE_TTL": "4s"}, "absolute shorter than idle": {"PULSE_SESSION_IDLE_TTL": "20s", "PULSE_SESSION_ABSOLUTE_TTL": "10s"}, "absolute unbounded": {"PULSE_SESSION_ABSOLUTE_TTL": "721h"}, } { t.Run(name, func(t *testing.T) { if _, err := LoadFrom(mapLookup(values)); err == nil { t.Fatal("expected bounded session configuration rejection") } }) } } func TestProductionRequiresWallboardCapableAbsoluteSessionLifetime(t *testing.T) { config := Config{ Environment: Production, Timezone: "Europe/Brussels", DefaultLocale: "nl-BE", LogLevel: "info", PublicURL: "https://pulse.example", DatabaseURL: "postgres://pulse@db/pulse", AuthMode: "oidc", OIDCIssuer: "https://auth.example", OIDCClientID: "pulse", OIDCClientSecret: "secret", OIDCRedirectURL: "https://pulse.example/auth/callback", OIDCRoleMapping: map[string]string{"viewer": "viewer"}, PrometheusTimeout: 10 * time.Second, NotificationWebhookTimeout: 10 * time.Second, BackupRetention: 5, SessionIdleTTL: 8 * time.Hour, SessionAbsoluteTTL: 23 * time.Hour, } if err := Validate(config); err == nil || !strings.Contains(err.Error(), "PULSE_SESSION_ABSOLUTE_TTL") { t.Fatalf("production accepted a session unable to cover the wallboard budget: %v", err) } config.SessionAbsoluteTTL = 24 * time.Hour config.SessionIdleTTL = 5 * time.Minute if err := Validate(config); err == nil || !strings.Contains(err.Error(), "PULSE_SESSION_IDLE_TTL") { t.Fatalf("production accepted an idle TTL that can race the five-minute wallboard refresh: %v", err) } config.SessionIdleTTL = 10 * time.Minute if err := Validate(config); err != nil { t.Fatalf("bounded 24-hour production session rejected: %v", err) } } func TestBackupConfigurationIsBoundedAndOptional(t *testing.T) { values := map[string]string{"PULSE_ENV": "development", "PULSE_AUTH_MODE": "mock", "PULSE_BACKUP_DIR": "C:/pulse-backups", "PULSE_BACKUP_RETENTION": "12"} config, err := LoadFrom(mapLookup(values)) if err != nil { t.Fatalf("backup config rejected: %v", err) } if config.BackupDirectory != values["PULSE_BACKUP_DIR"] || config.BackupRetention != 12 { t.Fatalf("unexpected backup config: %#v", config) } values["PULSE_BACKUP_RETENTION"] = "101" if _, err := LoadFrom(mapLookup(values)); err == nil || !strings.Contains(err.Error(), "PULSE_BACKUP_RETENTION") { t.Fatalf("expected bounded retention error, got %v", err) } } func mapLookup(values map[string]string) func(string) (string, bool) { return func(key string) (string, bool) { value, ok := values[key]; return value, ok } } func TestRoleMappingParsesClaimsOntoRoles(t *testing.T) { config, err := LoadFrom(mapLookup(map[string]string{ "PULSE_OIDC_ROLE_MAPPING": "pulse-admin=administrator, pulse-staff =viewer,pulse-ops=Operator", })) if err != nil { t.Fatalf("unexpected error: %v", err) } expected := map[string]string{"pulse-admin": "administrator", "pulse-staff": "viewer", "pulse-ops": "operator"} if len(config.OIDCRoleMapping) != len(expected) { t.Fatalf("expected %d mapped claims, got %d", len(expected), len(config.OIDCRoleMapping)) } for claim, role := range expected { if config.OIDCRoleMapping[claim] != role { t.Fatalf("claim %q mapped to %q, want %q", claim, config.OIDCRoleMapping[claim], role) } } } func TestRoleMappingDefaultsToNoAccess(t *testing.T) { config, err := LoadFrom(mapLookup(nil)) if err != nil { t.Fatalf("unexpected error: %v", err) } if config.OIDCRoleMapping != nil { t.Fatal("an unset role mapping must grant nobody a role") } if config.OIDCGroupsClaim != "groups" { t.Fatalf("groups claim defaulted to %q, want groups", config.OIDCGroupsClaim) } } func TestRoleMappingRejectsMalformedInput(t *testing.T) { for name, raw := range map[string]string{ "missing separator": "pulse-admin", "empty claim": "=administrator", "empty role": "pulse-admin=", "unknown role": "pulse-admin=superuser", "duplicate claim": "pulse-admin=viewer,pulse-admin=editor", "only separators": ",,", } { t.Run(name, func(t *testing.T) { if _, err := LoadFrom(mapLookup(map[string]string{"PULSE_OIDC_ROLE_MAPPING": raw})); err == nil { t.Fatalf("expected %q to be rejected", raw) } }) } } func TestProductionRequiresARoleMapping(t *testing.T) { base := map[string]string{ "PULSE_ENV": "production", "PULSE_PUBLIC_URL": "https://pulse.example.test", "PULSE_DATABASE_URL": "postgres://pulse@db:5432/pulse", "PULSE_OIDC_ISSUER": "https://id.example.test", "PULSE_OIDC_CLIENT_ID": "pulse", "PULSE_OIDC_CLIENT_SECRET": "secret", "PULSE_OIDC_REDIRECT_URL": "https://pulse.example.test/auth/callback", } if _, err := LoadFrom(mapLookup(base)); err == nil { t.Fatal("production without a role mapping must fail: no identity could obtain a role") } base["PULSE_OIDC_ROLE_MAPPING"] = "pulse-admin=administrator" if _, err := LoadFrom(mapLookup(base)); err != nil { t.Fatalf("production with a role mapping must succeed: %v", err) } } func TestLoadWorkerProductionDoesNotRequireAPICredentials(t *testing.T) { configuration, err := LoadWorkerFrom(mapLookup(map[string]string{ "PULSE_ENV": "production", "PULSE_DATABASE_URL": "postgres://pulse:secret@pulse-postgres:5432/pulse?sslmode=disable", "PULSE_PROMETHEUS_URL": "http://192.0.2.10:9090", })) if err != nil { t.Fatalf("LoadWorkerFrom returned API-only validation error: %v", err) } if configuration.Environment != Production || configuration.DatabaseURL == "" { t.Fatalf("unexpected worker config: %#v", configuration) } } func TestLoadWorkerStillRequiresDatabaseAndValidatesSources(t *testing.T) { _, err := LoadWorkerFrom(mapLookup(map[string]string{ "PULSE_ENV": "production", "PULSE_PROMETHEUS_URL": "://invalid", })) if err == nil { t.Fatal("LoadWorkerFrom accepted missing database and malformed Prometheus URL") } message := err.Error() if !strings.Contains(message, "PULSE_DATABASE_URL") || !strings.Contains(message, "PULSE_PROMETHEUS_URL") { t.Fatalf("worker validation error = %q", message) } }