Files
gochat/backend/internal/config/reloader_test.go
T
Rogee 92f0d51375 refactor: 统一 DB/Redis 配置为 DSN 模式 + 移除 Helm/K8s 部署
- 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, 滚动升级)
2026-07-29 20:58:10 +08:00

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{},
}
}