package config import ( "strings" "testing" "time" "github.com/stretchr/testify/require" ) func validEnv(t *testing.T) { t.Helper() t.Setenv("GAMEMASTER_INTERNAL_HTTP_ADDR", ":8097") t.Setenv("GAMEMASTER_POSTGRES_PRIMARY_DSN", "postgres://gm:secret@localhost:5432/galaxy?search_path=gamemaster&sslmode=disable") t.Setenv("GAMEMASTER_REDIS_MASTER_ADDR", "localhost:6379") t.Setenv("GAMEMASTER_REDIS_PASSWORD", "secret") t.Setenv("GAMEMASTER_LOBBY_INTERNAL_BASE_URL", "http://lobby:8095") t.Setenv("GAMEMASTER_RTM_INTERNAL_BASE_URL", "http://rtmanager:8096") } func TestLoadFromEnvAcceptsDefaults(t *testing.T) { validEnv(t) cfg, err := LoadFromEnv() require.NoError(t, err) require.Equal(t, ":8097", cfg.InternalHTTP.Addr) require.Equal(t, 30*time.Second, cfg.ShutdownTimeout) require.Equal(t, "info", cfg.Logging.Level) require.Equal(t, "gm:lobby_events", cfg.Streams.LobbyEvents) require.Equal(t, "runtime:health_events", cfg.Streams.HealthEvents) require.Equal(t, "notification:intents", cfg.Streams.NotificationIntents) require.Equal(t, 5*time.Second, cfg.Streams.BlockTimeout) require.Equal(t, 30*time.Second, cfg.EngineClient.CallTimeout) require.Equal(t, 5*time.Second, cfg.EngineClient.ProbeTimeout) require.Equal(t, "http://lobby:8095", cfg.Lobby.BaseURL) require.Equal(t, 2*time.Second, cfg.Lobby.Timeout) require.Equal(t, "http://rtmanager:8096", cfg.RTM.BaseURL) require.Equal(t, 5*time.Second, cfg.RTM.Timeout) require.Equal(t, time.Second, cfg.Scheduler.TickInterval) require.Equal(t, 60*time.Second, cfg.Scheduler.TurnGenerationTimeout) require.Equal(t, 30*time.Second, cfg.MembershipCache.TTL) require.Equal(t, 4096, cfg.MembershipCache.MaxGames) require.Equal(t, "galaxy-gamemaster", cfg.Telemetry.ServiceName) } func TestLoadFromEnvHonoursOverrides(t *testing.T) { validEnv(t) t.Setenv("GAMEMASTER_INTERNAL_HTTP_ADDR", ":9097") t.Setenv("GAMEMASTER_REDIS_LOBBY_EVENTS_STREAM", "custom:lobby_events") t.Setenv("GAMEMASTER_ENGINE_CALL_TIMEOUT", "45s") t.Setenv("GAMEMASTER_SCHEDULER_TICK_INTERVAL", "500ms") t.Setenv("GAMEMASTER_MEMBERSHIP_CACHE_TTL", "60s") t.Setenv("GAMEMASTER_MEMBERSHIP_CACHE_MAX_GAMES", "1024") cfg, err := LoadFromEnv() require.NoError(t, err) require.Equal(t, ":9097", cfg.InternalHTTP.Addr) require.Equal(t, "custom:lobby_events", cfg.Streams.LobbyEvents) require.Equal(t, 45*time.Second, cfg.EngineClient.CallTimeout) require.Equal(t, 500*time.Millisecond, cfg.Scheduler.TickInterval) require.Equal(t, 60*time.Second, cfg.MembershipCache.TTL) require.Equal(t, 1024, cfg.MembershipCache.MaxGames) } func TestLoadFromEnvRequiresInternalHTTPAddr(t *testing.T) { t.Setenv("GAMEMASTER_POSTGRES_PRIMARY_DSN", "postgres://gm:secret@localhost:5432/galaxy") t.Setenv("GAMEMASTER_REDIS_MASTER_ADDR", "localhost:6379") t.Setenv("GAMEMASTER_REDIS_PASSWORD", "secret") t.Setenv("GAMEMASTER_LOBBY_INTERNAL_BASE_URL", "http://lobby:8095") t.Setenv("GAMEMASTER_RTM_INTERNAL_BASE_URL", "http://rtmanager:8096") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "GAMEMASTER_INTERNAL_HTTP_ADDR") } func TestLoadFromEnvRequiresLobbyBaseURL(t *testing.T) { t.Setenv("GAMEMASTER_INTERNAL_HTTP_ADDR", ":8097") t.Setenv("GAMEMASTER_POSTGRES_PRIMARY_DSN", "postgres://gm:secret@localhost:5432/galaxy") t.Setenv("GAMEMASTER_REDIS_MASTER_ADDR", "localhost:6379") t.Setenv("GAMEMASTER_REDIS_PASSWORD", "secret") t.Setenv("GAMEMASTER_RTM_INTERNAL_BASE_URL", "http://rtmanager:8096") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "GAMEMASTER_LOBBY_INTERNAL_BASE_URL") } func TestLoadFromEnvRequiresRTMBaseURL(t *testing.T) { t.Setenv("GAMEMASTER_INTERNAL_HTTP_ADDR", ":8097") t.Setenv("GAMEMASTER_POSTGRES_PRIMARY_DSN", "postgres://gm:secret@localhost:5432/galaxy") t.Setenv("GAMEMASTER_REDIS_MASTER_ADDR", "localhost:6379") t.Setenv("GAMEMASTER_REDIS_PASSWORD", "secret") t.Setenv("GAMEMASTER_LOBBY_INTERNAL_BASE_URL", "http://lobby:8095") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "GAMEMASTER_RTM_INTERNAL_BASE_URL") } func TestLoadFromEnvRejectsBadLogLevel(t *testing.T) { validEnv(t) t.Setenv("GAMEMASTER_LOG_LEVEL", "verbose") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "GAMEMASTER_LOG_LEVEL") } func TestLoadFromEnvRejectsBadDuration(t *testing.T) { validEnv(t) t.Setenv("GAMEMASTER_ENGINE_CALL_TIMEOUT", "thirty seconds") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "GAMEMASTER_ENGINE_CALL_TIMEOUT") } func TestInternalHTTPValidateRejectsBadAddr(t *testing.T) { cfg := DefaultConfig().InternalHTTP cfg.Addr = "not-an-addr" err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "host:port") } func TestStreamsValidateRequiresAllNames(t *testing.T) { cfg := DefaultConfig().Streams cfg.LobbyEvents = " " err := cfg.Validate() require.Error(t, err) require.True(t, strings.Contains(err.Error(), "lobby events")) } func TestLobbyClientValidateRejectsBadURL(t *testing.T) { cfg := LobbyClientConfig{BaseURL: "ftp://lobby", Timeout: time.Second} err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "http(s)") } func TestRTMClientValidateRejectsEmptyURL(t *testing.T) { cfg := RTMClientConfig{BaseURL: " ", Timeout: time.Second} err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "rtm internal base url") } func TestSchedulerValidateRejectsZeroInterval(t *testing.T) { cfg := SchedulerConfig{TickInterval: 0, TurnGenerationTimeout: time.Second} err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "scheduler tick interval") } func TestMembershipCacheValidateRejectsZero(t *testing.T) { cfg := MembershipCacheConfig{TTL: 0, MaxGames: 1} err := cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "ttl") cfg = MembershipCacheConfig{TTL: time.Second, MaxGames: 0} err = cfg.Validate() require.Error(t, err) require.Contains(t, err.Error(), "max games") }