🐛 Fixed configuration merge override issue
This commit is contained in:
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
jsonparser "github.com/knadh/koanf/parsers/json"
|
||||
"github.com/knadh/koanf/providers/file"
|
||||
"github.com/knadh/koanf/v2"
|
||||
@@ -158,10 +159,795 @@ func TestConfigMigrator_AutoMigrate(t *testing.T) {
|
||||
assert.True(t, k.Bool("newSection.enabled"), "newSection.enabled should be added with correct value")
|
||||
assert.Equal(t, "new section", k.String("newSection.value"), "newSection.value should be added with correct value")
|
||||
|
||||
// Check that backup was created
|
||||
// Check that backup was cleaned up after successful migration
|
||||
backupFiles, err := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list backup files: %v", err)
|
||||
}
|
||||
assert.Equal(t, 1, len(backupFiles), "One backup file should have been created")
|
||||
assert.Equal(t, 0, len(backupFiles), "Backup file should have been cleaned up after successful migration")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_NoOverwrite tests that user configuration is never overwritten
|
||||
func TestConfigMigrator_NoOverwrite(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_no_overwrite_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create user config with custom values that differ from defaults
|
||||
userConfig := map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "CustomAppName", // Different from default
|
||||
"version": "2.0.0", // Different from default
|
||||
"theme": "custom", // Different from default
|
||||
},
|
||||
"user": map[string]interface{}{
|
||||
"name": "Custom User", // Different from default
|
||||
"email": "custom@example.com", // Different from default
|
||||
"settings": map[string]interface{}{
|
||||
"autoSave": false, // Different from default
|
||||
"language": "zh", // Different from default
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
// Create migrator and load config
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create default config with different values
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "DefaultApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "light"
|
||||
defaultConfig.User.Name = "Default User"
|
||||
defaultConfig.User.Email = "default@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true // This should be added
|
||||
defaultConfig.User.Settings.NewSetting2 = "value" // This should be added
|
||||
defaultConfig.NewSection.Enabled = true // This should be added
|
||||
defaultConfig.NewSection.Value = "new section" // This should be added
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// Verify user values are preserved
|
||||
assert.Equal(t, "CustomAppName", k.String("app.name"), "User's app name should not be overwritten")
|
||||
assert.Equal(t, "2.0.0", k.String("app.version"), "User's version should not be overwritten")
|
||||
assert.Equal(t, "custom", k.String("app.theme"), "User's theme should not be overwritten")
|
||||
assert.Equal(t, "Custom User", k.String("user.name"), "User's name should not be overwritten")
|
||||
assert.Equal(t, "custom@example.com", k.String("user.email"), "User's email should not be overwritten")
|
||||
assert.False(t, k.Bool("user.settings.autoSave"), "User's autoSave should not be overwritten")
|
||||
assert.Equal(t, "zh", k.String("user.settings.language"), "User's language should not be overwritten")
|
||||
|
||||
// Verify missing fields were added with default values
|
||||
assert.True(t, k.Bool("user.settings.newSetting"), "Missing field should be added")
|
||||
assert.Equal(t, "value", k.String("user.settings.newSetting2"), "Missing field should be added")
|
||||
assert.True(t, k.Bool("newSection.enabled"), "Missing section should be added")
|
||||
assert.Equal(t, "new section", k.String("newSection.value"), "Missing section should be added")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_TypeMismatch tests handling of type mismatches
|
||||
func TestConfigMigrator_TypeMismatch(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_type_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create user config where some fields have different types
|
||||
userConfig := map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "TestApp",
|
||||
"version": "1.0.0",
|
||||
"theme": "dark",
|
||||
},
|
||||
"user": map[string]interface{}{
|
||||
"name": "Test User",
|
||||
"email": "test@example.com",
|
||||
"settings": "simple_string", // This is a string, but default is an object
|
||||
},
|
||||
"newSection": 123, // This is a number, but default is an object
|
||||
}
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
// Create migrator and load config
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create default config
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "dark"
|
||||
defaultConfig.User.Name = "Test User"
|
||||
defaultConfig.User.Email = "test@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true
|
||||
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||
defaultConfig.NewSection.Enabled = true
|
||||
defaultConfig.NewSection.Value = "new section"
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Should detect missing fields but refuse to merge them due to type conflicts
|
||||
assert.True(t, result.Migrated, "Migration should be attempted")
|
||||
assert.Greater(t, len(result.MissingFields), 0, "Should detect missing fields even with type mismatch")
|
||||
|
||||
// Verify user's type-mismatched values are preserved (not overwritten)
|
||||
assert.Equal(t, "simple_string", k.String("user.settings"), "User's string value should be preserved")
|
||||
assert.Equal(t, int64(123), k.Int64("newSection"), "User's number value should be preserved")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_ComplexNested tests complex nested structure migration
|
||||
func TestConfigMigrator_ComplexNested(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_complex_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Complex user config with deep nesting
|
||||
userConfig := map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "TestApp",
|
||||
"version": "1.0.0",
|
||||
"advanced": map[string]interface{}{
|
||||
"logging": map[string]interface{}{
|
||||
"level": "info",
|
||||
"file": "/var/log/app.log",
|
||||
// Missing: format, rotation
|
||||
},
|
||||
"performance": map[string]interface{}{
|
||||
"cache": true,
|
||||
// Missing: timeout, maxConnections
|
||||
},
|
||||
// Missing: security section
|
||||
},
|
||||
},
|
||||
"plugins": map[string]interface{}{
|
||||
"enabled": []string{"plugin1", "plugin2"},
|
||||
// Missing: config section
|
||||
},
|
||||
// Missing: monitoring section
|
||||
}
|
||||
|
||||
// Default config with additional nested fields
|
||||
type ComplexConfig struct {
|
||||
App struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
Advanced struct {
|
||||
Logging struct {
|
||||
Level string `json:"level"`
|
||||
File string `json:"file"`
|
||||
Format string `json:"format"`
|
||||
Rotation bool `json:"rotation"`
|
||||
} `json:"logging"`
|
||||
Performance struct {
|
||||
Cache bool `json:"cache"`
|
||||
Timeout int `json:"timeout"`
|
||||
MaxConnections int `json:"maxConnections"`
|
||||
} `json:"performance"`
|
||||
Security struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
TokenType string `json:"tokenType"`
|
||||
ExpireTime int `json:"expireTime"`
|
||||
} `json:"security"`
|
||||
} `json:"advanced"`
|
||||
} `json:"app"`
|
||||
Plugins struct {
|
||||
Enabled []string `json:"enabled"`
|
||||
Config struct {
|
||||
LoadOrder []string `json:"loadOrder"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
} `json:"config"`
|
||||
} `json:"plugins"`
|
||||
Monitoring struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Interval int `json:"interval"`
|
||||
} `json:"monitoring"`
|
||||
}
|
||||
|
||||
// Create config file
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
// Create migrator and load config
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create complete default config
|
||||
defaultConfig := ComplexConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Advanced.Logging.Level = "info"
|
||||
defaultConfig.App.Advanced.Logging.File = "/var/log/app.log"
|
||||
defaultConfig.App.Advanced.Logging.Format = "json"
|
||||
defaultConfig.App.Advanced.Logging.Rotation = true
|
||||
defaultConfig.App.Advanced.Performance.Cache = true
|
||||
defaultConfig.App.Advanced.Performance.Timeout = 30
|
||||
defaultConfig.App.Advanced.Performance.MaxConnections = 100
|
||||
defaultConfig.App.Advanced.Security.Enabled = true
|
||||
defaultConfig.App.Advanced.Security.TokenType = "JWT"
|
||||
defaultConfig.App.Advanced.Security.ExpireTime = 3600
|
||||
defaultConfig.Plugins.Enabled = []string{"plugin1", "plugin2"}
|
||||
defaultConfig.Plugins.Config.LoadOrder = []string{"plugin1", "plugin2"}
|
||||
defaultConfig.Plugins.Config.Settings = map[string]string{"key": "value"}
|
||||
defaultConfig.Monitoring.Enabled = true
|
||||
defaultConfig.Monitoring.Endpoint = "/metrics"
|
||||
defaultConfig.Monitoring.Interval = 60
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// Verify user values are preserved
|
||||
assert.Equal(t, "info", k.String("app.advanced.logging.level"))
|
||||
assert.Equal(t, "/var/log/app.log", k.String("app.advanced.logging.file"))
|
||||
assert.True(t, k.Bool("app.advanced.performance.cache"))
|
||||
|
||||
// Verify missing fields were added
|
||||
assert.Equal(t, "json", k.String("app.advanced.logging.format"))
|
||||
assert.True(t, k.Bool("app.advanced.logging.rotation"))
|
||||
assert.Equal(t, 30, k.Int("app.advanced.performance.timeout"))
|
||||
assert.Equal(t, 100, k.Int("app.advanced.performance.maxConnections"))
|
||||
assert.True(t, k.Bool("app.advanced.security.enabled"))
|
||||
assert.Equal(t, "JWT", k.String("app.advanced.security.tokenType"))
|
||||
assert.Equal(t, 3600, k.Int("app.advanced.security.expireTime"))
|
||||
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.config.loadOrder"))
|
||||
assert.True(t, k.Bool("monitoring.enabled"))
|
||||
assert.Equal(t, "/metrics", k.String("monitoring.endpoint"))
|
||||
assert.Equal(t, 60, k.Int("monitoring.interval"))
|
||||
|
||||
t.Logf("Detected missing fields: %v", result.MissingFields)
|
||||
// Should detect multiple missing fields
|
||||
assert.Greater(t, len(result.MissingFields), 5, "Should detect multiple missing fields")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_MultipleMigrations tests running migration multiple times
|
||||
func TestConfigMigrator_MultipleMigrations(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_multiple_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create initial config
|
||||
configPath := createTestConfig(t, tempDir)
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
|
||||
// Create default config
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "dark"
|
||||
defaultConfig.User.Name = "Test User"
|
||||
defaultConfig.User.Email = "test@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true
|
||||
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||
defaultConfig.NewSection.Enabled = true
|
||||
defaultConfig.NewSection.Value = "new section"
|
||||
|
||||
// First migration
|
||||
k1 := koanf.New(".")
|
||||
k1.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
result1, err := migrator.AutoMigrate(defaultConfig, k1)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result1.Migrated, "First migration should be performed")
|
||||
|
||||
// Second migration - should detect no missing fields
|
||||
k2 := koanf.New(".")
|
||||
k2.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
result2, err := migrator.AutoMigrate(defaultConfig, k2)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result2.Migrated, "Second migration should not be needed")
|
||||
assert.Equal(t, 0, len(result2.MissingFields), "No fields should be missing in second migration")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_BackupHandling tests backup creation and cleanup
|
||||
func TestConfigMigrator_BackupHandling(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_backup_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
configPath := createTestConfig(t, tempDir)
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
|
||||
// Create default config
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "dark"
|
||||
defaultConfig.User.Name = "Test User"
|
||||
defaultConfig.User.Email = "test@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true
|
||||
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||
defaultConfig.NewSection.Enabled = true
|
||||
defaultConfig.NewSection.Value = "new section"
|
||||
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// Backup should be cleaned up after successful migration
|
||||
backupFiles, _ := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||
assert.Equal(t, 0, len(backupFiles), "Backup should be cleaned up after successful migration")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_NoMigrationNeeded tests when no migration is needed
|
||||
func TestConfigMigrator_NoMigrationNeeded(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_no_migration_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create complete config (no missing fields)
|
||||
completeConfig := map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "TestApp",
|
||||
"version": "1.0.0",
|
||||
"theme": "dark",
|
||||
},
|
||||
"user": map[string]interface{}{
|
||||
"name": "Test User",
|
||||
"email": "test@example.com",
|
||||
"settings": map[string]interface{}{
|
||||
"autoSave": true,
|
||||
"language": "en",
|
||||
"newSetting": true,
|
||||
"newSetting2": "value",
|
||||
},
|
||||
},
|
||||
"newSection": map[string]interface{}{
|
||||
"enabled": true,
|
||||
"value": "new section",
|
||||
},
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(completeConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create matching default config
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "dark"
|
||||
defaultConfig.User.Name = "Test User"
|
||||
defaultConfig.User.Email = "test@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true
|
||||
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||
defaultConfig.NewSection.Enabled = true
|
||||
defaultConfig.NewSection.Value = "new section"
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, result.Migrated, "No migration should be needed")
|
||||
assert.Equal(t, 0, len(result.MissingFields), "No fields should be missing")
|
||||
|
||||
// No backup should be created
|
||||
backupFiles, _ := filepath.Glob(filepath.Join(tempDir, "*.backup.*"))
|
||||
assert.Equal(t, 0, len(backupFiles), "No backup should be created when migration is not needed")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_PartialOverride tests partial user override scenarios
|
||||
func TestConfigMigrator_PartialOverride(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_partial_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create user config with partial overrides
|
||||
userConfig := map[string]interface{}{
|
||||
"app": map[string]interface{}{
|
||||
"name": "CustomApp",
|
||||
"version": "2.0.0", // User custom value
|
||||
// Missing: theme (should use default)
|
||||
},
|
||||
"user": map[string]interface{}{
|
||||
"name": "Custom User",
|
||||
"email": "custom@example.com",
|
||||
"settings": map[string]interface{}{
|
||||
"autoSave": false, // User custom value
|
||||
"language": "zh", // User custom value
|
||||
// Missing: newSetting, newSetting2 (should use defaults)
|
||||
},
|
||||
},
|
||||
// Missing: newSection (should use defaults)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create complete default config
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "DefaultApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "light" // Should be added
|
||||
defaultConfig.User.Name = "Default User"
|
||||
defaultConfig.User.Email = "default@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true // Should be added
|
||||
defaultConfig.User.Settings.NewSetting2 = "value" // Should be added
|
||||
defaultConfig.NewSection.Enabled = true // Should be added
|
||||
defaultConfig.NewSection.Value = "new section" // Should be added
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// Verify user values are preserved
|
||||
assert.Equal(t, "CustomApp", k.String("app.name"))
|
||||
assert.Equal(t, "2.0.0", k.String("app.version"))
|
||||
assert.Equal(t, "Custom User", k.String("user.name"))
|
||||
assert.Equal(t, "custom@example.com", k.String("user.email"))
|
||||
assert.False(t, k.Bool("user.settings.autoSave"))
|
||||
assert.Equal(t, "zh", k.String("user.settings.language"))
|
||||
|
||||
// Verify missing fields were added with defaults
|
||||
assert.Equal(t, "light", k.String("app.theme"))
|
||||
assert.True(t, k.Bool("user.settings.newSetting"))
|
||||
assert.Equal(t, "value", k.String("user.settings.newSetting2"))
|
||||
assert.True(t, k.Bool("newSection.enabled"))
|
||||
assert.Equal(t, "new section", k.String("newSection.value"))
|
||||
}
|
||||
|
||||
// TestConfigMigrator_ArrayMerge tests array and slice handling
|
||||
func TestConfigMigrator_ArrayMerge(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_array_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Config with arrays
|
||||
type ArrayConfig struct {
|
||||
Plugins struct {
|
||||
Enabled []string `json:"enabled"`
|
||||
Config struct {
|
||||
LoadOrder []string `json:"loadOrder"`
|
||||
Settings map[string]string `json:"settings"`
|
||||
} `json:"config"`
|
||||
} `json:"plugins"`
|
||||
Database struct {
|
||||
Hosts []string `json:"hosts"`
|
||||
Ports []int `json:"ports"`
|
||||
} `json:"database"`
|
||||
}
|
||||
|
||||
// User config with some arrays
|
||||
userConfig := map[string]interface{}{
|
||||
"plugins": map[string]interface{}{
|
||||
"enabled": []string{"plugin1", "plugin2"}, // User's plugin list
|
||||
// Missing: config section
|
||||
},
|
||||
// Missing: database section
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create default config with arrays
|
||||
defaultConfig := ArrayConfig{}
|
||||
defaultConfig.Plugins.Enabled = []string{"defaultPlugin1", "defaultPlugin2"}
|
||||
defaultConfig.Plugins.Config.LoadOrder = []string{"plugin1", "plugin2"}
|
||||
defaultConfig.Plugins.Config.Settings = map[string]string{"timeout": "30"}
|
||||
defaultConfig.Database.Hosts = []string{"localhost", "backup.host"}
|
||||
defaultConfig.Database.Ports = []int{5432, 5433}
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// User's array should be preserved
|
||||
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.enabled"))
|
||||
|
||||
// Missing arrays should be added from defaults
|
||||
assert.Equal(t, []string{"plugin1", "plugin2"}, k.Strings("plugins.config.loadOrder"))
|
||||
assert.Equal(t, []string{"localhost", "backup.host"}, k.Strings("database.hosts"))
|
||||
assert.Equal(t, []int{5432, 5433}, k.Ints("database.ports"))
|
||||
|
||||
expectedSettings := map[string]string{"timeout": "30"}
|
||||
assert.Equal(t, expectedSettings, k.StringMap("plugins.config.settings"))
|
||||
}
|
||||
|
||||
// TestConfigMigrator_DeepNesting tests very deep nested structures
|
||||
func TestConfigMigrator_DeepNesting(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_deep_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Deep nested config
|
||||
type DeepConfig struct {
|
||||
Level1 struct {
|
||||
Level2 struct {
|
||||
Level3 struct {
|
||||
Level4 struct {
|
||||
Level5 struct {
|
||||
Value string `json:"value"`
|
||||
Count int `json:"count"`
|
||||
} `json:"level5"`
|
||||
} `json:"level4"`
|
||||
} `json:"level3"`
|
||||
} `json:"level2"`
|
||||
} `json:"level1"`
|
||||
}
|
||||
|
||||
// User config with partial deep nesting
|
||||
userConfig := map[string]interface{}{
|
||||
"level1": map[string]interface{}{
|
||||
"level2": map[string]interface{}{
|
||||
"level3": map[string]interface{}{
|
||||
// Missing level4 completely
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Create default config with complete deep nesting
|
||||
defaultConfig := DeepConfig{}
|
||||
defaultConfig.Level1.Level2.Level3.Level4.Level5.Value = "deep_value"
|
||||
defaultConfig.Level1.Level2.Level3.Level4.Level5.Count = 42
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
|
||||
// Verify deep nested values were added
|
||||
assert.Equal(t, "deep_value", k.String("level1.level2.level3.level4.level5.value"))
|
||||
assert.Equal(t, 42, k.Int("level1.level2.level3.level4.level5.count"))
|
||||
|
||||
t.Logf("Missing fields: %v", result.MissingFields)
|
||||
assert.Equal(t, 2, len(result.MissingFields), "Should detect 2 missing deep nested fields")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_EdgeCases tests various edge cases
|
||||
func TestConfigMigrator_EdgeCases(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_edge_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Edge case config with various data types
|
||||
userConfig := map[string]interface{}{
|
||||
"string_empty": "",
|
||||
"string_spaces": " ",
|
||||
"number_zero": 0,
|
||||
"number_float": 3.14,
|
||||
"bool_false": false,
|
||||
"array_empty": []interface{}{},
|
||||
"map_empty": map[string]interface{}{},
|
||||
"null_value": nil,
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.json")
|
||||
jsonData, _ := json.MarshalIndent(userConfig, "", " ")
|
||||
os.WriteFile(configPath, jsonData, 0644)
|
||||
|
||||
logger := log.New()
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(configPath), jsonparser.Parser())
|
||||
|
||||
// Default config with different values
|
||||
type EdgeConfig struct {
|
||||
StringEmpty string `json:"string_empty"`
|
||||
StringSpaces string `json:"string_spaces"`
|
||||
NumberZero int `json:"number_zero"`
|
||||
NumberFloat float64 `json:"number_float"`
|
||||
BoolFalse bool `json:"bool_false"`
|
||||
ArrayEmpty []string `json:"array_empty"`
|
||||
MapEmpty map[string]interface{} `json:"map_empty"`
|
||||
NullValue *string `json:"null_value"`
|
||||
NewField string `json:"new_field"` // This should be added
|
||||
}
|
||||
|
||||
defaultConfig := EdgeConfig{}
|
||||
defaultConfig.StringEmpty = "default_string"
|
||||
defaultConfig.StringSpaces = "default_spaces"
|
||||
defaultConfig.NumberZero = 42
|
||||
defaultConfig.NumberFloat = 2.71
|
||||
defaultConfig.BoolFalse = true
|
||||
defaultConfig.ArrayEmpty = []string{"default"}
|
||||
defaultConfig.MapEmpty = map[string]interface{}{"key": "value"}
|
||||
defaultValue := "default_null"
|
||||
defaultConfig.NullValue = &defaultValue
|
||||
defaultConfig.NewField = "new_field_value"
|
||||
|
||||
// Run migration
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// All user edge case values should be preserved (they exist, even if empty/zero/false)
|
||||
assert.Equal(t, "", k.String("string_empty"))
|
||||
assert.Equal(t, " ", k.String("string_spaces"))
|
||||
assert.Equal(t, 0, k.Int("number_zero"))
|
||||
assert.Equal(t, 3.14, k.Float64("number_float"))
|
||||
assert.False(t, k.Bool("bool_false"))
|
||||
assert.Equal(t, []string{}, k.Strings("array_empty"))
|
||||
|
||||
// Only truly missing field should be added
|
||||
assert.Equal(t, "new_field_value", k.String("new_field"))
|
||||
|
||||
// Should detect 2 missing fields: new_field and map_empty.key
|
||||
// The user has an empty map, but default config has a key inside that map
|
||||
assert.Equal(t, 2, len(result.MissingFields), "Should detect 2 missing fields: new_field and map_empty.key")
|
||||
assert.Contains(t, result.MissingFields, "new_field")
|
||||
assert.Contains(t, result.MissingFields, "map_empty.key")
|
||||
|
||||
// Verify that the key was added to the empty map
|
||||
assert.Equal(t, "value", k.String("map_empty.key"))
|
||||
}
|
||||
|
||||
// TestConfigMigrator_ErrorHandling tests error conditions
|
||||
func TestConfigMigrator_ErrorHandling(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_error_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
logger := log.New()
|
||||
configPath := filepath.Join(tempDir, "nonexistent.json")
|
||||
migrator := NewConfigMigrator(logger, tempDir, "config", configPath)
|
||||
|
||||
// Test with empty koanf (no config file loaded)
|
||||
k := koanf.New(".")
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
|
||||
// This should still work (creates config from scratch)
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, result.Migrated)
|
||||
assert.Greater(t, len(result.MissingFields), 0)
|
||||
|
||||
// Test with corrupted config file
|
||||
corruptedPath := filepath.Join(tempDir, "corrupted.json")
|
||||
os.WriteFile(corruptedPath, []byte("{invalid json"), 0644)
|
||||
|
||||
k2 := koanf.New(".")
|
||||
// This should fail gracefully when trying to load the corrupted file
|
||||
err = k2.Load(file.Provider(corruptedPath), jsonparser.Parser())
|
||||
assert.Error(t, err, "Should fail to load corrupted JSON")
|
||||
}
|
||||
|
||||
// TestConfigMigrator_ConcurrentAccess tests concurrent migration access
|
||||
func TestConfigMigrator_ConcurrentAccess(t *testing.T) {
|
||||
tempDir, err := os.MkdirTemp("", "config_migrator_concurrent_test")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create temp directory: %v", err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
configPath := createTestConfig(t, tempDir)
|
||||
logger := log.New()
|
||||
|
||||
// Create multiple migrators
|
||||
numWorkers := 5
|
||||
results := make(chan *MigrationResult, numWorkers)
|
||||
errors := make(chan error, numWorkers)
|
||||
|
||||
defaultConfig := TestConfig{}
|
||||
defaultConfig.App.Name = "TestApp"
|
||||
defaultConfig.App.Version = "1.0.0"
|
||||
defaultConfig.App.Theme = "dark"
|
||||
defaultConfig.User.Name = "Test User"
|
||||
defaultConfig.User.Email = "test@example.com"
|
||||
defaultConfig.User.Settings.AutoSave = true
|
||||
defaultConfig.User.Settings.Language = "en"
|
||||
defaultConfig.User.Settings.NewSetting = true
|
||||
defaultConfig.User.Settings.NewSetting2 = "value"
|
||||
defaultConfig.NewSection.Enabled = true
|
||||
defaultConfig.NewSection.Value = "new section"
|
||||
|
||||
// Run concurrent migrations
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
go func(workerID int) {
|
||||
// Each worker gets its own config path to avoid file conflicts
|
||||
workerConfigPath := filepath.Join(tempDir, fmt.Sprintf("config_%d.json", workerID))
|
||||
|
||||
// Copy the original config for this worker
|
||||
originalData, _ := os.ReadFile(configPath)
|
||||
os.WriteFile(workerConfigPath, originalData, 0644)
|
||||
|
||||
migrator := NewConfigMigrator(logger, tempDir, fmt.Sprintf("config_%d", workerID), workerConfigPath)
|
||||
k := koanf.New(".")
|
||||
k.Load(file.Provider(workerConfigPath), jsonparser.Parser())
|
||||
|
||||
result, err := migrator.AutoMigrate(defaultConfig, k)
|
||||
if err != nil {
|
||||
errors <- err
|
||||
return
|
||||
}
|
||||
results <- result
|
||||
}(i)
|
||||
}
|
||||
|
||||
// Collect results
|
||||
for i := 0; i < numWorkers; i++ {
|
||||
select {
|
||||
case result := <-results:
|
||||
assert.True(t, result.Migrated, "Each worker should successfully migrate")
|
||||
assert.Equal(t, 4, len(result.MissingFields), "Each worker should detect same missing fields")
|
||||
case err := <-errors:
|
||||
t.Errorf("Worker failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user