package config import ( "os" "path/filepath" "strings" "testing" "gopkg.in/yaml.v3" ) // Test: LoadState loads valid state func TestLoadState(t *testing.T) { tests := []struct { name string stateYAML string verify func(t *testing.T, config *GlobalConfig) wantErr bool }{ { name: "loads complete state", stateYAML: `operator: email: "admin@example.com" cloud: central: domain: "central.example.com" `, verify: func(t *testing.T, config *GlobalConfig) { if config.Operator.Email != "admin@example.com" { t.Error("operator email not loaded correctly") } if config.Cloud.Central.Domain != "central.example.com" { t.Error("central domain not loaded correctly") } }, wantErr: false, }, { name: "loads minimal state", stateYAML: `operator: email: "admin@example.com" `, verify: func(t *testing.T, config *GlobalConfig) { if config.Operator.Email != "admin@example.com" { t.Error("operator email not loaded correctly") } }, wantErr: false, }, { name: "loads empty state", stateYAML: "{}\n", verify: func(t *testing.T, config *GlobalConfig) { if config.Operator.Email != "" { t.Error("expected empty operator email") } }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() statePath := filepath.Join(tempDir, "state.yaml") if err := os.WriteFile(statePath, []byte(tt.stateYAML), 0644); err != nil { t.Fatalf("setup failed: %v", err) } config, err := LoadState(statePath) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if config == nil { t.Fatal("config is nil") } if tt.verify != nil { tt.verify(t, config) } }) } } // Test: LoadState error cases func TestLoadState_Errors(t *testing.T) { tests := []struct { name string setupFunc func(t *testing.T) string errContains string }{ { name: "non-existent file", setupFunc: func(t *testing.T) string { return filepath.Join(t.TempDir(), "nonexistent.yaml") }, errContains: "reading config file", }, { name: "invalid yaml", setupFunc: func(t *testing.T) string { tempDir := t.TempDir() statePath := filepath.Join(tempDir, "state.yaml") content := `invalid: yaml: [[[` if err := os.WriteFile(statePath, []byte(content), 0644); err != nil { t.Fatalf("setup failed: %v", err) } return statePath }, errContains: "parsing config file", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { statePath := tt.setupFunc(t) _, err := LoadState(statePath) if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), tt.errContains) { t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) } }) } } // Test: SaveState saves state correctly func TestSaveState(t *testing.T) { tests := []struct { name string config *GlobalConfig verify func(t *testing.T, statePath string) }{ { name: "saves complete state", config: func() *GlobalConfig { cfg := &GlobalConfig{} cfg.Operator.Email = "admin@example.com" cfg.Cloud.Central.Domain = "central.example.com" return cfg }(), verify: func(t *testing.T, statePath string) { content, err := os.ReadFile(statePath) if err != nil { t.Fatalf("failed to read saved state: %v", err) } contentStr := string(content) if !strings.Contains(contentStr, "admin@example.com") { t.Error("saved state missing operator email") } if !strings.Contains(contentStr, "central.example.com") { t.Error("saved state missing central domain") } }, }, { name: "saves empty state", config: &GlobalConfig{}, verify: func(t *testing.T, statePath string) { if _, err := os.Stat(statePath); os.IsNotExist(err) { t.Error("state file not created") } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() statePath := filepath.Join(tempDir, "subdir", "state.yaml") err := SaveState(tt.config, statePath) if err != nil { t.Errorf("SaveState failed: %v", err) return } // Verify file exists if _, err := os.Stat(statePath); err != nil { t.Errorf("state file not created: %v", err) return } // Verify file permissions info, err := os.Stat(statePath) if err != nil { t.Fatalf("failed to stat state file: %v", err) } if info.Mode().Perm() != 0644 { t.Errorf("expected permissions 0644, got %v", info.Mode().Perm()) } // Verify content can be loaded back loadedConfig, err := LoadState(statePath) if err != nil { t.Errorf("failed to reload saved state: %v", err) } else if loadedConfig == nil { t.Error("loaded config is nil") } if tt.verify != nil { tt.verify(t, statePath) } }) } } // Test: SaveState creates directory func TestSaveState_CreatesDirectory(t *testing.T) { tempDir := t.TempDir() statePath := filepath.Join(tempDir, "nested", "dirs", "state.yaml") config := &GlobalConfig{} err := SaveState(config, statePath) if err != nil { t.Fatalf("SaveState failed: %v", err) } // Verify nested directories were created if _, err := os.Stat(filepath.Dir(statePath)); err != nil { t.Errorf("directory not created: %v", err) } // Verify file exists if _, err := os.Stat(statePath); err != nil { t.Errorf("state file not created: %v", err) } } // Test: GlobalConfig.IsEmpty checks if config is empty func TestGlobalConfig_IsEmpty(t *testing.T) { tests := []struct { name string config *GlobalConfig want bool }{ { name: "nil config is empty", config: nil, want: true, }, { name: "default config is empty", config: &GlobalConfig{}, want: true, }, { name: "config with only central domain is not empty", config: func() *GlobalConfig { cfg := &GlobalConfig{} cfg.Cloud.Central.Domain = "central.example.com" return cfg }(), want: false, }, { name: "config with only operator email is not empty", config: func() *GlobalConfig { cfg := &GlobalConfig{} cfg.Operator.Email = "admin@example.com" return cfg }(), want: false, }, { name: "config with all fields is not empty", config: func() *GlobalConfig { cfg := &GlobalConfig{} cfg.Cloud.Central.Domain = "central.example.com" cfg.Operator.Email = "admin@example.com" return cfg }(), want: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := tt.config.IsEmpty() if got != tt.want { t.Errorf("IsEmpty() = %v, want %v", got, tt.want) } }) } } // Test: LoadCloudConfig loads instance configuration func TestLoadCloudConfig(t *testing.T) { tests := []struct { name string configYAML string verify func(t *testing.T, config *InstanceConfig) wantErr bool }{ { name: "loads complete instance configuration", configYAML: `cloud: dhcpRange: "192.168.1.100,192.168.1.200" baseDomain: "example.com" domain: "home" internalDomain: "internal.example.com" cluster: name: "my-cluster" loadBalancerIp: "192.168.1.10" nodes: talos: version: "v1.8.0" activeNodes: - node1: role: "control" interface: "eth0" disk: "/dev/sda" `, verify: func(t *testing.T, config *InstanceConfig) { if config.Cloud.BaseDomain != "example.com" { t.Error("base domain not loaded correctly") } if config.Cloud.DHCPRange != "192.168.1.100,192.168.1.200" { t.Error("DHCP range not loaded correctly") } if config.Cluster.Name != "my-cluster" { t.Error("cluster name not loaded correctly") } if config.Cluster.Nodes.Talos.Version != "v1.8.0" { t.Error("talos version not loaded correctly") } }, wantErr: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.yaml") if err := os.WriteFile(configPath, []byte(tt.configYAML), 0644); err != nil { t.Fatalf("setup failed: %v", err) } config, err := LoadCloudConfig(configPath) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } return } if err != nil { t.Errorf("unexpected error: %v", err) return } if config == nil { t.Fatal("config is nil") } if tt.verify != nil { tt.verify(t, config) } }) } } // Test: LoadCloudConfig error cases func TestLoadCloudConfig_Errors(t *testing.T) { tests := []struct { name string setupFunc func(t *testing.T) string errContains string }{ { name: "non-existent file", setupFunc: func(t *testing.T) string { return filepath.Join(t.TempDir(), "nonexistent.yaml") }, errContains: "reading config file", }, { name: "invalid yaml", setupFunc: func(t *testing.T) string { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.yaml") content := `invalid: yaml: [[[` if err := os.WriteFile(configPath, []byte(content), 0644); err != nil { t.Fatalf("setup failed: %v", err) } return configPath }, errContains: "parsing config file", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { configPath := tt.setupFunc(t) _, err := LoadCloudConfig(configPath) if err == nil { t.Error("expected error, got nil") } else if !strings.Contains(err.Error(), tt.errContains) { t.Errorf("error %q does not contain %q", err.Error(), tt.errContains) } }) } } // Test: SaveCloudConfig saves instance configuration func TestSaveCloudConfig(t *testing.T) { tests := []struct { name string config *InstanceConfig verify func(t *testing.T, configPath string) }{ { name: "saves instance configuration", config: func() *InstanceConfig { cfg := &InstanceConfig{} cfg.Cloud.BaseDomain = "example.com" cfg.Cloud.Domain = "home" return cfg }(), verify: func(t *testing.T, configPath string) { content, err := os.ReadFile(configPath) if err != nil { t.Fatalf("failed to read saved config: %v", err) } contentStr := string(content) if !strings.Contains(contentStr, "example.com") { t.Error("saved config missing base domain") } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "subdir", "config.yaml") err := SaveCloudConfig(tt.config, configPath) if err != nil { t.Errorf("SaveCloudConfig failed: %v", err) return } // Verify file exists if _, err := os.Stat(configPath); err != nil { t.Errorf("config file not created: %v", err) return } // Verify content can be loaded back loadedConfig, err := LoadCloudConfig(configPath) if err != nil { t.Errorf("failed to reload saved config: %v", err) } else if loadedConfig == nil { t.Error("loaded config is nil") } if tt.verify != nil { tt.verify(t, configPath) } }) } } // Test: Round-trip save and load preserves data func TestGlobalConfig_RoundTrip(t *testing.T) { tempDir := t.TempDir() statePath := filepath.Join(tempDir, "state.yaml") // Create config with all fields original := &GlobalConfig{} original.Operator.Email = "admin@example.com" original.Cloud.Central.Domain = "central.example.com" // Save if err := SaveState(original, statePath); err != nil { t.Fatalf("SaveState failed: %v", err) } // Load loaded, err := LoadState(statePath) if err != nil { t.Fatalf("LoadState failed: %v", err) } // Verify all fields match if loaded.Operator.Email != original.Operator.Email { t.Errorf("email mismatch: got %q, want %q", loaded.Operator.Email, original.Operator.Email) } if loaded.Cloud.Central.Domain != original.Cloud.Central.Domain { t.Errorf("central domain mismatch: got %q, want %q", loaded.Cloud.Central.Domain, original.Cloud.Central.Domain) } } // Test: Round-trip save and load for instance config func TestInstanceConfig_RoundTrip(t *testing.T) { tempDir := t.TempDir() configPath := filepath.Join(tempDir, "config.yaml") // Create instance config original := &InstanceConfig{} original.Cloud.BaseDomain = "example.com" original.Cloud.Domain = "home" original.Cluster.Name = "my-cluster" // Save config if err := SaveCloudConfig(original, configPath); err != nil { t.Fatalf("SaveCloudConfig failed: %v", err) } // Load config loaded, err := LoadCloudConfig(configPath) if err != nil { t.Fatalf("LoadCloudConfig failed: %v", err) } // Verify fields match if loaded.Cloud.BaseDomain != original.Cloud.BaseDomain { t.Errorf("base domain mismatch: got %q, want %q", loaded.Cloud.BaseDomain, original.Cloud.BaseDomain) } if loaded.Cluster.Name != original.Cluster.Name { t.Errorf("cluster name mismatch: got %q, want %q", loaded.Cluster.Name, original.Cluster.Name) } } func TestDeepMerge(t *testing.T) { tests := []struct { name string dst map[string]any src map[string]any expected map[string]any }{ { name: "src overrides dst flat keys", dst: map[string]any{"a": "1", "b": "2"}, src: map[string]any{"b": "3", "c": "4"}, expected: map[string]any{"a": "1", "b": "3", "c": "4"}, }, { name: "nested maps merge recursively", dst: map[string]any{ "cloud": map[string]any{ "central": map[string]any{"domain": "central.example.com"}, }, }, src: map[string]any{ "cloud": map[string]any{ "domain": "example.com", }, }, expected: map[string]any{ "cloud": map[string]any{ "central": map[string]any{"domain": "central.example.com"}, "domain": "example.com", }, }, }, { name: "src nested key overrides dst nested key", dst: map[string]any{ "cloud": map[string]any{"domain": "old.com"}, }, src: map[string]any{ "cloud": map[string]any{"domain": "new.com"}, }, expected: map[string]any{ "cloud": map[string]any{"domain": "new.com"}, }, }, { name: "empty src returns dst", dst: map[string]any{"a": "1"}, src: map[string]any{}, expected: map[string]any{"a": "1"}, }, { name: "empty dst returns src", dst: map[string]any{}, src: map[string]any{"a": "1"}, expected: map[string]any{"a": "1"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := DeepMerge(tt.dst, tt.src) resultYAML, _ := yaml.Marshal(result) expectedYAML, _ := yaml.Marshal(tt.expected) if string(resultYAML) != string(expectedYAML) { t.Errorf("DeepMerge() =\n%s\nwant:\n%s", resultYAML, expectedYAML) } }) } } func TestLoadMergedInstanceConfig(t *testing.T) { dataDir := t.TempDir() instanceName := "test" instanceConfigDir := filepath.Join(dataDir, "instances", instanceName, "config") os.MkdirAll(instanceConfigDir, 0755) globalState := `operator: email: test@example.com cloud: central: domain: central.example.com ` instanceConfig := `cluster: name: test nodes: control: vip: 192.168.1.100 cloud: domain: cloud.example.com ` os.WriteFile(filepath.Join(dataDir, "state.yaml"), []byte(globalState), 0644) os.WriteFile(filepath.Join(instanceConfigDir, "instance.yaml"), []byte(instanceConfig), 0644) merged, err := LoadMergedInstanceConfig(dataDir, instanceName) if err != nil { t.Fatalf("LoadMergedInstanceConfig() error: %v", err) } cloud, ok := merged["cloud"].(map[string]any) if !ok { t.Fatal("missing cloud key in merged config") } central, ok := cloud["central"].(map[string]any) if !ok { t.Fatal("missing cloud.central key — global state not merged") } if central["domain"] != "central.example.com" { t.Errorf("cloud.central.domain = %v, want central.example.com", central["domain"]) } if cloud["domain"] != "cloud.example.com" { t.Errorf("cloud.domain = %v, want cloud.example.com", cloud["domain"]) } cluster, ok := merged["cluster"].(map[string]any) if !ok { t.Fatal("missing cluster key — instance config not merged") } if cluster["name"] != "test" { t.Errorf("cluster.name = %v, want test", cluster["name"]) } operator, ok := merged["operator"].(map[string]any) if !ok { t.Fatal("missing operator key — global state not merged") } if operator["email"] != "test@example.com" { t.Errorf("operator.email = %v, want test@example.com", operator["email"]) } } func TestLoadMergedInstanceConfig_NoGlobalState(t *testing.T) { dataDir := t.TempDir() instanceConfigDir := filepath.Join(dataDir, "instances", "test", "config") os.MkdirAll(instanceConfigDir, 0755) os.WriteFile(filepath.Join(instanceConfigDir, "instance.yaml"), []byte("cluster:\n name: test\n"), 0644) merged, err := LoadMergedInstanceConfig(dataDir, "test") if err != nil { t.Fatalf("LoadMergedInstanceConfig() error: %v", err) } cluster, ok := merged["cluster"].(map[string]any) if !ok { t.Fatal("missing cluster key") } if cluster["name"] != "test" { t.Errorf("cluster.name = %v, want test", cluster["name"]) } }