610 lines
22 KiB
Go
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, "")
|
|
}
|
|
}
|