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{ DSN: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable", }, Redis: RedisConfig{ DSN: "redis://localhost:6379", }, JWT: JWTConfig{ Secret: "test-secret-key-min-32-chars!!", ExpiryHours: 24, RefreshExpiryHours: 168, }, Log: LogConfig{Level: "info", Format: "json"}, Worker: WorkerConfig{Concurrency: 4, BlockTimeoutS: 5, SweepIntervalS: 30}, OAuth: OAuthConfig{}, 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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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_MissingDBDSN(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{}, Redis: RedisConfig{DSN: "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 DSN is required") } func TestValidate_InvalidDBDSN(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{DSN: "mysql://user@localhost/db"}, Redis: RedisConfig{DSN: "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 database DSN") } func TestValidate_MissingRedisDSN(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "redis DSN is required") } func TestValidate_InvalidRedisDSN(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "debug"}, Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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 DSN") } func TestValidate_JWTSecretInProduction(t *testing.T) { cfg := &Config{ Server: ServerConfig{Host: "localhost", Port: 8080, Mode: "release"}, Database: DatabaseConfig{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30}, 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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30}, 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{DSN: "postgres://user@localhost:5432/db?sslmode=disable"}, Redis: RedisConfig{DSN: "redis://localhost:6379"}, JWT: JWTConfig{Secret: "test-secret-key-min-32-chars!!"}, Log: LogConfig{Level: "info"}, Worker: WorkerConfig{Concurrency: 1, BlockTimeoutS: 5, SweepIntervalS: 30}, Search: SearchConfig{Engine: "db"}, } err := Validate(cfg) assert.Error(t, err) assert.Contains(t, err.Error(), "release mode requires meilisearch") } func TestDatabaseConfig_MigrateDSN(t *testing.T) { cfg := DatabaseConfig{ DSN: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable", } dsn := cfg.MigrateDSN() assert.Equal(t, "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable", dsn) } 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) }