package config import ( "testing" "time" "github.com/stretchr/testify/require" ) const ( testRedisMasterAddr = "MAIL_REDIS_MASTER_ADDR" testRedisPassword = "MAIL_REDIS_PASSWORD" testRedisDB = "MAIL_REDIS_DB" testRedisOpTimeout = "MAIL_REDIS_OPERATION_TIMEOUT" testRedisLegacyTLS = "MAIL_REDIS_TLS_ENABLED" testRedisLegacyUser = "MAIL_REDIS_USERNAME" testPostgresDSN = "MAIL_POSTGRES_PRIMARY_DSN" testPostgresOpT = "MAIL_POSTGRES_OPERATION_TIMEOUT" demoPostgresDSN = "postgres://mailservice:mailservice@localhost:5432/galaxy?search_path=mail&sslmode=disable" ) func setMinimalConn(t *testing.T) { t.Helper() t.Setenv(testRedisMasterAddr, "127.0.0.1:6379") t.Setenv(testRedisPassword, "secret") t.Setenv(testPostgresDSN, demoPostgresDSN) } func TestLoadFromEnvUsesDefaults(t *testing.T) { setMinimalConn(t) cfg, err := LoadFromEnv() require.NoError(t, err) defaults := DefaultConfig() require.Equal(t, defaults.ShutdownTimeout, cfg.ShutdownTimeout) require.Equal(t, defaults.Logging, cfg.Logging) require.Equal(t, defaults.InternalHTTP, cfg.InternalHTTP) 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.Conn.OperationTimeout, cfg.Redis.Conn.OperationTimeout) require.Equal(t, defaults.Redis.CommandStream, cfg.Redis.CommandStream) require.Equal(t, demoPostgresDSN, cfg.Postgres.Conn.PrimaryDSN) require.Equal(t, defaults.SMTP, cfg.SMTP) require.Equal(t, defaults.Templates, cfg.Templates) require.Equal(t, defaults.AttemptWorkerConcurrency, cfg.AttemptWorkerConcurrency) require.Equal(t, defaults.StreamBlockTimeout, cfg.StreamBlockTimeout) require.Equal(t, defaults.OperatorRequestTimeout, cfg.OperatorRequestTimeout) require.Equal(t, defaults.IdempotencyTTL, cfg.IdempotencyTTL) require.Equal(t, defaults.Retention, cfg.Retention) require.Equal(t, defaults.Telemetry, cfg.Telemetry) } func TestLoadFromEnvAppliesOverrides(t *testing.T) { setMinimalConn(t) t.Setenv(shutdownTimeoutEnvVar, "9s") t.Setenv(logLevelEnvVar, "debug") t.Setenv(internalHTTPAddrEnvVar, "127.0.0.1:18080") t.Setenv(internalHTTPReadHeaderTimeoutEnvVar, "3s") t.Setenv(internalHTTPReadTimeoutEnvVar, "11s") t.Setenv(internalHTTPIdleTimeoutEnvVar, "61s") t.Setenv(testRedisDB, "3") t.Setenv(testRedisOpTimeout, "750ms") t.Setenv(redisCommandStreamEnvVar, "mail:test_commands") t.Setenv(testPostgresOpT, "1500ms") t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") t.Setenv(smtpUsernameEnvVar, "mailer") t.Setenv(smtpPasswordEnvVar, "smtp-secret") t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") t.Setenv(smtpFromNameEnvVar, "Galaxy Mail") t.Setenv(smtpTimeoutEnvVar, "19s") t.Setenv(smtpInsecureSkipVerifyEnvVar, "true") t.Setenv(templateDirEnvVar, "/tmp/templates") t.Setenv(attemptWorkerConcurrencyEnvVar, "8") t.Setenv(streamBlockTimeoutEnvVar, "5s") t.Setenv(operatorRequestTimeoutEnvVar, "6s") t.Setenv(idempotencyTTLEnvVar, "48h") t.Setenv(deliveryRetentionEnvVar, "96h") t.Setenv(malformedCommandRetentionEnvVar, "240h") t.Setenv(cleanupIntervalEnvVar, "30m") t.Setenv(otelServiceNameEnvVar, "custom-mail") t.Setenv(otelTracesExporterEnvVar, "otlp") t.Setenv(otelMetricsExporterEnvVar, "otlp") t.Setenv(otelExporterOTLPProtocolEnvVar, "grpc") 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, InternalHTTPConfig{ Addr: "127.0.0.1:18080", ReadHeaderTimeout: 3 * time.Second, ReadTimeout: 11 * time.Second, IdleTimeout: 61 * time.Second, }, cfg.InternalHTTP) require.Equal(t, "127.0.0.1:6379", cfg.Redis.Conn.MasterAddr) require.Equal(t, "secret", cfg.Redis.Conn.Password) require.Equal(t, 3, cfg.Redis.Conn.DB) require.Equal(t, 750*time.Millisecond, cfg.Redis.Conn.OperationTimeout) require.Equal(t, "mail:test_commands", cfg.Redis.CommandStream) require.Equal(t, demoPostgresDSN, cfg.Postgres.Conn.PrimaryDSN) require.Equal(t, 1500*time.Millisecond, cfg.Postgres.Conn.OperationTimeout) require.Equal(t, SMTPConfig{ Mode: SMTPModeSMTP, Addr: "127.0.0.1:2525", Username: "mailer", Password: "smtp-secret", FromEmail: "noreply@example.com", FromName: "Galaxy Mail", Timeout: 19 * time.Second, InsecureSkipVerify: true, }, cfg.SMTP) require.Equal(t, TemplateConfig{Dir: "/tmp/templates"}, cfg.Templates) require.Equal(t, 8, cfg.AttemptWorkerConcurrency) require.Equal(t, 5*time.Second, cfg.StreamBlockTimeout) require.Equal(t, 6*time.Second, cfg.OperatorRequestTimeout) require.Equal(t, 48*time.Hour, cfg.IdempotencyTTL) require.Equal(t, 96*time.Hour, cfg.Retention.DeliveryRetention) require.Equal(t, 240*time.Hour, cfg.Retention.MalformedCommandRetention) require.Equal(t, 30*time.Minute, cfg.Retention.CleanupInterval) require.Equal(t, TelemetryConfig{ ServiceName: "custom-mail", TracesExporter: "otlp", MetricsExporter: "otlp", TracesProtocol: "grpc", MetricsProtocol: "grpc", StdoutTracesEnabled: true, StdoutMetricsEnabled: true, }, cfg.Telemetry) } func TestLoadFromEnvRejectsInvalidValues(t *testing.T) { tests := []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: testRedisDB, envVal: "db-three"}, {name: "invalid redis timeout", envName: testRedisOpTimeout, envVal: "never"}, {name: "invalid smtp mode", envName: smtpModeEnvVar, envVal: "ses"}, {name: "invalid smtp timeout", envName: smtpTimeoutEnvVar, envVal: "fast"}, {name: "invalid smtp insecure skip verify", envName: smtpInsecureSkipVerifyEnvVar, envVal: "sometimes"}, {name: "invalid worker count", envName: attemptWorkerConcurrencyEnvVar, envVal: "many"}, {name: "invalid otel traces exporter", envName: otelTracesExporterEnvVar, envVal: "stdout"}, {name: "invalid otel metrics exporter", envName: otelMetricsExporterEnvVar, envVal: "stdout"}, {name: "invalid otel traces protocol", envName: otelExporterOTLPTracesProtocolEnvVar, envVal: "udp"}, {name: "invalid otel metrics protocol", envName: otelExporterOTLPMetricsProtocolEnvVar, envVal: "udp"}, {name: "invalid otel stdout traces", envName: otelStdoutTracesEnabledEnvVar, envVal: "sometimes"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setMinimalConn(t) t.Setenv(tt.envName, tt.envVal) if tt.envName == smtpTimeoutEnvVar { t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") } _, err := LoadFromEnv() require.Error(t, err) }) } } func TestLoadFromEnvRejectsMissingRedisMasterAddr(t *testing.T) { t.Setenv(testRedisPassword, "secret") t.Setenv(testPostgresDSN, demoPostgresDSN) _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "MAIL_REDIS_MASTER_ADDR") } func TestLoadFromEnvRejectsMissingPostgresDSN(t *testing.T) { t.Setenv(testRedisMasterAddr, "127.0.0.1:6379") t.Setenv(testRedisPassword, "secret") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "MAIL_POSTGRES_PRIMARY_DSN") } func TestLoadFromEnvRejectsLegacyRedisVars(t *testing.T) { tests := map[string]string{ "tls": testRedisLegacyTLS, "username": testRedisLegacyUser, } for name, envVar := range tests { t.Run(name, func(t *testing.T) { setMinimalConn(t) t.Setenv(envVar, "anything") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), envVar) }) } } func TestLoadFromEnvRejectsInvalidSMTPConfiguration(t *testing.T) { t.Run("missing addr", func(t *testing.T) { setMinimalConn(t) t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "smtp addr") }) t.Run("missing from email", func(t *testing.T) { setMinimalConn(t) t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "smtp from email") }) t.Run("username without password", func(t *testing.T) { setMinimalConn(t) t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") t.Setenv(smtpUsernameEnvVar, "mailer") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "smtp username and password") }) t.Run("password without username", func(t *testing.T) { setMinimalConn(t) t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") t.Setenv(smtpPasswordEnvVar, "secret") _, err := LoadFromEnv() require.Error(t, err) require.Contains(t, err.Error(), "smtp username and password") }) } func TestLoadFromEnvRejectsNonPositiveDurationsAndCounts(t *testing.T) { tests := []struct { name string envName string envVal string }{ {name: "shutdown timeout", envName: shutdownTimeoutEnvVar, envVal: "0s"}, {name: "read header timeout", envName: internalHTTPReadHeaderTimeoutEnvVar, envVal: "0s"}, {name: "read timeout", envName: internalHTTPReadTimeoutEnvVar, envVal: "0s"}, {name: "idle timeout", envName: internalHTTPIdleTimeoutEnvVar, envVal: "0s"}, {name: "redis operation timeout", envName: testRedisOpTimeout, envVal: "0s"}, {name: "smtp timeout", envName: smtpTimeoutEnvVar, envVal: "0s"}, {name: "attempt worker concurrency", envName: attemptWorkerConcurrencyEnvVar, envVal: "0"}, {name: "stream block timeout", envName: streamBlockTimeoutEnvVar, envVal: "0s"}, {name: "operator request timeout", envName: operatorRequestTimeoutEnvVar, envVal: "0s"}, {name: "idempotency ttl", envName: idempotencyTTLEnvVar, envVal: "0s"}, {name: "delivery retention", envName: deliveryRetentionEnvVar, envVal: "0s"}, {name: "malformed command retention", envName: malformedCommandRetentionEnvVar, envVal: "0s"}, {name: "cleanup interval", envName: cleanupIntervalEnvVar, envVal: "0s"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { setMinimalConn(t) t.Setenv(tt.envName, tt.envVal) if tt.envName == smtpTimeoutEnvVar { t.Setenv(smtpModeEnvVar, SMTPModeSMTP) t.Setenv(smtpAddrEnvVar, "127.0.0.1:2525") t.Setenv(smtpFromEmailEnvVar, "noreply@example.com") } _, err := LoadFromEnv() require.Error(t, err) }) } }