package config import ( "fmt" "testing" "time" "github.com/stretchr/testify/assert" ) func TestValidate_ValidConfig(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{ Host: "localhost", Port: 5432, User: "gochat", Password: "secret", Name: "gochat_db", }, Redis: RedisConfig{ URL: "redis://localhost:6379", }, JWT: JWTConfig{ Secret: "test-secret-key-min-32-chars!!", ExpiryHours: 24, RefreshExpiryHours: 168, }, Log: LogConfig{Level: "info", Format: "json"}, Captain: CaptainConfig{Enabled: false}, Worker: WorkerConfig{Concurrency: 4}, OAuth: OAuthConfig{}, RateLimit: RateLimitConfig{Enabled: true, RequestsPerMinute: 100, WindowSeconds: 60}, Search: SearchConfig{Engine: "meilisearch", Host: "http://localhost:7700", IndexPrefix: "gochat_", TimeoutSeconds: 5}, } err := Validate(cfg) assert.NoError(t, err) } func TestValidate_InvalidPort(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 0, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid server port") } func TestValidate_InvalidMode(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "invalid"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid server mode") } func TestValidate_MissingDBHost(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "database host is required") } func TestValidate_MissingDBName(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "database name is required") } func TestValidate_MissingRedisURL(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "redis url is required") } func TestValidate_InvalidRedisURL(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "not-a-valid-url://::"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid redis URL") } func TestValidate_JWTSecretInProduction(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "change-me-in-production"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "JWT secret must be changed in production") } func TestValidate_InvalidLogLevel(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "invalid"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid log level") } func TestValidate_InvalidWorkerConcurrency(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 0}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "worker concurrency") } func TestValidate_SearchMeilisearchRequiresValidHost(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1}, RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60}, Search: SearchConfig{Engine: "meilisearch", Host: "not a url", TimeoutSeconds: 5}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "invalid search.host") } func TestValidate_SearchDBFallbackAllowed(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1}, RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60}, Search: SearchConfig{Engine: "db"}, } err := Validate(cfg) assert.NoError(t, err) } func TestValidate_SearchDBFallbackRejectedInRelease(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"}, Database: DatabaseConfig{Host: "localhost", Name: "db", User: "user"}, Redis: RedisConfig{URL: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1}, RateLimit: RateLimitConfig{RequestsPerMinute: 100, WindowSeconds: 60}, Search: SearchConfig{Engine: "db"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "release mode requires meilisearch") } func TestDatabaseConfig_DSN(t *testing.T) { cfg := DatabaseConfig{ Host: "localhost", Port: 5432, User: "gochat", Password: "secret", DBName: "gochat_db", SSLMode: "disable", } dsn := cfg.DSN() assert.Contains(t, dsn, "host=localhost") assert.Contains(t, dsn, "port=5432") assert.Contains(t, dsn, "user=gochat") assert.Contains(t, dsn, "dbname=gochat_db") assert.Contains(t, dsn, "sslmode=disable") } func TestJWTConfig_ExpiryDuration(t *testing.T) { cfg := JWTConfig{ExpiryHours: 24} dur := cfg.ExpiryDuration() assert.Equal(t, 24*time.Hour, dur) } func TestServerConfig_Address(t *testing.T) { cfg := ServerConfig{Host: "0.0.0.0", Port: 3000} addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) assert.Equal(t, "0.0.0.0:3000", addr) }