package config import ( "log/slog" "testing" "time" ) func TestLoadDefaultsAndValidation(t *testing.T) { t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef") t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2") t.Setenv("CONTROL_ADDR", "") t.Setenv("CONTROL_DB_PATH", "") // The lab broker has no TLS listener yet, so the pre-TLS escape hatch is // the current deployment default (see CONTROL_MQTT_INSECURE). t.Setenv("CONTROL_MQTT_INSECURE", "true") cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if cfg.Addr != ":8791" { t.Errorf("Addr = %q, want :8791", cfg.Addr) } if cfg.DBPath != "/data/control.db" { t.Errorf("DBPath = %q, want /data/control.db", cfg.DBPath) } if cfg.AdminUsername != "sam" { t.Errorf("AdminUsername = %q, want sam", cfg.AdminUsername) } if cfg.SessionTTL != 12*time.Hour { t.Errorf("SessionTTL = %v, want 12h", cfg.SessionTTL) } if cfg.LogLevel != slog.LevelInfo { t.Errorf("LogLevel = %v, want info", cfg.LogLevel) } if !cfg.CookieSecure { t.Error("CookieSecure = false, want true by default") } if cfg.ScheduleLocation == nil || cfg.ScheduleLocation.String() != DefaultTimezone { t.Errorf("ScheduleLocation = %v, want %s", cfg.ScheduleLocation, DefaultTimezone) } } func TestScheduleTimezone(t *testing.T) { t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef") t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2") t.Setenv("CONTROL_MQTT_INSECURE", "true") t.Setenv("CONTROL_TIMEZONE", "UTC") cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if cfg.ScheduleLocation == nil || cfg.ScheduleLocation.String() != "UTC" { t.Errorf("ScheduleLocation = %v, want UTC", cfg.ScheduleLocation) } t.Setenv("CONTROL_TIMEZONE", "Not/AZone") if _, err := Load(); err == nil { t.Error("expected an error for an unknown CONTROL_TIMEZONE") } } func TestLoadRequiresSecretAndPassword(t *testing.T) { t.Setenv("CONTROL_SESSION_SECRET", "") t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2") if _, err := Load(); err == nil { t.Error("expected error when session secret is missing") } t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef") t.Setenv("CONTROL_ADMIN_PASSWORD", "") t.Setenv("CONTROL_ADMIN_PASSWORD_HASH", "") if _, err := Load(); err == nil { t.Error("expected error when no admin password is set") } } func TestMQTTDefaultsAndGuards(t *testing.T) { t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef") t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2") // Enabled by default, pointed at the shared Mosquitto on `.13`. t.Setenv("CONTROL_MQTT_INSECURE", "true") cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if !cfg.MQTTEnabled { t.Error("MQTTEnabled = false, want true by default") } if cfg.MQTTBroker != DefaultMQTTBroker { t.Errorf("MQTTBroker = %q, want %q", cfg.MQTTBroker, DefaultMQTTBroker) } if cfg.MQTTTLS() { t.Error("MQTTTLS = true for the default plaintext broker, want false") } // Without the escape hatch, a plaintext credential-less broker is refused // (AGENTS.md ยง10 rule 6). t.Setenv("CONTROL_MQTT_INSECURE", "false") if _, err := Load(); err == nil { t.Error("expected an error for a plaintext broker without CONTROL_MQTT_INSECURE") } // TLS plus credentials is the supported shape. t.Setenv("CONTROL_MQTT_BROKER", "tls://mosquitto.lab.audasmedia.com.au:8883") t.Setenv("CONTROL_MQTT_USERNAME", "control-service") t.Setenv("CONTROL_MQTT_PASSWORD", "secret") cfg, err = Load() if err != nil { t.Fatalf("Load with TLS: %v", err) } if !cfg.MQTTTLS() { t.Error("MQTTTLS = false for a tls:// broker, want true") } // MQTT can be switched off entirely. t.Setenv("CONTROL_MQTT_ENABLED", "false") t.Setenv("CONTROL_MQTT_BROKER", "") t.Setenv("CONTROL_MQTT_USERNAME", "") t.Setenv("CONTROL_MQTT_PASSWORD", "") cfg, err = Load() if err != nil { t.Fatalf("Load with MQTT disabled: %v", err) } if cfg.MQTTEnabled { t.Error("MQTTEnabled = true, want false when CONTROL_MQTT_ENABLED=false") } } func TestPhase9HubAndAlertDefaults(t *testing.T) { t.Setenv("CONTROL_SESSION_SECRET", "0123456789abcdef") t.Setenv("CONTROL_ADMIN_PASSWORD", "hunter2") t.Setenv("CONTROL_MQTT_INSECURE", "true") for _, k := range []string{ "CONTROL_HUB_TOKEN", "CONTROL_APPRISE_KEY", "CONTROL_APPRISE_URLS", "CONTROL_APPRISE_TOKEN", "CONTROL_ALERTS_ENABLED", "CONTROL_ALERT_OFFLINE_AFTER", "CONTROL_APPRISE_URL", } { t.Setenv(k, "") } cfg, err := Load() if err != nil { t.Fatalf("Load: %v", err) } if cfg.HubConfigured() { t.Error("HubConfigured = true with no CONTROL_HUB_TOKEN") } if cfg.AppriseURL != DefaultAppriseURL { t.Errorf("AppriseURL = %q, want %q", cfg.AppriseURL, DefaultAppriseURL) } if cfg.AppriseConfigured() { t.Error("AppriseConfigured = true with no destination") } if !cfg.AlertsEnabled { t.Error("AlertsEnabled = false, want true by default") } if cfg.AlertOfflineAfter != DefaultAlertOfflineAfter { t.Errorf("AlertOfflineAfter = %v, want %v", cfg.AlertOfflineAfter, DefaultAlertOfflineAfter) } t.Setenv("CONTROL_HUB_TOKEN", "hub-secret") t.Setenv("CONTROL_APPRISE_URLS", "mailto:sam@example.com pushover://x/y") t.Setenv("CONTROL_ALERT_OFFLINE_AFTER", "10m") t.Setenv("CONTROL_ALERTS_ENABLED", "false") cfg, err = Load() if err != nil { t.Fatalf("Load: %v", err) } if !cfg.HubConfigured() { t.Error("HubConfigured = false with a token set") } if !cfg.AppriseConfigured() { t.Error("AppriseConfigured = false with CONTROL_APPRISE_URLS set") } if len(cfg.AppriseURLs) != 2 { t.Errorf("AppriseURLs = %#v, want 2 entries", cfg.AppriseURLs) } if cfg.AlertOfflineAfter != 10*time.Minute { t.Errorf("AlertOfflineAfter = %v, want 10m", cfg.AlertOfflineAfter) } if cfg.AlertsEnabled { t.Error("AlertsEnabled = true, want false") } }