package config import ( "os" "testing" ) func TestDefaultConfig(t *testing.T) { cfg := DefaultConfig() if cfg.Port != 8080 { t.Errorf("expected Port=8080, got %d", cfg.Port) } if cfg.Env != "development" { t.Errorf("expected Env=development, got %s", cfg.Env) } if cfg.PostgresHost != "localhost" { t.Errorf("expected PostgresHost=localhost, got %s", cfg.PostgresHost) } if cfg.IoTProvider != "mock" { t.Errorf("expected IoTProvider=mock, got %s", cfg.IoTProvider) } } func TestGetDatabaseURL(t *testing.T) { tests := []struct { name string dbURL string host string port int user string pass string db string sslMode string expected string }{ { name: "explicit DATABASE_URL takes priority", dbURL: "postgres://custom:pass@remote:9999/mydb?sslmode=require", expected: "postgres://custom:pass@remote:9999/mydb?sslmode=require", }, { name: "default connection string from individual fields", host: "localhost", port: 5432, user: "cyrene", pass: "cyrene_pass", db: "cyrene_ai", sslMode: "disable", expected: "postgres://cyrene:cyrene_pass@localhost:5432/cyrene_ai?sslmode=disable", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { cfg := DefaultConfig() if tt.dbURL != "" { cfg.DatabaseURL = tt.dbURL } if tt.host != "" { cfg.PostgresHost = tt.host } if tt.port != 0 { cfg.PostgresPort = tt.port } if tt.user != "" { cfg.PostgresUser = tt.user } if tt.pass != "" { cfg.PostgresPassword = tt.pass } if tt.db != "" { cfg.PostgresDB = tt.db } if tt.sslMode != "" { cfg.PostgresSSLMode = tt.sslMode } got := cfg.GetDatabaseURL() if got != tt.expected { t.Errorf("GetDatabaseURL() = %q, want %q", got, tt.expected) } }) } } func TestIsDevelopment(t *testing.T) { cfg := DefaultConfig() cfg.Env = "development" if !cfg.IsDevelopment() { t.Error("expected IsDevelopment() to be true") } if cfg.IsProduction() { t.Error("expected IsProduction() to be false in dev mode") } cfg.Env = "production" if cfg.IsDevelopment() { t.Error("expected IsDevelopment() to be false in prod mode") } if !cfg.IsProduction() { t.Error("expected IsProduction() to be true") } } func TestValidateMissingLLMAPIKey(t *testing.T) { cfg := DefaultConfig() cfg.LLMAPIKey = "" cfg.Env = "development" // In dev mode, we validate but don't call ValidateOrFatal directly errs := cfg.validate() found := false for _, e := range errs { if contains(e, "LLM_API_KEY") { found = true } } if !found { t.Errorf("expected LLM_API_KEY validation error, got: %v", errs) } } func TestValidateMissingJWTSecret(t *testing.T) { cfg := DefaultConfig() cfg.JWTSecret = "" errs := cfg.validate() found := false for _, e := range errs { if contains(e, "JWT_SECRET") { found = true } } if !found { t.Errorf("expected JWT_SECRET validation error, got: %v", errs) } } func TestValidateJWTSecretTooShort(t *testing.T) { cfg := DefaultConfig() cfg.JWTSecret = "short" errs := cfg.validate() found := false for _, e := range errs { if contains(e, "JWT_SECRET must be at least 32 characters") { found = true } } if !found { t.Errorf("expected JWT_SECRET length validation error, got: %v", errs) } } func TestValidateMissingAdminPassword(t *testing.T) { cfg := DefaultConfig() cfg.AdminPassword = "" errs := cfg.validate() found := false for _, e := range errs { if contains(e, "ADMIN_PASSWORD") { found = true } } if !found { t.Errorf("expected ADMIN_PASSWORD validation error, got: %v", errs) } } func TestValidateAdminPasswordTooShort(t *testing.T) { cfg := DefaultConfig() cfg.AdminPassword = "1234567" // 7 chars, min is 8 errs := cfg.validate() found := false for _, e := range errs { if contains(e, "ADMIN_PASSWORD must be at least 8 characters") { found = true } } if !found { t.Errorf("expected ADMIN_PASSWORD length validation error, got: %v", errs) } } func TestValidateInvalidIoTProvider(t *testing.T) { cfg := DefaultConfig() cfg.IoTProvider = "invalid-provider" errs := cfg.validate() found := false for _, e := range errs { if contains(e, "IOT_PROVIDER") { found = true } } if !found { t.Errorf("expected IOT_PROVIDER validation error, got: %v", errs) } } func TestLoadFromEnv(t *testing.T) { // Set some test env vars os.Setenv("PORT", "9999") os.Setenv("ENV", "test") os.Setenv("LLM_API_KEY", "sk-test-key") os.Setenv("JWT_SECRET", "a-very-long-secret-that-is-at-least-32-chars!!") os.Setenv("ADMIN_PASSWORD", "test-admin-password") defer func() { os.Unsetenv("PORT") os.Unsetenv("ENV") os.Unsetenv("LLM_API_KEY") os.Unsetenv("JWT_SECRET") os.Unsetenv("ADMIN_PASSWORD") }() cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.Port != 9999 { t.Errorf("expected Port=9999 from env, got %d", cfg.Port) } if cfg.Env != "test" { t.Errorf("expected Env=test from env, got %s", cfg.Env) } if cfg.LLMAPIKey != "sk-test-key" { t.Errorf("expected LLMAPIKey from env, got %s", cfg.LLMAPIKey) } // Validation should pass with these values errs := cfg.validate() if len(errs) > 0 { t.Errorf("validation should pass but got errors: %v", errs) } } func TestGetEnvBool(t *testing.T) { tests := []struct { input string expected bool }{ {"1", true}, {"true", true}, {"TRUE", true}, {"yes", true}, {"on", true}, {"0", false}, {"false", false}, {"FALSE", false}, {"no", false}, {"off", false}, {"", true}, // empty should use default=true } for _, tt := range tests { os.Setenv("TEST_BOOL_VAR", tt.input) got := getEnvBool("TEST_BOOL_VAR", true) if got != tt.expected { t.Errorf("getEnvBool(%q, true) = %v, want %v", tt.input, got, tt.expected) } } os.Unsetenv("TEST_BOOL_VAR") } func TestGetEnvSlice(t *testing.T) { tests := []struct { input string expected []string }{ {"", []string{"default"}}, {"a,b,c", []string{"a", "b", "c"}}, {" a , b , c ", []string{"a", "b", "c"}}, // trim spaces {"single", []string{"single"}}, } for _, tt := range tests { os.Setenv("TEST_SLICE_VAR", tt.input) got := getEnvSlice("TEST_SLICE_VAR", []string{"default"}) if len(got) != len(tt.expected) { t.Errorf("getEnvSlice(%q) length = %d, want %d", tt.input, len(got), len(tt.expected)) continue } for i := range got { if got[i] != tt.expected[i] { t.Errorf("getEnvSlice(%q)[%d] = %q, want %q", tt.input, i, got[i], tt.expected[i]) } } } os.Unsetenv("TEST_SLICE_VAR") } func TestAllValidIoTProviders(t *testing.T) { validProviders := []string{"mock", "homeassistant", "mqtt", "none"} for _, provider := range validProviders { cfg := DefaultConfig() cfg.IoTProvider = provider errs := cfg.validate() for _, e := range errs { if contains(e, "IOT_PROVIDER") { t.Errorf("IoTProvider=%q should be valid but got error: %s", provider, e) } } } } func contains(s, substr string) bool { return len(s) >= len(substr) && searchSubstring(s, substr) } func searchSubstring(s, substr string) bool { for i := 0; i <= len(s)-len(substr); i++ { if s[i:i+len(substr)] == substr { return true } } return false }