Files
ITWorx-Pulse-Public/internal/config/config_test.go
T
ITWorx Pulse release export bd774932d5
Public source validation / validate (push) Failing after 3m8s
Publish ITWorx Pulse source
2026-09-03 02:09:19 +02:00

260 lines
10 KiB
Go

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)
}
}