package services import ( "encoding/json" "fmt" jsonparser "github.com/knadh/koanf/parsers/json" "github.com/knadh/koanf/providers/file" "github.com/knadh/koanf/v2" "github.com/stretchr/testify/assert" "github.com/wailsapp/wails/v3/pkg/services/log" "os" "path/filepath" "testing" ) // TestConfig represents a simplified config structure for testing type TestConfig struct { App struct { Name string `json:"name"` Version string `json:"version"` Theme string `json:"theme"` } `json:"app"` User struct { Name string `json:"name"` Email string `json:"email"` Settings struct { AutoSave bool `json:"autoSave"` Language string `json:"language"` NewSetting bool `json:"newSetting"` // This field will be missing in old config NewSetting2 string `json:"newSetting2"` // This field will be missing in old config } `json:"settings"` } `json:"user"` NewSection struct { Enabled bool `json:"enabled"` Value string `json:"value"` } `json:"newSection"` // This entire section will be missing in old config } // createTestConfig creates a test configuration file func createTestConfig(t *testing.T, tempDir string) string { // Old config without some fields oldConfig := 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", // Missing newSetting and newSetting2 }, }, // Missing newSection } // Create config file configPath := filepath.Join(tempDir, "config.json") jsonData, err := json.MarshalIndent(oldConfig, "", " ") if err != nil { t.Fatalf("Failed to marshal test config: %v", err) } err = os.WriteFile(configPath, jsonData, 0644) if err != nil { t.Fatalf("Failed to write test config: %v", err) } return configPath } // TestConfigMigrator_AutoMigrate tests the ConfigMigrator's AutoMigrate functionality func TestConfigMigrator_AutoMigrate(t *testing.T) { // Create temp directory for test tempDir, err := os.MkdirTemp("", "config_migrator_test") if err != nil { t.Fatalf("Failed to create temp directory: %v", err) } defer os.RemoveAll(tempDir) // Create test config file configPath := createTestConfig(t, tempDir) // Create logger logger := log.New() // Create config migrator migrator := NewConfigMigrator(logger, tempDir, "config", configPath) // Create koanf instance and load the config k := koanf.New(".") fileProvider := file.Provider(configPath) jsonParser := jsonparser.Parser() if err := k.Load(fileProvider, jsonParser); err != nil { t.Fatalf("Failed to load config: %v", err) } // Create default config with all fields 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 // New field defaultConfig.User.Settings.NewSetting2 = "value" // New field defaultConfig.NewSection.Enabled = true // New section defaultConfig.NewSection.Value = "new section" // New section // Run auto migration result, err := migrator.AutoMigrate(defaultConfig, k) if err != nil { t.Fatalf("Failed to auto migrate: %v", err) } // Assertions assert.True(t, result.Migrated, "Migration should have been performed") // 打印检测到的缺失字段,便于分析 t.Logf("Detected fields: %v", result.MissingFields) // 验证检测到了正确数量的字段 - 实际检测到4个 assert.Equal(t, 4, len(result.MissingFields), "Should have detected 4 missing fields") // 期望检测到的缺失字段 expectedFields := map[string]bool{ "user.settings.newSetting": true, "user.settings.newSetting2": true, "newSection.enabled": true, "newSection.value": true, } // 验证所有预期的字段都被检测到了 for _, field := range result.MissingFields { _, expected := expectedFields[field] assert.True(t, expected, "Field %s was detected but not expected", field) } // 验证所有检测到的字段都在预期之内 for expectedField := range expectedFields { found := false for _, field := range result.MissingFields { if field == expectedField { found = true break } } assert.True(t, found, "Expected field %s was not detected", expectedField) } // Verify that the fields were actually added to the config assert.True(t, k.Bool("user.settings.newSetting"), "newSetting should be added with correct value") assert.Equal(t, "value", k.String("user.settings.newSetting2"), "newSetting2 should be added with correct value") 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 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, 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) } } }