423 lines
9.2 KiB
Go
423 lines
9.2 KiB
Go
package config
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/break/junhong_cmp_fiber/pkg/constants"
|
|
"github.com/spf13/viper"
|
|
"go.uber.org/zap"
|
|
"go.uber.org/zap/zaptest"
|
|
)
|
|
|
|
// TestWatch tests the config hot reload watcher
|
|
func TestWatch(t *testing.T) {
|
|
// Reset viper
|
|
viper.Reset()
|
|
|
|
// Create temp config file
|
|
tmpDir := t.TempDir()
|
|
configFile := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
// Initial config
|
|
initialContent := `
|
|
server:
|
|
address: ":3000"
|
|
read_timeout: "10s"
|
|
write_timeout: "10s"
|
|
shutdown_timeout: "30s"
|
|
prefork: false
|
|
|
|
redis:
|
|
address: "localhost"
|
|
port: 6379
|
|
password: ""
|
|
db: 0
|
|
pool_size: 10
|
|
min_idle_conns: 5
|
|
dial_timeout: "5s"
|
|
read_timeout: "3s"
|
|
write_timeout: "3s"
|
|
|
|
logging:
|
|
level: "info"
|
|
development: false
|
|
app_log:
|
|
filename: "logs/app.log"
|
|
max_size: 100
|
|
max_backups: 30
|
|
max_age: 30
|
|
compress: true
|
|
access_log:
|
|
filename: "logs/access.log"
|
|
max_size: 500
|
|
max_backups: 90
|
|
max_age: 90
|
|
compress: true
|
|
|
|
middleware:
|
|
enable_auth: true
|
|
enable_rate_limiter: false
|
|
rate_limiter:
|
|
max: 100
|
|
expiration: "1m"
|
|
storage: "memory"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(initialContent), 0644); err != nil {
|
|
t.Fatalf("failed to create config file: %v", err)
|
|
}
|
|
|
|
// Set config path
|
|
os.Setenv(constants.EnvConfigPath, configFile)
|
|
defer os.Unsetenv(constants.EnvConfigPath)
|
|
|
|
// Load initial config
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("failed to load initial config: %v", err)
|
|
}
|
|
|
|
// Verify initial values
|
|
if cfg.Logging.Level != "info" {
|
|
t.Fatalf("expected initial logging.level info, got %s", cfg.Logging.Level)
|
|
}
|
|
|
|
// Create logger for testing
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
// Start watcher in goroutine with context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go Watch(ctx, logger)
|
|
|
|
// Give watcher time to initialize
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Modify config file to trigger hot reload
|
|
updatedContent := `
|
|
server:
|
|
address: ":8080"
|
|
read_timeout: "15s"
|
|
write_timeout: "15s"
|
|
shutdown_timeout: "30s"
|
|
prefork: false
|
|
|
|
redis:
|
|
address: "localhost"
|
|
port: 6379
|
|
password: ""
|
|
db: 0
|
|
pool_size: 20
|
|
min_idle_conns: 10
|
|
dial_timeout: "5s"
|
|
read_timeout: "3s"
|
|
write_timeout: "3s"
|
|
|
|
logging:
|
|
level: "debug"
|
|
development: true
|
|
app_log:
|
|
filename: "logs/app.log"
|
|
max_size: 100
|
|
max_backups: 30
|
|
max_age: 30
|
|
compress: true
|
|
access_log:
|
|
filename: "logs/access.log"
|
|
max_size: 500
|
|
max_backups: 90
|
|
max_age: 90
|
|
compress: true
|
|
|
|
middleware:
|
|
enable_auth: false
|
|
enable_rate_limiter: true
|
|
rate_limiter:
|
|
max: 200
|
|
expiration: "2m"
|
|
storage: "redis"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(updatedContent), 0644); err != nil {
|
|
t.Fatalf("failed to update config file: %v", err)
|
|
}
|
|
|
|
// Wait for watcher to detect and process changes (spec requires detection within 5 seconds)
|
|
// We use a more aggressive timeout for testing
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Verify config was reloaded
|
|
reloadedCfg := Get()
|
|
if reloadedCfg.Logging.Level != "debug" {
|
|
t.Errorf("expected config hot reload, got logging.level %s instead of debug", reloadedCfg.Logging.Level)
|
|
}
|
|
if reloadedCfg.Server.Address != ":8080" {
|
|
t.Errorf("expected config hot reload, got server.address %s instead of :8080", reloadedCfg.Server.Address)
|
|
}
|
|
if reloadedCfg.Redis.PoolSize != 20 {
|
|
t.Errorf("expected config hot reload, got redis.pool_size %d instead of 20", reloadedCfg.Redis.PoolSize)
|
|
}
|
|
|
|
// Cancel context to stop watcher
|
|
cancel()
|
|
|
|
// Give watcher time to shut down gracefully
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// TestWatch_InvalidConfigRejected tests that invalid config changes are rejected
|
|
func TestWatch_InvalidConfigRejected(t *testing.T) {
|
|
// Reset viper
|
|
viper.Reset()
|
|
|
|
// Create temp config file
|
|
tmpDir := t.TempDir()
|
|
configFile := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
// Initial valid config
|
|
validContent := `
|
|
server:
|
|
address: ":3000"
|
|
read_timeout: "10s"
|
|
write_timeout: "10s"
|
|
shutdown_timeout: "30s"
|
|
prefork: false
|
|
|
|
redis:
|
|
address: "localhost"
|
|
port: 6379
|
|
password: ""
|
|
db: 0
|
|
pool_size: 10
|
|
min_idle_conns: 5
|
|
dial_timeout: "5s"
|
|
read_timeout: "3s"
|
|
write_timeout: "3s"
|
|
|
|
logging:
|
|
level: "info"
|
|
development: false
|
|
app_log:
|
|
filename: "logs/app.log"
|
|
max_size: 100
|
|
max_backups: 30
|
|
max_age: 30
|
|
compress: true
|
|
access_log:
|
|
filename: "logs/access.log"
|
|
max_size: 500
|
|
max_backups: 90
|
|
max_age: 90
|
|
compress: true
|
|
|
|
middleware:
|
|
enable_auth: true
|
|
enable_rate_limiter: false
|
|
rate_limiter:
|
|
max: 100
|
|
expiration: "1m"
|
|
storage: "memory"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(validContent), 0644); err != nil {
|
|
t.Fatalf("failed to create config file: %v", err)
|
|
}
|
|
|
|
// Set config path
|
|
os.Setenv(constants.EnvConfigPath, configFile)
|
|
defer os.Unsetenv(constants.EnvConfigPath)
|
|
|
|
// Load initial config
|
|
cfg, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("failed to load initial config: %v", err)
|
|
}
|
|
|
|
initialLevel := cfg.Logging.Level
|
|
if initialLevel != "info" {
|
|
t.Fatalf("expected initial logging.level info, got %s", initialLevel)
|
|
}
|
|
|
|
// Create logger for testing
|
|
logger := zaptest.NewLogger(t)
|
|
|
|
// Start watcher
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
|
|
go Watch(ctx, logger)
|
|
|
|
// Give watcher time to initialize
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Write INVALID config (malformed YAML)
|
|
invalidContent := `
|
|
server:
|
|
address: ":3000"
|
|
invalid yaml syntax here!!!
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(invalidContent), 0644); err != nil {
|
|
t.Fatalf("failed to write invalid config: %v", err)
|
|
}
|
|
|
|
// Wait for watcher to detect changes
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Verify config was NOT changed (should keep previous valid config)
|
|
currentCfg := Get()
|
|
if currentCfg.Logging.Level != initialLevel {
|
|
t.Errorf("expected config to remain unchanged after invalid update, got logging.level %s instead of %s", currentCfg.Logging.Level, initialLevel)
|
|
}
|
|
|
|
// Restore valid config
|
|
if err := os.WriteFile(configFile, []byte(validContent), 0644); err != nil {
|
|
t.Fatalf("failed to restore valid config: %v", err)
|
|
}
|
|
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
// Now write config with validation error (timeout out of range)
|
|
invalidValidationContent := `
|
|
server:
|
|
address: ":3000"
|
|
read_timeout: "1s"
|
|
write_timeout: "10s"
|
|
shutdown_timeout: "30s"
|
|
|
|
redis:
|
|
address: "localhost"
|
|
port: 6379
|
|
db: 0
|
|
pool_size: 10
|
|
min_idle_conns: 5
|
|
|
|
logging:
|
|
level: "debug"
|
|
app_log:
|
|
filename: "logs/app.log"
|
|
max_size: 100
|
|
max_backups: 30
|
|
max_age: 30
|
|
compress: true
|
|
access_log:
|
|
filename: "logs/access.log"
|
|
max_size: 500
|
|
max_backups: 90
|
|
max_age: 90
|
|
compress: true
|
|
|
|
middleware:
|
|
enable_auth: true
|
|
rate_limiter:
|
|
max: 100
|
|
expiration: "1m"
|
|
storage: "memory"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(invalidValidationContent), 0644); err != nil {
|
|
t.Fatalf("failed to write config with validation error: %v", err)
|
|
}
|
|
|
|
// Wait for watcher to detect changes
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// Verify config was NOT changed (validation should have failed)
|
|
finalCfg := Get()
|
|
if finalCfg.Logging.Level != initialLevel {
|
|
t.Errorf("expected config to remain unchanged after validation error, got logging.level %s instead of %s", finalCfg.Logging.Level, initialLevel)
|
|
}
|
|
if finalCfg.Server.ReadTimeout == 1*time.Second {
|
|
t.Error("expected config to remain unchanged, but read_timeout was updated to invalid value")
|
|
}
|
|
|
|
// Cancel context
|
|
cancel()
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
|
|
// TestWatch_ContextCancellation tests graceful shutdown on context cancellation
|
|
func TestWatch_ContextCancellation(t *testing.T) {
|
|
// Reset viper
|
|
viper.Reset()
|
|
|
|
// Create temp config file
|
|
tmpDir := t.TempDir()
|
|
configFile := filepath.Join(tmpDir, "config.yaml")
|
|
|
|
content := `
|
|
server:
|
|
address: ":3000"
|
|
read_timeout: "10s"
|
|
write_timeout: "10s"
|
|
shutdown_timeout: "30s"
|
|
|
|
redis:
|
|
address: "localhost"
|
|
port: 6379
|
|
db: 0
|
|
pool_size: 10
|
|
min_idle_conns: 5
|
|
|
|
logging:
|
|
level: "info"
|
|
app_log:
|
|
filename: "logs/app.log"
|
|
max_size: 100
|
|
max_backups: 30
|
|
max_age: 30
|
|
compress: true
|
|
access_log:
|
|
filename: "logs/access.log"
|
|
max_size: 500
|
|
max_backups: 90
|
|
max_age: 90
|
|
compress: true
|
|
|
|
middleware:
|
|
enable_auth: true
|
|
rate_limiter:
|
|
max: 100
|
|
expiration: "1m"
|
|
storage: "memory"
|
|
`
|
|
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
|
|
t.Fatalf("failed to create config file: %v", err)
|
|
}
|
|
|
|
os.Setenv(constants.EnvConfigPath, configFile)
|
|
defer os.Unsetenv(constants.EnvConfigPath)
|
|
|
|
// Load config
|
|
_, err := Load()
|
|
if err != nil {
|
|
t.Fatalf("failed to load config: %v", err)
|
|
}
|
|
|
|
// Create logger
|
|
logger := zap.NewNop() // Use no-op logger for this test
|
|
|
|
// Start watcher with context
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
done := make(chan bool)
|
|
go func() {
|
|
Watch(ctx, logger)
|
|
done <- true
|
|
}()
|
|
|
|
// Give watcher time to start
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
// Cancel context (simulate graceful shutdown)
|
|
cancel()
|
|
|
|
// Wait for watcher to stop (should happen quickly)
|
|
select {
|
|
case <-done:
|
|
// Watcher stopped successfully
|
|
case <-time.After(2 * time.Second):
|
|
t.Error("watcher did not stop within timeout after context cancellation")
|
|
}
|
|
}
|