- DatabaseConfig: Host/Port/User/Password/Name/DBName/SSLMode → 单个 DSN 字段 - RedisConfig: Host/Port/Password/DB/URL → 单个 DSN 字段 - 环境变量: GOCHAT_DATABASE_* (7个) → GOCHAT_DATABASE_DSN, GOCHAT_REDIS_* (5个) → GOCHAT_REDIS_DSN - validator.go: DSN URL 解析校验 (scheme + host) - redis.go: redis.ParseURL(cfg.DSN) 直连 - 所有 docker-compose / CI / shell 脚本 / .env 同步更新 - 删除 deploy/helm/ 整个目录 (20个文件) - CI 删除 helm-validate / deploy-staging / deploy-production 三个 job - 文档同步更新 (README, 架构设计, PRD, 滚动升级)
248 lines
6.9 KiB
Go
248 lines
6.9 KiB
Go
package config
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"github.com/fsnotify/fsnotify"
|
|
"github.com/spf13/viper"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// --- ConfigReloader Tests ---
|
|
|
|
func TestNewConfigReloader(t *testing.T) {
|
|
cfg := validTestConfig()
|
|
r, err := NewConfigReloader(cfg, "test")
|
|
require.NoError(t, err)
|
|
require.NotNil(t, r)
|
|
assert.Equal(t, cfg, r.Config())
|
|
r.Stop()
|
|
}
|
|
|
|
func TestConfigReloader_ConfigThreadSafe(t *testing.T) {
|
|
cfg := validTestConfig()
|
|
r, err := NewConfigReloader(cfg, "test")
|
|
require.NoError(t, err)
|
|
|
|
// Concurrent reads should be safe
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
c := r.Config()
|
|
assert.NotNil(t, c)
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
r.Stop()
|
|
}
|
|
|
|
func TestConfigReloader_OnChangeCallback(t *testing.T) {
|
|
cfg := validTestConfig()
|
|
r, err := NewConfigReloader(cfg, "test")
|
|
require.NoError(t, err)
|
|
|
|
// Register callback — verify it's registered
|
|
callbackCount := 0
|
|
r.OnChange(func(old, new *Config) {
|
|
callbackCount++
|
|
})
|
|
assert.Len(t, r.onChange, 1)
|
|
r.Stop()
|
|
}
|
|
|
|
func TestConfigReloader_ReloadableFieldsList(t *testing.T) {
|
|
// Verify ReloadableFields contains expected entries
|
|
expectedFields := []string{
|
|
"log.level",
|
|
"log.format",
|
|
"worker.concurrency",
|
|
"worker.redis_block_timeout_s",
|
|
"worker.redis_sweep_interval_s",
|
|
"server.cors.allowed_origins",
|
|
}
|
|
assert.ElementsMatch(t, expectedFields, ReloadableFields)
|
|
}
|
|
|
|
func TestConfigReloader_ApplyReloadableFields(t *testing.T) {
|
|
oldCfg := validTestConfig()
|
|
oldCfg.Log.Level = "debug"
|
|
oldCfg.Worker.Concurrency = 4
|
|
oldCfg.Database.DSN = "postgres://user@original-host:5432/db?sslmode=disable" // immutable field
|
|
|
|
newCfg := validTestConfig()
|
|
newCfg.Log.Level = "info"
|
|
newCfg.Worker.Concurrency = 8
|
|
newCfg.Database.DSN = "postgres://user@changed-host:5432/db?sslmode=disable" // should NOT be applied
|
|
|
|
r := &ConfigReloader{cfg: oldCfg}
|
|
r.applyReloadableFields(newCfg)
|
|
|
|
// Reloadable fields should be updated
|
|
assert.Equal(t, "info", r.cfg.Log.Level)
|
|
assert.Equal(t, 8, r.cfg.Worker.Concurrency)
|
|
|
|
// Immutable fields should NOT be updated
|
|
assert.Equal(t, "postgres://user@original-host:5432/db?sslmode=disable", r.cfg.Database.DSN)
|
|
}
|
|
|
|
func TestConfigReloader_Stop(t *testing.T) {
|
|
cfg := validTestConfig()
|
|
r, err := NewConfigReloader(cfg, "test")
|
|
require.NoError(t, err)
|
|
r.Stop()
|
|
// Stop should close stopCh — calling Stop again should not panic
|
|
r.Stop()
|
|
}
|
|
|
|
func TestConfigReloader_HandleConfigChange_InvalidConfig(t *testing.T) {
|
|
cfg := validTestConfig()
|
|
r := &ConfigReloader{
|
|
v: viper.New(),
|
|
cfg: cfg,
|
|
}
|
|
|
|
// Simulate a config change event with an event name
|
|
event := fsnotify.Event{Name: "config.yaml", Op: fsnotify.Write}
|
|
r.handleConfigChange(event)
|
|
|
|
// Since viper.Unmarshal with a fresh viper returns zero-valued config,
|
|
// validation should fail and old config should be preserved
|
|
assert.Equal(t, "info", r.cfg.Log.Level) // unchanged
|
|
}
|
|
|
|
// --- LoadWithEnv Tests ---
|
|
|
|
func TestLoadWithEnv_ValidConfig(t *testing.T) {
|
|
// Create a temporary config file
|
|
tmpDir := t.TempDir()
|
|
configContent := `
|
|
server:
|
|
host: "0.0.0.0"
|
|
port: 3000
|
|
mode: "debug"
|
|
database:
|
|
dsn: "postgres://gochat:secret@localhost:5432/gochat_db?sslmode=disable"
|
|
redis:
|
|
dsn: "redis://localhost:6379"
|
|
jwt:
|
|
secret: "test-secret-key-min-32-chars!!"
|
|
expiry_hours: 72
|
|
log:
|
|
level: "debug"
|
|
format: "json"
|
|
worker:
|
|
concurrency: 4
|
|
`
|
|
err := os.WriteFile(filepath.Join(tmpDir, "config.yaml"), []byte(configContent), 0644)
|
|
require.NoError(t, err)
|
|
|
|
// Load config from temp directory
|
|
v := viper.New()
|
|
v.SetConfigName("config")
|
|
v.SetConfigType("yaml")
|
|
v.AddConfigPath(tmpDir)
|
|
require.NoError(t, v.ReadInConfig())
|
|
|
|
cfg, err := LoadWithEnv("")
|
|
// LoadWithEnv uses global viper, so this test verifies the function exists
|
|
// and doesn't panic. The actual file loading depends on test environment.
|
|
if err != nil {
|
|
t.Skip("LoadWithEnv requires config file in ./configs/ — skipping in CI")
|
|
}
|
|
assert.NotNil(t, cfg)
|
|
}
|
|
|
|
func TestLoadWithEnv_EnvironmentOverlay(t *testing.T) {
|
|
// Verify that LoadWithEnv function exists and handles env parameter
|
|
// Actual file-based test would need config files present
|
|
t.Skip("Environment overlay requires config.{env}.yaml files — skipping in CI")
|
|
}
|
|
|
|
func TestLoadWithEnv_DotEnvFile(t *testing.T) {
|
|
key, value, ok := parseDotEnvLine("GOCHAT_LOG_LEVEL=debug # log level")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "GOCHAT_LOG_LEVEL", key)
|
|
assert.Equal(t, "debug", value)
|
|
|
|
_, _, ok = parseDotEnvLine("# Comment line should be ignored")
|
|
assert.False(t, ok)
|
|
}
|
|
|
|
func TestLoadDotEnv_QuotedValues(t *testing.T) {
|
|
_, value, ok := parseDotEnvLine(`GOCHAT_JWT_SECRET="my-super-secret-key-with-quotes"`)
|
|
require.True(t, ok)
|
|
assert.Equal(t, "my-super-secret-key-with-quotes", value)
|
|
|
|
_, value, ok = parseDotEnvLine("GOCHAT_REDIS_DSN='redis://special-host:6379'")
|
|
require.True(t, ok)
|
|
assert.Equal(t, "redis://special-host:6379", value)
|
|
}
|
|
|
|
// --- Env Binding Tests ---
|
|
|
|
func TestEnvBindingsComplete(t *testing.T) {
|
|
// Verify env prefix and key replacer work correctly
|
|
v := viper.New()
|
|
v.SetEnvPrefix("GOCHAT")
|
|
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
v.AutomaticEnv()
|
|
|
|
// GOCHAT_DATABASE_DSN → viper key "database.dsn" via env prefix + replacer
|
|
// The replacer replaces . with _ in viper keys when looking up env vars.
|
|
// So viper key "database.dsn" → env key "DATABASE_DSN" → with prefix "GOCHAT_DATABASE_DSN"
|
|
lowerKey := strings.ToLower(strings.ReplaceAll("database.dsn", ".", "_"))
|
|
assert.Equal(t, "database_dsn", lowerKey)
|
|
}
|
|
|
|
// --- Defaults Tests ---
|
|
|
|
func TestSetDefaults(t *testing.T) {
|
|
v := viper.New()
|
|
setDefaults(v)
|
|
|
|
assert.Equal(t, "0.0.0.0", v.GetString("server.host"))
|
|
assert.Equal(t, 3000, v.GetInt("server.port"))
|
|
assert.Equal(t, "debug", v.GetString("server.mode"))
|
|
assert.Equal(t, 10, v.GetInt("database.max_idle_conns"))
|
|
assert.Equal(t, 100, v.GetInt("database.max_open_conns"))
|
|
assert.Equal(t, 50, v.GetInt("redis.pool_size"))
|
|
assert.Equal(t, "debug", v.GetString("log.level"))
|
|
assert.Equal(t, "json", v.GetString("log.format"))
|
|
assert.Equal(t, 4, v.GetInt("worker.concurrency"))
|
|
}
|
|
|
|
func TestApplyZeroDefaults(t *testing.T) {
|
|
cfg := Config{} // all zeros
|
|
applyZeroDefaults(&cfg)
|
|
|
|
assert.Equal(t, 4, cfg.Worker.Concurrency)
|
|
|
|
// Non-zero values should not be overwritten
|
|
cfg2 := Config{
|
|
Worker: WorkerConfig{Concurrency: 16},
|
|
}
|
|
applyZeroDefaults(&cfg2)
|
|
assert.Equal(t, 16, cfg2.Worker.Concurrency)
|
|
}
|
|
|
|
// --- Helper ---
|
|
|
|
func validTestConfig() *Config {
|
|
return &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},
|
|
OAuth: OAuthConfig{},
|
|
}
|
|
}
|