feat: game lobby service
This commit is contained in:
@@ -0,0 +1,333 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
|
||||
assert.Equal(t, 30*time.Second, cfg.ShutdownTimeout)
|
||||
assert.Equal(t, "info", cfg.Logging.Level)
|
||||
assert.Equal(t, ":8094", cfg.PublicHTTP.Addr)
|
||||
assert.Equal(t, ":8095", cfg.InternalHTTP.Addr)
|
||||
assert.Equal(t, 2*time.Second, cfg.Redis.OperationTimeout)
|
||||
assert.Equal(t, "gm:lobby_events", cfg.Redis.GMEventsStream)
|
||||
assert.Equal(t, "runtime:start_jobs", cfg.Redis.RuntimeStartJobsStream)
|
||||
assert.Equal(t, "runtime:stop_jobs", cfg.Redis.RuntimeStopJobsStream)
|
||||
assert.Equal(t, "runtime:job_results", cfg.Redis.RuntimeJobResultsStream)
|
||||
assert.Equal(t, "notification:intents", cfg.Redis.NotificationIntentsStream)
|
||||
assert.Equal(t, time.Second, cfg.UserService.Timeout)
|
||||
assert.Equal(t, 5*time.Second, cfg.GM.Timeout)
|
||||
assert.Equal(t, 30*time.Second, cfg.EnrollmentAutomation.Interval)
|
||||
assert.Equal(t, time.Hour, cfg.PendingRegistration.Interval)
|
||||
assert.Equal(t, "galaxy-lobby", cfg.Telemetry.ServiceName)
|
||||
assert.Equal(t, "none", cfg.Telemetry.TracesExporter)
|
||||
assert.Equal(t, "none", cfg.Telemetry.MetricsExporter)
|
||||
}
|
||||
|
||||
func TestLoadFromEnvAppliesRequiredFields(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
t.Setenv("LOBBY_REDIS_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv("LOBBY_USER_SERVICE_BASE_URL", "http://user.internal:8090")
|
||||
t.Setenv("LOBBY_GM_BASE_URL", "http://gm.internal:8091")
|
||||
|
||||
cfg, err := LoadFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "127.0.0.1:6379", cfg.Redis.Addr)
|
||||
assert.Equal(t, "http://user.internal:8090", cfg.UserService.BaseURL)
|
||||
assert.Equal(t, "http://gm.internal:8091", cfg.GM.BaseURL)
|
||||
}
|
||||
|
||||
func TestLoadFromEnvMissingRequiredFields(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
|
||||
_, err := LoadFromEnv()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "redis addr must not be empty")
|
||||
}
|
||||
|
||||
func TestLoadFromEnvOverrides(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
t.Setenv("LOBBY_REDIS_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv("LOBBY_USER_SERVICE_BASE_URL", "http://user.internal:8090")
|
||||
t.Setenv("LOBBY_GM_BASE_URL", "http://gm.internal:8091")
|
||||
|
||||
t.Setenv("LOBBY_SHUTDOWN_TIMEOUT", "12s")
|
||||
t.Setenv("LOBBY_LOG_LEVEL", "debug")
|
||||
t.Setenv("LOBBY_PUBLIC_HTTP_ADDR", "127.0.0.1:9001")
|
||||
t.Setenv("LOBBY_INTERNAL_HTTP_ADDR", "127.0.0.1:9002")
|
||||
t.Setenv("LOBBY_REDIS_DB", "5")
|
||||
t.Setenv("LOBBY_REDIS_TLS_ENABLED", "true")
|
||||
t.Setenv("LOBBY_GM_EVENTS_STREAM", "alt:gm_events")
|
||||
t.Setenv("LOBBY_NOTIFICATION_INTENTS_STREAM", "alt:intents")
|
||||
t.Setenv("LOBBY_ENROLLMENT_AUTOMATION_INTERVAL", "45s")
|
||||
t.Setenv("LOBBY_RACE_NAME_EXPIRATION_INTERVAL", "15m")
|
||||
t.Setenv("OTEL_SERVICE_NAME", "galaxy-lobby-test")
|
||||
|
||||
cfg, err := LoadFromEnv()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, 12*time.Second, cfg.ShutdownTimeout)
|
||||
assert.Equal(t, "debug", cfg.Logging.Level)
|
||||
assert.Equal(t, "127.0.0.1:9001", cfg.PublicHTTP.Addr)
|
||||
assert.Equal(t, "127.0.0.1:9002", cfg.InternalHTTP.Addr)
|
||||
assert.Equal(t, 5, cfg.Redis.DB)
|
||||
assert.True(t, cfg.Redis.TLSEnabled)
|
||||
assert.Equal(t, "alt:gm_events", cfg.Redis.GMEventsStream)
|
||||
assert.Equal(t, "alt:intents", cfg.Redis.NotificationIntentsStream)
|
||||
assert.Equal(t, 45*time.Second, cfg.EnrollmentAutomation.Interval)
|
||||
assert.Equal(t, 15*time.Minute, cfg.PendingRegistration.Interval)
|
||||
assert.Equal(t, "galaxy-lobby-test", cfg.Telemetry.ServiceName)
|
||||
assert.NotNil(t, cfg.Redis.TLSConfig())
|
||||
}
|
||||
|
||||
func TestLoadFromEnvInvalidDuration(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
t.Setenv("LOBBY_REDIS_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv("LOBBY_USER_SERVICE_BASE_URL", "http://user.internal:8090")
|
||||
t.Setenv("LOBBY_GM_BASE_URL", "http://gm.internal:8091")
|
||||
t.Setenv("LOBBY_SHUTDOWN_TIMEOUT", "not-a-duration")
|
||||
|
||||
_, err := LoadFromEnv()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "LOBBY_SHUTDOWN_TIMEOUT")
|
||||
}
|
||||
|
||||
func TestPublicHTTPConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*PublicHTTPConfig)
|
||||
wantErr string
|
||||
}{
|
||||
{name: "ok", mutate: func(*PublicHTTPConfig) {}},
|
||||
{name: "empty addr", mutate: func(cfg *PublicHTTPConfig) { cfg.Addr = "" }, wantErr: "addr must not be empty"},
|
||||
{name: "malformed addr", mutate: func(cfg *PublicHTTPConfig) { cfg.Addr = "not-an-addr" }, wantErr: "must use host:port"},
|
||||
{name: "zero read header", mutate: func(cfg *PublicHTTPConfig) { cfg.ReadHeaderTimeout = 0 }, wantErr: "read header timeout"},
|
||||
{name: "zero read", mutate: func(cfg *PublicHTTPConfig) { cfg.ReadTimeout = 0 }, wantErr: "read timeout"},
|
||||
{name: "zero idle", mutate: func(cfg *PublicHTTPConfig) { cfg.IdleTimeout = 0 }, wantErr: "idle timeout"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig().PublicHTTP
|
||||
tt.mutate(&cfg)
|
||||
|
||||
err := cfg.Validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalHTTPConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig().InternalHTTP
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
cfg.Addr = "bogus"
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "must use host:port")
|
||||
}
|
||||
|
||||
func TestRedisConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
base := DefaultConfig().Redis
|
||||
base.Addr = "127.0.0.1:6379"
|
||||
require.NoError(t, base.Validate())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(*RedisConfig)
|
||||
wantErr string
|
||||
}{
|
||||
{name: "empty addr", mutate: func(cfg *RedisConfig) { cfg.Addr = "" }, wantErr: "addr must not be empty"},
|
||||
{name: "bad addr", mutate: func(cfg *RedisConfig) { cfg.Addr = "weird" }, wantErr: "must use host:port"},
|
||||
{name: "negative db", mutate: func(cfg *RedisConfig) { cfg.DB = -1 }, wantErr: "must not be negative"},
|
||||
{name: "zero op timeout", mutate: func(cfg *RedisConfig) { cfg.OperationTimeout = 0 }, wantErr: "operation timeout"},
|
||||
{name: "empty gm stream", mutate: func(cfg *RedisConfig) { cfg.GMEventsStream = "" }, wantErr: "gm events stream"},
|
||||
{name: "zero gm block", mutate: func(cfg *RedisConfig) { cfg.GMEventsReadBlockTimeout = 0 }, wantErr: "gm events read block timeout"},
|
||||
{name: "empty start jobs", mutate: func(cfg *RedisConfig) { cfg.RuntimeStartJobsStream = "" }, wantErr: "runtime start jobs"},
|
||||
{name: "empty stop jobs", mutate: func(cfg *RedisConfig) { cfg.RuntimeStopJobsStream = "" }, wantErr: "runtime stop jobs"},
|
||||
{name: "empty job results", mutate: func(cfg *RedisConfig) { cfg.RuntimeJobResultsStream = "" }, wantErr: "runtime job results"},
|
||||
{name: "zero job block", mutate: func(cfg *RedisConfig) { cfg.RuntimeJobResultsReadBlockTimeout = 0 }, wantErr: "runtime job results read block"},
|
||||
{name: "empty intents", mutate: func(cfg *RedisConfig) { cfg.NotificationIntentsStream = "" }, wantErr: "notification intents"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := base
|
||||
tt.mutate(&cfg)
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserServiceConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg UserServiceConfig
|
||||
wantErr string
|
||||
}{
|
||||
{name: "ok", cfg: UserServiceConfig{BaseURL: "http://x:1", Timeout: time.Second}},
|
||||
{name: "empty base url", cfg: UserServiceConfig{Timeout: time.Second}, wantErr: "base url must not be empty"},
|
||||
{name: "ftp scheme", cfg: UserServiceConfig{BaseURL: "ftp://x", Timeout: time.Second}, wantErr: "absolute http"},
|
||||
{name: "zero timeout", cfg: UserServiceConfig{BaseURL: "http://x:1"}, wantErr: "timeout must be positive"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := tt.cfg.Validate()
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), tt.wantErr)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGMConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NoError(t, GMConfig{BaseURL: "https://gm:443", Timeout: time.Second}.Validate())
|
||||
|
||||
require.ErrorContains(t, GMConfig{Timeout: time.Second}.Validate(), "base url must not be empty")
|
||||
require.ErrorContains(t, GMConfig{BaseURL: "http://gm", Timeout: 0}.Validate(), "timeout must be positive")
|
||||
}
|
||||
|
||||
func TestEnrollmentAutomationConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NoError(t, EnrollmentAutomationConfig{Interval: time.Second}.Validate())
|
||||
require.ErrorContains(t, EnrollmentAutomationConfig{}.Validate(), "interval must be positive")
|
||||
}
|
||||
|
||||
func TestPendingRegistrationConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NoError(t, PendingRegistrationConfig{Interval: time.Hour}.Validate())
|
||||
require.ErrorContains(t, PendingRegistrationConfig{}.Validate(), "race name expiration interval must be positive")
|
||||
}
|
||||
|
||||
func TestTelemetryConfigValidate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
require.NoError(t, TelemetryConfig{TracesExporter: "none", MetricsExporter: "none"}.Validate())
|
||||
require.ErrorContains(t, TelemetryConfig{TracesExporter: "weird", MetricsExporter: "none"}.Validate(), "unsupported traces exporter")
|
||||
require.ErrorContains(t, TelemetryConfig{TracesExporter: "none", MetricsExporter: "weird"}.Validate(), "unsupported metrics exporter")
|
||||
require.ErrorContains(t, TelemetryConfig{TracesExporter: "none", MetricsExporter: "none", TracesProtocol: "ws"}.Validate(), "OTLP traces protocol")
|
||||
require.ErrorContains(t, TelemetryConfig{TracesExporter: "none", MetricsExporter: "none", MetricsProtocol: "ws"}.Validate(), "OTLP metrics protocol")
|
||||
}
|
||||
|
||||
func TestConfigValidateLogLevel(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cfg := DefaultConfig()
|
||||
cfg.Redis.Addr = "127.0.0.1:6379"
|
||||
cfg.UserService.BaseURL = "http://u:1"
|
||||
cfg.GM.BaseURL = "http://gm:1"
|
||||
require.NoError(t, cfg.Validate())
|
||||
|
||||
cfg.Logging.Level = "bogus"
|
||||
err := cfg.Validate()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "slog level")
|
||||
}
|
||||
|
||||
func TestLoadFromEnvBoolParseError(t *testing.T) {
|
||||
clearAllEnv(t)
|
||||
t.Setenv("LOBBY_REDIS_ADDR", "127.0.0.1:6379")
|
||||
t.Setenv("LOBBY_USER_SERVICE_BASE_URL", "http://u:1")
|
||||
t.Setenv("LOBBY_GM_BASE_URL", "http://gm:1")
|
||||
t.Setenv("LOBBY_REDIS_TLS_ENABLED", "not-bool")
|
||||
|
||||
_, err := LoadFromEnv()
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), "LOBBY_REDIS_TLS_ENABLED")
|
||||
}
|
||||
|
||||
// clearAllEnv unsets every environment variable the config package reads so
|
||||
// tests can configure their expected values explicitly.
|
||||
func clearAllEnv(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
envVars := []string{
|
||||
shutdownTimeoutEnvVar,
|
||||
logLevelEnvVar,
|
||||
publicHTTPAddrEnvVar,
|
||||
publicHTTPReadHeaderTimeoutEnvVar,
|
||||
publicHTTPReadTimeoutEnvVar,
|
||||
publicHTTPIdleTimeoutEnvVar,
|
||||
internalHTTPAddrEnvVar,
|
||||
internalHTTPReadHeaderTimeoutEnvVar,
|
||||
internalHTTPReadTimeoutEnvVar,
|
||||
internalHTTPIdleTimeoutEnvVar,
|
||||
redisAddrEnvVar,
|
||||
redisUsernameEnvVar,
|
||||
redisPasswordEnvVar,
|
||||
redisDBEnvVar,
|
||||
redisTLSEnabledEnvVar,
|
||||
redisOperationTimeoutEnvVar,
|
||||
gmEventsStreamEnvVar,
|
||||
gmEventsReadBlockTimeoutEnvVar,
|
||||
runtimeStartJobsStreamEnvVar,
|
||||
runtimeJobResultsStreamEnvVar,
|
||||
runtimeJobResultsReadBlockTimeoutEnv,
|
||||
notificationIntentsStreamEnvVar,
|
||||
userServiceBaseURLEnvVar,
|
||||
userServiceTimeoutEnvVar,
|
||||
gmBaseURLEnvVar,
|
||||
gmTimeoutEnvVar,
|
||||
enrollmentAutomationIntervalEnvVar,
|
||||
raceNameDirectoryBackendEnvVar,
|
||||
raceNameExpirationIntervalEnvVar,
|
||||
otelServiceNameEnvVar,
|
||||
otelTracesExporterEnvVar,
|
||||
otelMetricsExporterEnvVar,
|
||||
otelExporterOTLPProtocolEnvVar,
|
||||
otelExporterOTLPTracesProtocolEnvVar,
|
||||
otelExporterOTLPMetricsProtocolEnvVar,
|
||||
otelStdoutTracesEnabledEnvVar,
|
||||
otelStdoutMetricsEnabledEnvVar,
|
||||
}
|
||||
|
||||
for _, name := range envVars {
|
||||
// t.Setenv registers a Cleanup that restores the pre-test value.
|
||||
// Unsetenv after enrolling the cleanup leaves the variable unset for
|
||||
// the duration of the test while still restoring prior state on exit.
|
||||
t.Setenv(name, "")
|
||||
require.NoError(t, os.Unsetenv(name))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user