Files
fire-safety-ymd/internal/config/config_test.go
T
2026-09-06 00:25:25 +08:00

610 lines
22 KiB
Go

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