备份一下

This commit is contained in:
2025-11-11 15:53:01 +08:00
parent e98dd4d725
commit 39c5b524a9
10 changed files with 4295 additions and 63 deletions

View File

@@ -3,7 +3,6 @@ package config
import (
"errors"
"fmt"
"strings"
"sync/atomic"
"time"
"unsafe"
@@ -93,8 +92,9 @@ func (c *Config) Validate() error {
if c.Redis.Address == "" {
return fmt.Errorf("invalid configuration: redis.address: must be non-empty (current value: empty)")
}
if !strings.Contains(c.Redis.Address, ":") {
return fmt.Errorf("invalid configuration: redis.address: invalid format (current value: %s, expected: HOST:PORT)", c.Redis.Address)
// Port 验证(独立字段)
if c.Redis.Port <= 0 || c.Redis.Port > 65535 {
return fmt.Errorf("invalid configuration: redis.port: port number out of range (current value: %d, expected: 1-65535)", c.Redis.Port)
}
if c.Redis.DB < 0 || c.Redis.DB > 15 {
return fmt.Errorf("invalid configuration: redis.db: database number out of range (current value: %d, expected: 0-15)", c.Redis.DB)

615
pkg/config/config_test.go Normal file
View File

@@ -0,0 +1,615 @@
package config
import (
"testing"
"time"
)
// TestConfig_Validate tests configuration validation rules
func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
config *Config
wantErr bool
errMsg string
}{
{
name: "valid config",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
DB: 0,
PoolSize: 10,
MinIdleConns: 5,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
MaxBackups: 30,
MaxAge: 30,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
MaxBackups: 90,
MaxAge: 90,
},
},
Middleware: MiddlewareConfig{
RateLimiter: RateLimiterConfig{
Max: 100,
Expiration: 1 * time.Minute,
Storage: "memory",
},
},
},
wantErr: false,
},
{
name: "empty server address",
config: &Config{
Server: ServerConfig{
Address: "",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "server.address",
},
{
name: "read timeout too short",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 1 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "read_timeout",
},
{
name: "read timeout too long",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 400 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "read_timeout",
},
{
name: "write timeout out of range",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 1 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "write_timeout",
},
{
name: "shutdown timeout too short",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 5 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "shutdown_timeout",
},
{
name: "empty redis address",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "redis.address",
},
{
name: "invalid redis port - too high",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 99999,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "redis.port",
},
{
name: "invalid redis port - zero",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 0,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "redis.port",
},
{
name: "redis db out of range",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
DB: 20,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "redis.db",
},
{
name: "redis pool size too large",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 2000,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "pool_size",
},
{
name: "min idle conns exceeds pool size",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
MinIdleConns: 20,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "min_idle_conns",
},
{
name: "invalid log level",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "invalid",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "logging.level",
},
{
name: "empty app log filename",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "app_log.filename",
},
{
name: "app log max size out of range",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 2000,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
},
wantErr: true,
errMsg: "app_log.max_size",
},
{
name: "invalid rate limiter storage",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
Middleware: MiddlewareConfig{
RateLimiter: RateLimiterConfig{
Max: 100,
Storage: "invalid",
},
},
},
wantErr: true,
errMsg: "rate_limiter.storage",
},
{
name: "rate limiter max too high",
config: &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
Middleware: MiddlewareConfig{
RateLimiter: RateLimiterConfig{
Max: 20000,
Storage: "memory",
},
},
},
wantErr: true,
errMsg: "rate_limiter.max",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.config.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Config.Validate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && tt.errMsg != "" {
if err == nil {
t.Errorf("expected error containing %q, got nil", tt.errMsg)
} else if err.Error() == "" {
t.Errorf("expected error containing %q, got empty error", tt.errMsg)
}
// Note: We check that error message exists, not exact match
// This is because error messages might change slightly
}
})
}
}
// TestSet tests the Set function
func TestSet(t *testing.T) {
// Valid config
validCfg := &Config{
Server: ServerConfig{
Address: ":3000",
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
ShutdownTimeout: 30 * time.Second,
},
Redis: RedisConfig{
Address: "localhost",
Port: 6379,
PoolSize: 10,
},
Logging: LoggingConfig{
Level: "info",
AppLog: LogRotationConfig{
Filename: "logs/app.log",
MaxSize: 100,
},
AccessLog: LogRotationConfig{
Filename: "logs/access.log",
MaxSize: 500,
},
},
}
err := Set(validCfg)
if err != nil {
t.Errorf("Set() with valid config failed: %v", err)
}
// Verify it was set
got := Get()
if got.Server.Address != ":3000" {
t.Errorf("Get() after Set() returned wrong address: got %s, want :3000", got.Server.Address)
}
// Test with nil config
err = Set(nil)
if err == nil {
t.Error("Set(nil) should return error")
}
// Test with invalid config
invalidCfg := &Config{
Server: ServerConfig{
Address: "", // Empty address is invalid
},
}
err = Set(invalidCfg)
if err == nil {
t.Error("Set() with invalid config should return error")
}
}

661
pkg/config/loader_test.go Normal file
View File

@@ -0,0 +1,661 @@
package config
import (
"os"
"path/filepath"
"testing"
"time"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/spf13/viper"
)
// TestLoad tests the config loading functionality
func TestLoad(t *testing.T) {
tests := []struct {
name string
setupEnv func()
cleanupEnv func()
createConfig func(t *testing.T) string
wantErr bool
validateFunc func(t *testing.T, cfg *Config)
}{
{
name: "valid default config",
setupEnv: func() {
os.Setenv(constants.EnvConfigPath, "")
os.Setenv(constants.EnvConfigEnv, "")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigPath)
os.Unsetenv(constants.EnvConfigEnv)
},
createConfig: func(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
content := `
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(content), 0644); err != nil {
t.Fatalf("failed to create config file: %v", err)
}
// Set as default config path
os.Setenv(constants.EnvConfigPath, configFile)
return configFile
},
wantErr: false,
validateFunc: func(t *testing.T, cfg *Config) {
if cfg.Server.Address != ":3000" {
t.Errorf("expected server.address :3000, got %s", cfg.Server.Address)
}
if cfg.Server.ReadTimeout != 10*time.Second {
t.Errorf("expected read_timeout 10s, got %v", cfg.Server.ReadTimeout)
}
if cfg.Redis.Address != "localhost" {
t.Errorf("expected redis.address localhost, got %s", cfg.Redis.Address)
}
if cfg.Redis.Port != 6379 {
t.Errorf("expected redis.port 6379, got %d", cfg.Redis.Port)
}
if cfg.Redis.PoolSize != 10 {
t.Errorf("expected redis.pool_size 10, got %d", cfg.Redis.PoolSize)
}
if cfg.Logging.Level != "info" {
t.Errorf("expected logging.level info, got %s", cfg.Logging.Level)
}
if cfg.Middleware.EnableAuth != true {
t.Errorf("expected enable_auth true, got %v", cfg.Middleware.EnableAuth)
}
},
},
{
name: "environment-specific config (dev)",
setupEnv: func() {
os.Setenv(constants.EnvConfigEnv, "dev")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigEnv)
os.Unsetenv(constants.EnvConfigPath)
},
createConfig: func(t *testing.T) string {
t.Helper()
// Create configs directory in temp
tmpDir := t.TempDir()
configsDir := filepath.Join(tmpDir, "configs")
if err := os.MkdirAll(configsDir, 0755); err != nil {
t.Fatalf("failed to create configs dir: %v", err)
}
// Create dev config
devConfigFile := filepath.Join(configsDir, "config.dev.yaml")
content := `
server:
address: ":8080"
read_timeout: "15s"
write_timeout: "15s"
shutdown_timeout: "30s"
prefork: false
redis:
address: "localhost"
port: 6379
password: ""
db: 1
pool_size: 5
min_idle_conns: 2
dial_timeout: "5s"
read_timeout: "3s"
write_timeout: "3s"
logging:
level: "debug"
development: true
app_log:
filename: "logs/app.log"
max_size: 50
max_backups: 10
max_age: 7
compress: false
access_log:
filename: "logs/access.log"
max_size: 100
max_backups: 30
max_age: 30
compress: false
middleware:
enable_auth: false
enable_rate_limiter: false
rate_limiter:
max: 50
expiration: "1m"
storage: "memory"
`
if err := os.WriteFile(devConfigFile, []byte(content), 0644); err != nil {
t.Fatalf("failed to create dev config file: %v", err)
}
// Change to tmpDir so relative path works
originalWd, _ := os.Getwd()
os.Chdir(tmpDir)
t.Cleanup(func() { os.Chdir(originalWd) })
return devConfigFile
},
wantErr: false,
validateFunc: func(t *testing.T, cfg *Config) {
if cfg.Server.Address != ":8080" {
t.Errorf("expected server.address :8080, got %s", cfg.Server.Address)
}
if cfg.Redis.DB != 1 {
t.Errorf("expected redis.db 1, got %d", cfg.Redis.DB)
}
if cfg.Logging.Level != "debug" {
t.Errorf("expected logging.level debug, got %s", cfg.Logging.Level)
}
if cfg.Middleware.EnableAuth != false {
t.Errorf("expected enable_auth false, got %v", cfg.Middleware.EnableAuth)
}
},
},
{
name: "invalid YAML syntax",
setupEnv: func() {
os.Setenv(constants.EnvConfigPath, "")
os.Setenv(constants.EnvConfigEnv, "")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigPath)
os.Unsetenv(constants.EnvConfigEnv)
},
createConfig: func(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
content := `
server:
address: ":3000"
invalid yaml syntax here!!!
`
if err := os.WriteFile(configFile, []byte(content), 0644); err != nil {
t.Fatalf("failed to create config file: %v", err)
}
os.Setenv(constants.EnvConfigPath, configFile)
return configFile
},
wantErr: true,
validateFunc: nil,
},
{
name: "validation error - invalid server address",
setupEnv: func() {
os.Setenv(constants.EnvConfigPath, "")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigPath)
},
createConfig: func(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
content := `
server:
address: ""
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)
return configFile
},
wantErr: true,
validateFunc: nil,
},
{
name: "validation error - timeout out of range",
setupEnv: func() {
os.Setenv(constants.EnvConfigPath, "")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigPath)
},
createConfig: func(t *testing.T) string {
t.Helper()
tmpDir := t.TempDir()
configFile := filepath.Join(tmpDir, "config.yaml")
content := `
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: "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)
return configFile
},
wantErr: true,
validateFunc: nil,
},
{
name: "validation error - invalid redis port",
setupEnv: func() {
os.Setenv(constants.EnvConfigPath, "")
},
cleanupEnv: func() {
os.Unsetenv(constants.EnvConfigPath)
},
createConfig: func(t *testing.T) string {
t.Helper()
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: 99999
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)
return configFile
},
wantErr: true,
validateFunc: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset viper for each test
viper.Reset()
// Setup environment
if tt.setupEnv != nil {
tt.setupEnv()
}
// Create config file
if tt.createConfig != nil {
tt.createConfig(t)
}
// Cleanup after test
if tt.cleanupEnv != nil {
defer tt.cleanupEnv()
}
// Load config
cfg, err := Load()
// Check error expectation
if (err != nil) != tt.wantErr {
t.Errorf("Load() error = %v, wantErr %v", err, tt.wantErr)
return
}
// Validate config if no error expected
if !tt.wantErr && tt.validateFunc != nil {
tt.validateFunc(t, cfg)
}
})
}
}
// TestReload tests the config reload functionality
func TestReload(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.Errorf("expected initial logging.level info, got %s", cfg.Logging.Level)
}
if cfg.Server.Address != ":3000" {
t.Errorf("expected initial server.address :3000, got %s", cfg.Server.Address)
}
// Modify config file
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)
}
// Reload config
newCfg, err := Reload()
if err != nil {
t.Fatalf("failed to reload config: %v", err)
}
// Verify updated values
if newCfg.Logging.Level != "debug" {
t.Errorf("expected updated logging.level debug, got %s", newCfg.Logging.Level)
}
if newCfg.Server.Address != ":8080" {
t.Errorf("expected updated server.address :8080, got %s", newCfg.Server.Address)
}
if newCfg.Redis.PoolSize != 20 {
t.Errorf("expected updated redis.pool_size 20, got %d", newCfg.Redis.PoolSize)
}
if newCfg.Middleware.EnableAuth != false {
t.Errorf("expected updated enable_auth false, got %v", newCfg.Middleware.EnableAuth)
}
if newCfg.Middleware.EnableRateLimiter != true {
t.Errorf("expected updated enable_rate_limiter true, got %v", newCfg.Middleware.EnableRateLimiter)
}
// Verify global config was updated
globalCfg := Get()
if globalCfg.Logging.Level != "debug" {
t.Errorf("expected global config updated, got logging.level %s", globalCfg.Logging.Level)
}
}
// TestGetConfigPath tests the GetConfigPath function
func TestGetConfigPath(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)
}
// Get config path
path := GetConfigPath()
if path == "" {
t.Error("expected non-empty config path")
}
// Verify it's an absolute path
if !filepath.IsAbs(path) {
t.Errorf("expected absolute path, got %s", path)
}
}

422
pkg/config/watcher_test.go Normal file
View File

@@ -0,0 +1,422 @@
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")
}
}