package config import ( "strings" "testing" "time" "github.com/stretchr/testify/require" ) const ( redisMasterAddrEnvVar = "USERSERVICE_REDIS_MASTER_ADDR" redisReplicaAddrsEnvVar = "USERSERVICE_REDIS_REPLICA_ADDRS" redisPasswordEnvVar = "USERSERVICE_REDIS_PASSWORD" redisDBEnvVar = "USERSERVICE_REDIS_DB" redisOperationTimeoutEnvVar = "USERSERVICE_REDIS_OPERATION_TIMEOUT" redisLegacyAddrEnvVar = "USERSERVICE_REDIS_ADDR" redisLegacyUsernameEnvVar = "USERSERVICE_REDIS_USERNAME" redisLegacyTLSEnabledEnvVar = "USERSERVICE_REDIS_TLS_ENABLED" redisLegacyKeyspacePrefixEnv = "USERSERVICE_REDIS_KEYSPACE_PREFIX" postgresPrimaryDSNEnvVar = "USERSERVICE_POSTGRES_PRIMARY_DSN" postgresReplicaDSNsEnvVar = "USERSERVICE_POSTGRES_REPLICA_DSNS" postgresOperationTimeoutEnvVar = "USERSERVICE_POSTGRES_OPERATION_TIMEOUT" postgresMaxOpenConnsEnvVar = "USERSERVICE_POSTGRES_MAX_OPEN_CONNS" postgresMaxIdleConnsEnvVar = "USERSERVICE_POSTGRES_MAX_IDLE_CONNS" postgresConnMaxLifetimeEnvVar = "USERSERVICE_POSTGRES_CONN_MAX_LIFETIME" defaultPostgresDSN = "postgres://userservice:secret@127.0.0.1:5432/galaxy?search_path=user&sslmode=disable" ) func TestLoadFromEnvUsesDefaults(t *testing.T) { t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6379") t.Setenv(redisPasswordEnvVar, "secret") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) cfg, err := LoadFromEnv() require.NoError(t, err) defaults := DefaultConfig() require.Equal(t, defaults.ShutdownTimeout, cfg.ShutdownTimeout) require.Equal(t, defaults.Logging.Level, cfg.Logging.Level) require.Equal(t, defaults.InternalHTTP, cfg.InternalHTTP) require.Equal(t, defaults.AdminHTTP, cfg.AdminHTTP) require.Equal(t, "127.0.0.1:6379", cfg.Redis.Conn.MasterAddr) require.Equal(t, "secret", cfg.Redis.Conn.Password) require.Equal(t, defaults.Redis.Conn.DB, cfg.Redis.Conn.DB) require.Equal(t, defaults.Redis.DomainEventsStream, cfg.Redis.DomainEventsStream) require.Equal(t, defaults.Redis.DomainEventsStreamMaxLen, cfg.Redis.DomainEventsStreamMaxLen) require.Equal(t, defaults.Redis.LifecycleEventsStream, cfg.Redis.LifecycleEventsStream) require.Equal(t, defaults.Redis.LifecycleEventsStreamMaxLen, cfg.Redis.LifecycleEventsStreamMaxLen) require.Equal(t, defaultPostgresDSN, cfg.Postgres.Conn.PrimaryDSN) require.Equal(t, defaults.Postgres.Conn.OperationTimeout, cfg.Postgres.Conn.OperationTimeout) require.Equal(t, defaults.Postgres.Conn.MaxOpenConns, cfg.Postgres.Conn.MaxOpenConns) require.Equal(t, defaults.Postgres.Conn.MaxIdleConns, cfg.Postgres.Conn.MaxIdleConns) require.Equal(t, defaults.Postgres.Conn.ConnMaxLifetime, cfg.Postgres.Conn.ConnMaxLifetime) require.Equal(t, defaults.Telemetry, cfg.Telemetry) } func TestLoadFromEnvAppliesOverrides(t *testing.T) { t.Setenv(shutdownTimeoutEnvVar, "9s") t.Setenv(logLevelEnvVar, "debug") t.Setenv(internalHTTPAddrEnvVar, "127.0.0.1:18091") t.Setenv(internalHTTPReadHeaderTimeoutEnvVar, "3s") t.Setenv(internalHTTPRequestTimeoutEnvVar, "750ms") t.Setenv(adminHTTPAddrEnvVar, "127.0.0.1:19091") t.Setenv(adminHTTPIdleTimeoutEnvVar, "90s") t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6380") t.Setenv(redisReplicaAddrsEnvVar, "127.0.0.1:6381,127.0.0.1:6382") t.Setenv(redisPasswordEnvVar, "redis-secret") t.Setenv(redisDBEnvVar, "3") t.Setenv(redisOperationTimeoutEnvVar, "900ms") t.Setenv(redisDomainEventsStreamEnvVar, "user:test_events") t.Setenv(redisDomainEventsStreamMaxLenEnvVar, "2048") t.Setenv(redisLifecycleEventsStreamEnvVar, "user:test_lifecycle") t.Setenv(redisLifecycleEventsStreamMaxLenEnvVar, "512") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) t.Setenv(postgresReplicaDSNsEnvVar, "postgres://userservice:secret@replica-a/galaxy?sslmode=disable,postgres://userservice:secret@replica-b/galaxy?sslmode=disable") t.Setenv(postgresOperationTimeoutEnvVar, "2s") t.Setenv(postgresMaxOpenConnsEnvVar, "40") t.Setenv(postgresMaxIdleConnsEnvVar, "8") t.Setenv(postgresConnMaxLifetimeEnvVar, "45m") t.Setenv(otelServiceNameEnvVar, "galaxy-user-stage12") t.Setenv(otelTracesExporterEnvVar, "otlp") t.Setenv(otelMetricsExporterEnvVar, "otlp") t.Setenv(otelExporterOTLPTracesProtocolEnvVar, "grpc") t.Setenv(otelExporterOTLPMetricsProtocolEnvVar, "http/protobuf") t.Setenv(otelStdoutTracesEnabledEnvVar, "true") t.Setenv(otelStdoutMetricsEnabledEnvVar, "true") cfg, err := LoadFromEnv() require.NoError(t, err) require.Equal(t, 9*time.Second, cfg.ShutdownTimeout) require.Equal(t, "debug", cfg.Logging.Level) require.Equal(t, "127.0.0.1:18091", cfg.InternalHTTP.Addr) require.Equal(t, 3*time.Second, cfg.InternalHTTP.ReadHeaderTimeout) require.Equal(t, 750*time.Millisecond, cfg.InternalHTTP.RequestTimeout) require.Equal(t, "127.0.0.1:19091", cfg.AdminHTTP.Addr) require.Equal(t, 90*time.Second, cfg.AdminHTTP.IdleTimeout) require.Equal(t, "127.0.0.1:6380", cfg.Redis.Conn.MasterAddr) require.Equal(t, []string{"127.0.0.1:6381", "127.0.0.1:6382"}, cfg.Redis.Conn.ReplicaAddrs) require.Equal(t, "redis-secret", cfg.Redis.Conn.Password) require.Equal(t, 3, cfg.Redis.Conn.DB) require.Equal(t, 900*time.Millisecond, cfg.Redis.Conn.OperationTimeout) require.Equal(t, "user:test_events", cfg.Redis.DomainEventsStream) require.Equal(t, int64(2048), cfg.Redis.DomainEventsStreamMaxLen) require.Equal(t, "user:test_lifecycle", cfg.Redis.LifecycleEventsStream) require.Equal(t, int64(512), cfg.Redis.LifecycleEventsStreamMaxLen) require.Equal(t, defaultPostgresDSN, cfg.Postgres.Conn.PrimaryDSN) require.Equal(t, []string{ "postgres://userservice:secret@replica-a/galaxy?sslmode=disable", "postgres://userservice:secret@replica-b/galaxy?sslmode=disable", }, cfg.Postgres.Conn.ReplicaDSNs) require.Equal(t, 2*time.Second, cfg.Postgres.Conn.OperationTimeout) require.Equal(t, 40, cfg.Postgres.Conn.MaxOpenConns) require.Equal(t, 8, cfg.Postgres.Conn.MaxIdleConns) require.Equal(t, 45*time.Minute, cfg.Postgres.Conn.ConnMaxLifetime) require.Equal(t, "galaxy-user-stage12", cfg.Telemetry.ServiceName) require.Equal(t, "otlp", cfg.Telemetry.TracesExporter) require.Equal(t, "otlp", cfg.Telemetry.MetricsExporter) require.Equal(t, "grpc", cfg.Telemetry.TracesProtocol) require.Equal(t, "http/protobuf", cfg.Telemetry.MetricsProtocol) require.True(t, cfg.Telemetry.StdoutTracesEnabled) require.True(t, cfg.Telemetry.StdoutMetricsEnabled) } // TestLoadFromEnvRejectsLegacyRedisVars verifies the architectural rule from // PG_PLAN.md §3 / ARCHITECTURE.md §Persistence Backends: legacy // USERSERVICE_REDIS_TLS_ENABLED and USERSERVICE_REDIS_USERNAME variables must // produce a startup error from `pkg/redisconn` so operators see the breaking // rename immediately. func TestLoadFromEnvRejectsLegacyRedisVars(t *testing.T) { cases := []struct { name string envName string }{ {name: "tls_enabled deprecated", envName: redisLegacyTLSEnabledEnvVar}, {name: "username deprecated", envName: redisLegacyUsernameEnvVar}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6379") t.Setenv(redisPasswordEnvVar, "secret") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) t.Setenv(tc.envName, "true") _, err := LoadFromEnv() require.Error(t, err) require.True(t, strings.Contains(err.Error(), "no longer supported")) }) } } // TestLoadFromEnvRequiresMandatoryFields covers the architectural rule that // Redis password, master address and Postgres primary DSN are mandatory; // missing any one returns a startup error. func TestLoadFromEnvRequiresMandatoryFields(t *testing.T) { t.Run("missing redis password", func(t *testing.T) { t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6379") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) _, err := LoadFromEnv() require.Error(t, err) }) t.Run("missing redis master addr", func(t *testing.T) { t.Setenv(redisPasswordEnvVar, "secret") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) _, err := LoadFromEnv() require.Error(t, err) }) t.Run("missing postgres dsn", func(t *testing.T) { t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6379") t.Setenv(redisPasswordEnvVar, "secret") _, err := LoadFromEnv() require.Error(t, err) }) } func TestLoadFromEnvRejectsInvalidValues(t *testing.T) { cases := []struct { name string envName string envVal string }{ {name: "invalid duration", envName: shutdownTimeoutEnvVar, envVal: "later"}, {name: "invalid log level", envName: logLevelEnvVar, envVal: "verbose"}, {name: "invalid redis db", envName: redisDBEnvVar, envVal: "db-three"}, {name: "invalid stream max len", envName: redisDomainEventsStreamMaxLenEnvVar, envVal: "many"}, {name: "invalid traces exporter", envName: otelTracesExporterEnvVar, envVal: "zipkin"}, {name: "invalid metrics protocol", envName: otelExporterOTLPMetricsProtocolEnvVar, envVal: "udp"}, {name: "invalid postgres operation timeout", envName: postgresOperationTimeoutEnvVar, envVal: "soon"}, {name: "invalid postgres max open conns", envName: postgresMaxOpenConnsEnvVar, envVal: "none"}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Setenv(redisMasterAddrEnvVar, "127.0.0.1:6379") t.Setenv(redisPasswordEnvVar, "secret") t.Setenv(postgresPrimaryDSNEnvVar, defaultPostgresDSN) t.Setenv(tc.envName, tc.envVal) _, err := LoadFromEnv() require.Error(t, err) }) } } // Suppress unused-warning for legacy keyspace prefix env reference: keep the // constant in test scope for documentation, though no current code uses it. var _ = redisLegacyAddrEnvVar var _ = redisLegacyKeyspacePrefixEnv