package config import ( "strconv" "strings" "testing" "time" ) func TestLoadDefaults(t *testing.T) { clearEnvironment(t) cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.HTTPAddress != ":8080" { t.Fatalf("HTTPAddress = %q, want %q", cfg.HTTPAddress, ":8080") } if cfg.SuperAgent.Enabled { t.Fatal("SuperAgent.Enabled = true, want false") } if cfg.SuperAgent.ConnectTimeout != 15*time.Second { t.Fatalf("ConnectTimeout = %v, want 15s", cfg.SuperAgent.ConnectTimeout) } if cfg.SuperAgent.RecoveryMaxAttempts != 5 { t.Fatalf("RecoveryMaxAttempts = %d, want 5", cfg.SuperAgent.RecoveryMaxAttempts) } if cfg.SuperAgent.RecoveryInitialBackoff != 250*time.Millisecond { t.Fatalf("RecoveryInitialBackoff = %v, want 250ms", cfg.SuperAgent.RecoveryInitialBackoff) } if cfg.SuperAgent.MaxMessageBytes != 64*1024 { t.Fatalf("MaxMessageBytes = %d, want %d", cfg.SuperAgent.MaxMessageBytes, 64*1024) } if cfg.SuperAgent.ProbeSubjectID != "fire-safety-ymd-connectivity-probe" { t.Fatalf("ProbeSubjectID = %q", cfg.SuperAgent.ProbeSubjectID) } if cfg.SuperAgent.ProbeTimeout != 10*time.Minute { t.Fatalf("ProbeTimeout = %v, want 10m", cfg.SuperAgent.ProbeTimeout) } if cfg.Chat.Enabled { t.Fatal("Chat.Enabled = true, want false") } if cfg.Chat.PageEnabled { t.Fatal("Chat.PageEnabled = true, want false") } if cfg.Chat.AllowLegacyShortToken { t.Fatal("Chat.AllowLegacyShortToken = true, want false") } if cfg.Chat.SubjectID != "fire-safety-ymd-chat-test-subject" || cfg.Chat.MaxBodyBytes != 128*1024 || cfg.Chat.CompatAppID != "" || cfg.Chat.RunTimeout != 10*time.Minute || cfg.Chat.SessionTTL != 30*time.Minute || cfg.Chat.MaxSessions != 1000 { t.Fatalf("unexpected Chat defaults: %#v", cfg.Chat) } if cfg.MCP.Enabled || cfg.PostGIS.Enabled { t.Fatalf("MCP/PostGIS must be disabled by default: MCP=%v PostGIS=%v", cfg.MCP.Enabled, cfg.PostGIS.Enabled) } if cfg.MCP.MaxBodyBytes != 256*1024 || cfg.MCP.ToolTimeout != 5*time.Second { t.Fatalf("unexpected MCP defaults: %#v", cfg.MCP) } if cfg.MCP.ScopeMode != MCPScopeModeTownAllowlist { t.Fatalf("MCP.ScopeMode = %q, want %q", cfg.MCP.ScopeMode, MCPScopeModeTownAllowlist) } if cfg.PostGIS.ConnectTimeout != 5*time.Second || cfg.PostGIS.QueryTimeout != 3*time.Second || cfg.PostGIS.MaxConns != 4 { t.Fatalf("unexpected PostGIS defaults: %#v", cfg.PostGIS) } } func TestLoadConfiguredChat(t *testing.T) { clearEnvironment(t) t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, "superagent-test-open-api-key") t.Setenv(ChatEnabledEnv, "true") t.Setenv(ChatPageEnabledEnv, "true") t.Setenv(ChatAuthTokenEnv, "0123456789abcdef0123456789abcdef") t.Setenv(ChatSubjectIDEnv, " local-chat-test-subject ") t.Setenv(ChatCompatAppIDEnv, " fire-safety-public-app ") t.Setenv(ChatAllowedOriginsEnv, " http://localhost:5173/,https://fire.example.test,http://localhost:5173 ") t.Setenv(ChatMaxBodyBytesEnv, "8192") t.Setenv(ChatRunTimeoutEnv, "2m") t.Setenv(ChatSessionTTLEnv, "45m") t.Setenv(ChatMaxSessionsEnv, "50") cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if !cfg.Chat.Enabled || !cfg.Chat.PageEnabled || cfg.Chat.AuthToken == "" || cfg.Chat.SubjectID != "local-chat-test-subject" || cfg.Chat.CompatAppID != "fire-safety-public-app" { t.Fatalf("unexpected Chat config: %#v", cfg.Chat) } if got, want := strings.Join(cfg.Chat.AllowedOrigins, ","), "http://localhost:5173,https://fire.example.test"; got != want { t.Fatalf("AllowedOrigins = %q, want %q", got, want) } if cfg.Chat.MaxBodyBytes != 8192 || cfg.Chat.RunTimeout != 2*time.Minute || cfg.Chat.SessionTTL != 45*time.Minute || cfg.Chat.MaxSessions != 50 { t.Fatalf("unexpected Chat limits: %#v", cfg.Chat) } } func TestLoadConfiguredMCPAndPostGIS(t *testing.T) { clearEnvironment(t) t.Setenv(MCPEnabledEnv, "true") t.Setenv(MCPAuthTokenEnv, "0123456789abcdef0123456789abcdef") t.Setenv(MCPScopeModeEnv, " TOWN_ALLOWLIST ") t.Setenv(MCPAllowedTownsEnv, " 莒格庄镇, 高陵镇,莒格庄镇 ") t.Setenv(MCPMaxBodyBytesEnv, "4096") t.Setenv(MCPToolTimeoutEnv, "7s") t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, "postgresql://fire:secret@127.0.0.1:5432/fire_safety?sslmode=disable") t.Setenv(PostGISExpectedSRIDEnv, "4326") t.Setenv(PostGISConnectTimeoutEnv, "2s") t.Setenv(PostGISQueryTimeoutEnv, "4s") t.Setenv(PostGISMaxConnsEnv, "6") cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if !cfg.MCP.Enabled || cfg.MCP.AuthToken == "" || cfg.MCP.MaxBodyBytes != 4096 || cfg.MCP.ToolTimeout != 7*time.Second { t.Fatalf("unexpected MCP config: %#v", cfg.MCP) } if cfg.MCP.ScopeMode != MCPScopeModeTownAllowlist { t.Fatalf("MCP.ScopeMode = %q, want %q", cfg.MCP.ScopeMode, MCPScopeModeTownAllowlist) } if got, want := strings.Join(cfg.MCP.AllowedTowns, ","), "莒格庄镇,高陵镇"; got != want { t.Fatalf("AllowedTowns = %q, want %q", got, want) } if !cfg.PostGIS.Enabled || cfg.PostGIS.ExpectedSRID != 4326 || cfg.PostGIS.ConnectTimeout != 2*time.Second || cfg.PostGIS.QueryTimeout != 4*time.Second || cfg.PostGIS.MaxConns != 6 { t.Fatalf("unexpected PostGIS config: %#v", cfg.PostGIS) } } func TestLoadConfiguredMCPAllScopeWithoutTownEnumeration(t *testing.T) { clearEnvironment(t) t.Setenv(MCPEnabledEnv, "true") t.Setenv(MCPAuthTokenEnv, "0123456789abcdef0123456789abcdef") t.Setenv(MCPScopeModeEnv, "all") t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, "postgresql://fire:secret@127.0.0.1:5432/fire_safety?sslmode=disable") t.Setenv(PostGISExpectedSRIDEnv, "4326") cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.MCP.ScopeMode != MCPScopeModeAll { t.Fatalf("MCP.ScopeMode = %q, want %q", cfg.MCP.ScopeMode, MCPScopeModeAll) } if len(cfg.MCP.AllowedTowns) != 0 { t.Fatalf("AllowedTowns = %#v, want empty in all scope", cfg.MCP.AllowedTowns) } } func TestLoadRejectsAllScopeWithTownAllowlist(t *testing.T) { clearEnvironment(t) t.Setenv(MCPEnabledEnv, "true") t.Setenv(MCPAuthTokenEnv, "0123456789abcdef0123456789abcdef") t.Setenv(MCPScopeModeEnv, "all") t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") _, err := Load() if err == nil || !strings.Contains(err.Error(), MCPAllowedTownsEnv) { t.Fatalf("Load() error = %v, want ambiguous scope rejection", err) } } func TestLoadConfiguredValues(t *testing.T) { clearEnvironment(t) t.Setenv(HTTPAddressEnv, " 127.0.0.1:9090 ") t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, " https://superagent.example.test/root/ ") t.Setenv(SuperAgentOpenAPIKeyEnv, " test-open-api-key ") t.Setenv(SuperAgentConnectTimeoutEnv, "3s") t.Setenv(SuperAgentRecoveryMaxAttemptsEnv, "2") t.Setenv(SuperAgentRecoveryInitialBackoffEnv, "20ms") t.Setenv(SuperAgentMaxMessageBytesEnv, "4096") t.Setenv(SuperAgentProbeSubjectIDEnv, " probe-user ") t.Setenv(SuperAgentProbeTimeoutEnv, "30s") cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if cfg.HTTPAddress != "127.0.0.1:9090" || !cfg.SuperAgent.Enabled { t.Fatalf("unexpected base config: %#v", cfg) } if cfg.SuperAgent.BaseURL != "https://superagent.example.test/root" { t.Fatalf("BaseURL = %q", cfg.SuperAgent.BaseURL) } if cfg.SuperAgent.OpenAPIKey != "test-open-api-key" { t.Fatal("OpenAPIKey was not loaded") } if cfg.SuperAgent.ConnectTimeout != 3*time.Second || cfg.SuperAgent.RecoveryMaxAttempts != 2 || cfg.SuperAgent.RecoveryInitialBackoff != 20*time.Millisecond { t.Fatalf("unexpected recovery config: %#v", cfg.SuperAgent) } if cfg.SuperAgent.MaxMessageBytes != 4096 || cfg.SuperAgent.ProbeSubjectID != "probe-user" || cfg.SuperAgent.ProbeTimeout != 30*time.Second { t.Fatalf("unexpected probe config: %#v", cfg.SuperAgent) } } func TestLoadRejectsInvalidValues(t *testing.T) { tests := []struct { name string key string value string }{ {name: "boolean", key: SuperAgentEnabledEnv, value: "sometimes"}, {name: "connect timeout syntax", key: SuperAgentConnectTimeoutEnv, value: "soon"}, {name: "connect timeout non-positive", key: SuperAgentConnectTimeoutEnv, value: "0s"}, {name: "recovery attempts syntax", key: SuperAgentRecoveryMaxAttemptsEnv, value: "many"}, {name: "recovery attempts negative", key: SuperAgentRecoveryMaxAttemptsEnv, value: "-1"}, {name: "recovery attempts too large", key: SuperAgentRecoveryMaxAttemptsEnv, value: "21"}, {name: "recovery backoff syntax", key: SuperAgentRecoveryInitialBackoffEnv, value: "later"}, {name: "recovery backoff non-positive", key: SuperAgentRecoveryInitialBackoffEnv, value: "0s"}, {name: "message bytes syntax", key: SuperAgentMaxMessageBytesEnv, value: "large"}, {name: "message bytes non-positive", key: SuperAgentMaxMessageBytesEnv, value: "0"}, {name: "message bytes too large", key: SuperAgentMaxMessageBytesEnv, value: "16777217"}, {name: "probe timeout syntax", key: SuperAgentProbeTimeoutEnv, value: "forever"}, {name: "probe timeout non-positive", key: SuperAgentProbeTimeoutEnv, value: "0s"}, {name: "Chat boolean", key: ChatEnabledEnv, value: "sometimes"}, {name: "Chat page boolean", key: ChatPageEnabledEnv, value: "sometimes"}, {name: "Chat legacy short token boolean", key: ChatAllowLegacyShortTokenEnv, value: "sometimes"}, {name: "Chat body syntax", key: ChatMaxBodyBytesEnv, value: "large"}, {name: "Chat body non-positive", key: ChatMaxBodyBytesEnv, value: "0"}, {name: "Chat body too large", key: ChatMaxBodyBytesEnv, value: "1048577"}, {name: "Chat run timeout syntax", key: ChatRunTimeoutEnv, value: "forever"}, {name: "Chat run timeout too large", key: ChatRunTimeoutEnv, value: "31m"}, {name: "Chat session TTL non-positive", key: ChatSessionTTLEnv, value: "0s"}, {name: "Chat session TTL too large", key: ChatSessionTTLEnv, value: "25h"}, {name: "Chat session capacity syntax", key: ChatMaxSessionsEnv, value: "many"}, {name: "Chat session capacity too large", key: ChatMaxSessionsEnv, value: "10001"}, {name: "Chat invalid origin", key: ChatAllowedOriginsEnv, value: "https://fire.example.test/path"}, {name: "Chat wildcard origin", key: ChatAllowedOriginsEnv, value: "*"}, {name: "Chat invalid subject", key: ChatSubjectIDEnv, value: "subject\nvalue"}, {name: "Chat invalid compatibility app ID", key: ChatCompatAppIDEnv, value: "invalid/app/id"}, {name: "MCP boolean", key: MCPEnabledEnv, value: "sometimes"}, {name: "MCP scope mode", key: MCPScopeModeEnv, value: "automatic"}, {name: "MCP body syntax", key: MCPMaxBodyBytesEnv, value: "large"}, {name: "MCP body non-positive", key: MCPMaxBodyBytesEnv, value: "0"}, {name: "MCP body too large", key: MCPMaxBodyBytesEnv, value: "1048577"}, {name: "MCP timeout syntax", key: MCPToolTimeoutEnv, value: "soon"}, {name: "MCP timeout too large", key: MCPToolTimeoutEnv, value: "31s"}, {name: "PostGIS boolean", key: PostGISEnabledEnv, value: "sometimes"}, {name: "PostGIS SRID syntax", key: PostGISExpectedSRIDEnv, value: "wgs84"}, {name: "PostGIS SRID negative", key: PostGISExpectedSRIDEnv, value: "-1"}, {name: "PostGIS connect timeout", key: PostGISConnectTimeoutEnv, value: "31s"}, {name: "PostGIS query timeout", key: PostGISQueryTimeoutEnv, value: "0s"}, {name: "PostGIS max conns syntax", key: PostGISMaxConnsEnv, value: "many"}, {name: "PostGIS max conns too large", key: PostGISMaxConnsEnv, value: "21"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearEnvironment(t) t.Setenv(tt.key, tt.value) _, err := Load() if err == nil || !strings.Contains(err.Error(), tt.key) { t.Fatalf("Load() error = %v, want error mentioning %s", err, tt.key) } }) } } func TestLoadRequiresChatPageDependencies(t *testing.T) { configureEnabledChat := func(t *testing.T) { t.Helper() t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, "superagent-open-api-key") t.Setenv(ChatEnabledEnv, "true") t.Setenv(ChatAuthTokenEnv, "0123456789abcdef0123456789abcdef") } tests := []struct { name string configure func(*testing.T) wantErrKey string }{ { name: "chat disabled", configure: func(t *testing.T) { t.Setenv(ChatCompatAppIDEnv, "fire-safety-app") t.Setenv(ChatAllowedOriginsEnv, "https://fire.example.test") }, wantErrKey: ChatEnabledEnv, }, { name: "missing compatibility app ID", configure: func(t *testing.T) { configureEnabledChat(t) t.Setenv(ChatAllowedOriginsEnv, "https://fire.example.test") }, wantErrKey: ChatCompatAppIDEnv, }, { name: "missing allowed origin", configure: func(t *testing.T) { configureEnabledChat(t) t.Setenv(ChatCompatAppIDEnv, "fire-safety-app") }, wantErrKey: ChatAllowedOriginsEnv, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearEnvironment(t) t.Setenv(ChatPageEnabledEnv, "true") tt.configure(t) _, err := Load() if err == nil || !strings.Contains(err.Error(), tt.wantErrKey) { t.Fatalf("Load() error = %v, want error mentioning %s", err, tt.wantErrKey) } }) } } func TestLoadRequiresEnabledChatSettings(t *testing.T) { tests := []struct { name string configure func(*testing.T) wantErrKey string }{ { name: "missing chat token", configure: func(t *testing.T) { t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, "superagent-open-api-key") }, wantErrKey: ChatAuthTokenEnv, }, { name: "SuperAgent disabled", configure: func(t *testing.T) { t.Setenv(ChatAuthTokenEnv, "0123456789abcdef0123456789abcdef") }, wantErrKey: SuperAgentEnabledEnv, }, { name: "reused SuperAgent key", configure: func(t *testing.T) { shared := "0123456789abcdef0123456789abcdef" t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, shared) t.Setenv(ChatAuthTokenEnv, shared) }, wantErrKey: ChatAuthTokenEnv, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearEnvironment(t) t.Setenv(ChatEnabledEnv, "true") tt.configure(t) _, err := Load() if err == nil || !strings.Contains(err.Error(), tt.wantErrKey) { t.Fatalf("Load() error = %v, want error mentioning %s", err, tt.wantErrKey) } }) } } func TestLoadChatLegacyShortTokenPolicy(t *testing.T) { configureChat := func(t *testing.T, token string, allowLegacy bool) { t.Helper() t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, "different-superagent-open-api-key") t.Setenv(ChatEnabledEnv, "true") t.Setenv(ChatAuthTokenEnv, token) t.Setenv(ChatAllowLegacyShortTokenEnv, strconv.FormatBool(allowLegacy)) } t.Run("default rejects short token", func(t *testing.T) { clearEnvironment(t) configureChat(t, "delivered-key", false) if _, err := Load(); err == nil || !strings.Contains(err.Error(), ChatAuthTokenEnv) { t.Fatalf("Load() error = %v, want short token rejection", err) } }) t.Run("explicit compatibility allows short token", func(t *testing.T) { clearEnvironment(t) configureChat(t, "delivered-key", true) cfg, err := Load() if err != nil { t.Fatalf("Load() error = %v", err) } if !cfg.Chat.AllowLegacyShortToken || cfg.Chat.AuthToken != "delivered-key" { t.Fatalf("unexpected Chat config: %#v", cfg.Chat) } }) for _, tt := range []struct { name string token string }{ {name: "empty", token: ""}, {name: "space", token: "delivered key"}, {name: "control character", token: "delivered\tkey"}, {name: "Unicode", token: "delivered密钥"}, {name: "oversized", token: strings.Repeat("a", 4097)}, } { t.Run("compatibility rejects "+tt.name, func(t *testing.T) { clearEnvironment(t) configureChat(t, tt.token, true) if _, err := Load(); err == nil || !strings.Contains(err.Error(), ChatAuthTokenEnv) { t.Fatalf("Load() error = %v, want unsafe token rejection", err) } }) } } func TestLoadRequiresEnabledMCPSettings(t *testing.T) { validToken := "0123456789abcdef0123456789abcdef" validDSN := "postgresql://fire:secret@127.0.0.1:5432/fire_safety?sslmode=disable" tests := []struct { name string configure func(*testing.T) wantErrKey string }{ { name: "missing token", configure: func(t *testing.T) { t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") }, wantErrKey: MCPAuthTokenEnv, }, { name: "short token", configure: func(t *testing.T) { t.Setenv(ChatAllowLegacyShortTokenEnv, "true") t.Setenv(MCPAuthTokenEnv, "short-token") t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") }, wantErrKey: MCPAuthTokenEnv, }, { name: "missing scope", configure: func(t *testing.T) { t.Setenv(MCPAuthTokenEnv, validToken) }, wantErrKey: MCPAllowedTownsEnv, }, { name: "PostGIS disabled", configure: func(t *testing.T) { t.Setenv(MCPAuthTokenEnv, validToken) t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") }, wantErrKey: PostGISEnabledEnv, }, { name: "missing expected SRID", configure: func(t *testing.T) { t.Setenv(MCPAuthTokenEnv, validToken) t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, validDSN) }, wantErrKey: PostGISExpectedSRIDEnv, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearEnvironment(t) t.Setenv(MCPEnabledEnv, "true") tt.configure(t) _, err := Load() if err == nil || !strings.Contains(err.Error(), tt.wantErrKey) { t.Fatalf("Load() error = %v, want error mentioning %s", err, tt.wantErrKey) } }) } } func TestLoadRejectsReusedSuperAgentAndMCPToken(t *testing.T) { clearEnvironment(t) sharedToken := "0123456789abcdef0123456789abcdef" t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, sharedToken) t.Setenv(MCPEnabledEnv, "true") t.Setenv(MCPAuthTokenEnv, sharedToken) t.Setenv(MCPAllowedTownsEnv, "莒格庄镇") t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, "postgresql://fire:secret@127.0.0.1:5432/fire_safety?sslmode=disable") t.Setenv(PostGISExpectedSRIDEnv, "4326") _, err := Load() if err == nil || !strings.Contains(err.Error(), MCPAuthTokenEnv) { t.Fatalf("Load() error = %v, want token reuse rejection", err) } } func TestLoadRejectsReusedChatAndMCPToken(t *testing.T) { clearEnvironment(t) sharedToken := "0123456789abcdef0123456789abcdef" t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, "https://superagent.example.test") t.Setenv(SuperAgentOpenAPIKeyEnv, "different-superagent-open-api-key") t.Setenv(ChatEnabledEnv, "true") t.Setenv(ChatAuthTokenEnv, sharedToken) t.Setenv(MCPEnabledEnv, "true") t.Setenv(MCPAuthTokenEnv, sharedToken) t.Setenv(MCPScopeModeEnv, "all") t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, "postgresql://fire:secret@127.0.0.1:5432/fire_safety?sslmode=disable") t.Setenv(PostGISExpectedSRIDEnv, "4326") _, err := Load() if err == nil || !strings.Contains(err.Error(), ChatAuthTokenEnv) { t.Fatalf("Load() error = %v, want chat/MCP token reuse rejection", err) } } func TestLoadRejectsInvalidPostGISDSNWithoutLeakingIt(t *testing.T) { clearEnvironment(t) secret := "do-not-leak-this-password" t.Setenv(PostGISEnabledEnv, "true") t.Setenv(PostGISDSNEnv, "not-a-url-"+secret) _, err := Load() if err == nil || !strings.Contains(err.Error(), PostGISDSNEnv) { t.Fatalf("Load() error = %v, want error mentioning %s", err, PostGISDSNEnv) } if strings.Contains(err.Error(), secret) { t.Fatal("PostGIS configuration error leaked DSN content") } } func TestLoadRequiresEnabledSuperAgentSettings(t *testing.T) { tests := []struct { name string baseURL string apiKey string wantErrKey string }{ {name: "missing base URL", apiKey: "test-open-api-key", wantErrKey: SuperAgentBaseURLEnv}, {name: "missing key", baseURL: "https://superagent.example.test", wantErrKey: SuperAgentOpenAPIKeyEnv}, {name: "relative URL", baseURL: "/api", apiKey: "test-open-api-key", wantErrKey: SuperAgentBaseURLEnv}, {name: "unsupported scheme", baseURL: "ftp://superagent.example.test", apiKey: "test-open-api-key", wantErrKey: SuperAgentBaseURLEnv}, {name: "URL credentials", baseURL: "https://user:pass@superagent.example.test", apiKey: "test-open-api-key", wantErrKey: SuperAgentBaseURLEnv}, {name: "URL query", baseURL: "https://superagent.example.test?key=value", apiKey: "test-open-api-key", wantErrKey: SuperAgentBaseURLEnv}, {name: "invalid key header", baseURL: "https://superagent.example.test", apiKey: "test open api key", wantErrKey: SuperAgentOpenAPIKeyEnv}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { clearEnvironment(t) t.Setenv(SuperAgentEnabledEnv, "true") t.Setenv(SuperAgentBaseURLEnv, tt.baseURL) t.Setenv(SuperAgentOpenAPIKeyEnv, tt.apiKey) _, err := Load() if err == nil || !strings.Contains(err.Error(), tt.wantErrKey) { t.Fatalf("Load() error = %v, want error mentioning %s", err, tt.wantErrKey) } }) } } func clearEnvironment(t *testing.T) { t.Helper() for _, key := range []string{ HTTPAddressEnv, SuperAgentEnabledEnv, SuperAgentBaseURLEnv, SuperAgentOpenAPIKeyEnv, SuperAgentConnectTimeoutEnv, SuperAgentRecoveryMaxAttemptsEnv, SuperAgentRecoveryInitialBackoffEnv, SuperAgentMaxMessageBytesEnv, SuperAgentProbeSubjectIDEnv, SuperAgentProbeTimeoutEnv, ChatEnabledEnv, ChatPageEnabledEnv, ChatAuthTokenEnv, ChatAllowLegacyShortTokenEnv, ChatSubjectIDEnv, ChatAllowedOriginsEnv, ChatMaxBodyBytesEnv, ChatRunTimeoutEnv, ChatSessionTTLEnv, ChatMaxSessionsEnv, ChatCompatAppIDEnv, MCPEnabledEnv, MCPAuthTokenEnv, MCPScopeModeEnv, MCPAllowedTownsEnv, MCPMaxBodyBytesEnv, MCPToolTimeoutEnv, PostGISEnabledEnv, PostGISDSNEnv, PostGISExpectedSRIDEnv, PostGISConnectTimeoutEnv, PostGISQueryTimeoutEnv, PostGISMaxConnsEnv, } { t.Setenv(key, "") } }