Adds tests.
This commit is contained in:
714
internal/config/config_test.go
Normal file
714
internal/config/config_test.go
Normal file
@@ -0,0 +1,714 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Test: LoadGlobalConfig loads valid configuration
|
||||
func TestLoadGlobalConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configYAML string
|
||||
verify func(t *testing.T, config *GlobalConfig)
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "loads complete configuration",
|
||||
configYAML: `wildcloud:
|
||||
repository: "https://github.com/example/repo"
|
||||
currentPhase: "setup"
|
||||
completedPhases:
|
||||
- "phase1"
|
||||
- "phase2"
|
||||
server:
|
||||
port: 8080
|
||||
host: "localhost"
|
||||
operator:
|
||||
email: "admin@example.com"
|
||||
cloud:
|
||||
dns:
|
||||
ip: "192.168.1.1"
|
||||
externalResolver: "8.8.8.8"
|
||||
router:
|
||||
ip: "192.168.1.254"
|
||||
dynamicDns: "example.dyndns.org"
|
||||
dnsmasq:
|
||||
interface: "eth0"
|
||||
cluster:
|
||||
endpointIp: "192.168.1.100"
|
||||
nodes:
|
||||
talos:
|
||||
version: "v1.8.0"
|
||||
`,
|
||||
verify: func(t *testing.T, config *GlobalConfig) {
|
||||
if config.Wildcloud.Repository != "https://github.com/example/repo" {
|
||||
t.Error("repository not loaded correctly")
|
||||
}
|
||||
if config.Server.Port != 8080 {
|
||||
t.Error("port not loaded correctly")
|
||||
}
|
||||
if config.Cloud.DNS.IP != "192.168.1.1" {
|
||||
t.Error("DNS IP not loaded correctly")
|
||||
}
|
||||
if config.Cluster.EndpointIP != "192.168.1.100" {
|
||||
t.Error("endpoint IP not loaded correctly")
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "applies default values",
|
||||
configYAML: `cloud:
|
||||
dns:
|
||||
ip: "192.168.1.1"
|
||||
cluster:
|
||||
nodes:
|
||||
talos:
|
||||
version: "v1.8.0"
|
||||
`,
|
||||
verify: func(t *testing.T, config *GlobalConfig) {
|
||||
if config.Server.Port != 5055 {
|
||||
t.Errorf("default port not applied, got %d, want 5055", config.Server.Port)
|
||||
}
|
||||
if config.Server.Host != "0.0.0.0" {
|
||||
t.Errorf("default host not applied, got %q, want %q", config.Server.Host, "0.0.0.0")
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "preserves custom port and host",
|
||||
configYAML: `server:
|
||||
port: 9000
|
||||
host: "127.0.0.1"
|
||||
cloud:
|
||||
dns:
|
||||
ip: "192.168.1.1"
|
||||
cluster:
|
||||
nodes:
|
||||
talos:
|
||||
version: "v1.8.0"
|
||||
`,
|
||||
verify: func(t *testing.T, config *GlobalConfig) {
|
||||
if config.Server.Port != 9000 {
|
||||
t.Errorf("custom port not preserved, got %d, want 9000", config.Server.Port)
|
||||
}
|
||||
if config.Server.Host != "127.0.0.1" {
|
||||
t.Errorf("custom host not preserved, got %q, want %q", config.Server.Host, "127.0.0.1")
|
||||
}
|
||||
},
|
||||
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 := LoadGlobalConfig(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: LoadGlobalConfig error cases
|
||||
func TestLoadGlobalConfig_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 := LoadGlobalConfig(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: SaveGlobalConfig saves configuration correctly
|
||||
func TestSaveGlobalConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *GlobalConfig
|
||||
verify func(t *testing.T, configPath string)
|
||||
}{
|
||||
{
|
||||
name: "saves complete configuration",
|
||||
config: &GlobalConfig{
|
||||
Wildcloud: struct {
|
||||
Repository string `yaml:"repository" json:"repository"`
|
||||
CurrentPhase string `yaml:"currentPhase" json:"currentPhase"`
|
||||
CompletedPhases []string `yaml:"completedPhases" json:"completedPhases"`
|
||||
}{
|
||||
Repository: "https://github.com/example/repo",
|
||||
CurrentPhase: "setup",
|
||||
CompletedPhases: []string{"phase1", "phase2"},
|
||||
},
|
||||
Server: struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
}{
|
||||
Port: 8080,
|
||||
Host: "localhost",
|
||||
},
|
||||
},
|
||||
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, "repository") {
|
||||
t.Error("saved config missing repository field")
|
||||
}
|
||||
if !strings.Contains(contentStr, "8080") {
|
||||
t.Error("saved config missing port value")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "saves empty configuration",
|
||||
config: &GlobalConfig{},
|
||||
verify: func(t *testing.T, configPath string) {
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
t.Error("config file not created")
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "subdir", "config.yaml")
|
||||
|
||||
err := SaveGlobalConfig(tt.config, configPath)
|
||||
if err != nil {
|
||||
t.Errorf("SaveGlobalConfig failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
t.Errorf("config file not created: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify file permissions
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat config 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 := LoadGlobalConfig(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: SaveGlobalConfig creates directory
|
||||
func TestSaveGlobalConfig_CreatesDirectory(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "nested", "dirs", "config.yaml")
|
||||
|
||||
config := &GlobalConfig{}
|
||||
err := SaveGlobalConfig(config, configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveGlobalConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify nested directories were created
|
||||
if _, err := os.Stat(filepath.Dir(configPath)); err != nil {
|
||||
t.Errorf("directory not created: %v", err)
|
||||
}
|
||||
|
||||
// Verify file exists
|
||||
if _, err := os.Stat(configPath); err != nil {
|
||||
t.Errorf("config 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 DNS IP is empty",
|
||||
config: &GlobalConfig{
|
||||
Cloud: struct {
|
||||
DNS struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
} `yaml:"dns" json:"dns"`
|
||||
Router struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
DynamicDns string `yaml:"dynamicDns" json:"dynamicDns"`
|
||||
} `yaml:"router" json:"router"`
|
||||
Dnsmasq struct {
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
} `yaml:"dnsmasq" json:"dnsmasq"`
|
||||
}{
|
||||
DNS: struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
}{
|
||||
IP: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "config with only Talos version is empty",
|
||||
config: &GlobalConfig{
|
||||
Cluster: struct {
|
||||
EndpointIP string `yaml:"endpointIp" json:"endpointIp"`
|
||||
Nodes struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
} `yaml:"nodes" json:"nodes"`
|
||||
}{
|
||||
Nodes: struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
}{
|
||||
Talos: struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
}{
|
||||
Version: "v1.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "config with both DNS IP and Talos version is not empty",
|
||||
config: &GlobalConfig{
|
||||
Cloud: struct {
|
||||
DNS struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
} `yaml:"dns" json:"dns"`
|
||||
Router struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
DynamicDns string `yaml:"dynamicDns" json:"dynamicDns"`
|
||||
} `yaml:"router" json:"router"`
|
||||
Dnsmasq struct {
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
} `yaml:"dnsmasq" json:"dnsmasq"`
|
||||
}{
|
||||
DNS: struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
}{
|
||||
IP: "192.168.1.1",
|
||||
},
|
||||
},
|
||||
Cluster: struct {
|
||||
EndpointIP string `yaml:"endpointIp" json:"endpointIp"`
|
||||
Nodes struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
} `yaml:"nodes" json:"nodes"`
|
||||
}{
|
||||
Nodes: struct {
|
||||
Talos struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
} `yaml:"talos" json:"talos"`
|
||||
}{
|
||||
Talos: struct {
|
||||
Version string `yaml:"version" json:"version"`
|
||||
}{
|
||||
Version: "v1.8.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
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:
|
||||
router:
|
||||
ip: "192.168.1.254"
|
||||
dns:
|
||||
ip: "192.168.1.1"
|
||||
externalResolver: "8.8.8.8"
|
||||
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.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: &InstanceConfig{
|
||||
Cloud: struct {
|
||||
Router struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
} `yaml:"router" json:"router"`
|
||||
DNS struct {
|
||||
IP string `yaml:"ip" json:"ip"`
|
||||
ExternalResolver string `yaml:"externalResolver" json:"externalResolver"`
|
||||
} `yaml:"dns" json:"dns"`
|
||||
DHCPRange string `yaml:"dhcpRange" json:"dhcpRange"`
|
||||
Dnsmasq struct {
|
||||
Interface string `yaml:"interface" json:"interface"`
|
||||
} `yaml:"dnsmasq" json:"dnsmasq"`
|
||||
BaseDomain string `yaml:"baseDomain" json:"baseDomain"`
|
||||
Domain string `yaml:"domain" json:"domain"`
|
||||
InternalDomain string `yaml:"internalDomain" json:"internalDomain"`
|
||||
NFS struct {
|
||||
MediaPath string `yaml:"mediaPath" json:"mediaPath"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
StorageCapacity string `yaml:"storageCapacity" json:"storageCapacity"`
|
||||
} `yaml:"nfs" json:"nfs"`
|
||||
DockerRegistryHost string `yaml:"dockerRegistryHost" json:"dockerRegistryHost"`
|
||||
Backup struct {
|
||||
Root string `yaml:"root" json:"root"`
|
||||
} `yaml:"backup" json:"backup"`
|
||||
}{
|
||||
BaseDomain: "example.com",
|
||||
Domain: "home",
|
||||
},
|
||||
},
|
||||
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()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
// Create config with all fields
|
||||
original := &GlobalConfig{
|
||||
Wildcloud: struct {
|
||||
Repository string `yaml:"repository" json:"repository"`
|
||||
CurrentPhase string `yaml:"currentPhase" json:"currentPhase"`
|
||||
CompletedPhases []string `yaml:"completedPhases" json:"completedPhases"`
|
||||
}{
|
||||
Repository: "https://github.com/example/repo",
|
||||
CurrentPhase: "setup",
|
||||
CompletedPhases: []string{"phase1", "phase2"},
|
||||
},
|
||||
Server: struct {
|
||||
Port int `yaml:"port" json:"port"`
|
||||
Host string `yaml:"host" json:"host"`
|
||||
}{
|
||||
Port: 8080,
|
||||
Host: "localhost",
|
||||
},
|
||||
Operator: struct {
|
||||
Email string `yaml:"email" json:"email"`
|
||||
}{
|
||||
Email: "admin@example.com",
|
||||
},
|
||||
}
|
||||
|
||||
// Save config
|
||||
if err := SaveGlobalConfig(original, configPath); err != nil {
|
||||
t.Fatalf("SaveGlobalConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Load config
|
||||
loaded, err := LoadGlobalConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("LoadGlobalConfig failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all fields match
|
||||
if loaded.Wildcloud.Repository != original.Wildcloud.Repository {
|
||||
t.Errorf("repository mismatch: got %q, want %q", loaded.Wildcloud.Repository, original.Wildcloud.Repository)
|
||||
}
|
||||
if loaded.Server.Port != original.Server.Port {
|
||||
t.Errorf("port mismatch: got %d, want %d", loaded.Server.Port, original.Server.Port)
|
||||
}
|
||||
if loaded.Operator.Email != original.Operator.Email {
|
||||
t.Errorf("email mismatch: got %q, want %q", loaded.Operator.Email, original.Operator.Email)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
905
internal/config/manager_test.go
Normal file
905
internal/config/manager_test.go
Normal file
@@ -0,0 +1,905 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/wild-cloud/wild-central/daemon/internal/storage"
|
||||
)
|
||||
|
||||
// Test: NewManager creates manager successfully
|
||||
func TestNewManager(t *testing.T) {
|
||||
m := NewManager()
|
||||
if m == nil {
|
||||
t.Fatal("NewManager returned nil")
|
||||
}
|
||||
if m.yq == nil {
|
||||
t.Error("Manager.yq is nil")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: EnsureInstanceConfig creates config file with proper structure
|
||||
func TestEnsureInstanceConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, instancePath string)
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "creates config when not exists",
|
||||
setupFunc: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns nil when config exists",
|
||||
setupFunc: func(t *testing.T, instancePath string) {
|
||||
configPath := filepath.Join(instancePath, "config.yaml")
|
||||
content := `baseDomain: "test.local"
|
||||
domain: "test"
|
||||
internalDomain: "internal.test"
|
||||
dhcpRange: ""
|
||||
backup:
|
||||
root: ""
|
||||
nfs:
|
||||
host: ""
|
||||
mediaPath: ""
|
||||
cluster:
|
||||
name: ""
|
||||
loadBalancerIp: ""
|
||||
ipAddressPool: ""
|
||||
hostnamePrefix: ""
|
||||
certManager:
|
||||
cloudflare:
|
||||
domain: ""
|
||||
zoneID: ""
|
||||
externalDns:
|
||||
ownerId: ""
|
||||
nodes:
|
||||
talos:
|
||||
version: ""
|
||||
schematicId: ""
|
||||
control:
|
||||
vip: ""
|
||||
activeNodes: []
|
||||
`
|
||||
if err := storage.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "returns error when config is invalid yaml",
|
||||
setupFunc: func(t *testing.T, instancePath string) {
|
||||
configPath := filepath.Join(instancePath, "config.yaml")
|
||||
content := `invalid: yaml: content: [[[`
|
||||
if err := storage.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: true,
|
||||
errContains: "invalid config file",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
instancePath := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if tt.setupFunc != nil {
|
||||
tt.setupFunc(t, instancePath)
|
||||
}
|
||||
|
||||
err := m.EnsureInstanceConfig(instancePath)
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify config file exists
|
||||
configPath := filepath.Join(instancePath, "config.yaml")
|
||||
if !storage.FileExists(configPath) {
|
||||
t.Error("config file not created")
|
||||
}
|
||||
|
||||
// Verify config is valid YAML
|
||||
if err := m.ValidateConfig(configPath); err != nil {
|
||||
t.Errorf("config validation failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify config has expected structure
|
||||
content, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
contentStr := string(content)
|
||||
requiredFields := []string{"baseDomain:", "domain:", "cluster:", "backup:", "nfs:"}
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(contentStr, field) {
|
||||
t.Errorf("config missing required field: %s", field)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GetConfigValue retrieves values correctly
|
||||
func TestGetConfigValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configYAML string
|
||||
key string
|
||||
want string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "get simple string value",
|
||||
configYAML: `baseDomain: "example.com"
|
||||
domain: "test"
|
||||
`,
|
||||
key: "baseDomain",
|
||||
want: "example.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get nested value with dot notation",
|
||||
configYAML: `cluster:
|
||||
name: "my-cluster"
|
||||
nodes:
|
||||
talos:
|
||||
version: "v1.8.0"
|
||||
`,
|
||||
key: "cluster.nodes.talos.version",
|
||||
want: "v1.8.0",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get empty string value",
|
||||
configYAML: `baseDomain: ""
|
||||
`,
|
||||
key: "baseDomain",
|
||||
want: "",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get non-existent key returns null",
|
||||
configYAML: `baseDomain: "example.com"
|
||||
`,
|
||||
key: "nonexistent",
|
||||
want: "null",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "get from array",
|
||||
configYAML: `cluster:
|
||||
nodes:
|
||||
activeNodes:
|
||||
- "node1"
|
||||
- "node2"
|
||||
`,
|
||||
key: "cluster.nodes.activeNodes.[0]",
|
||||
want: "node1",
|
||||
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 := storage.WriteFile(configPath, []byte(tt.configYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, tt.key)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
if got != tt.want {
|
||||
t.Errorf("got %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: GetConfigValue error cases
|
||||
func TestGetConfigValue_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T) string
|
||||
key string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "non-existent file",
|
||||
setupFunc: func(t *testing.T) string {
|
||||
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
||||
},
|
||||
key: "baseDomain",
|
||||
errContains: "config file not found",
|
||||
},
|
||||
{
|
||||
name: "malformed yaml",
|
||||
setupFunc: func(t *testing.T) string {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
content := `invalid: yaml: [[[`
|
||||
if err := storage.WriteFile(configPath, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
return configPath
|
||||
},
|
||||
key: "baseDomain",
|
||||
errContains: "getting config value",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
configPath := tt.setupFunc(t)
|
||||
m := NewManager()
|
||||
|
||||
_, err := m.GetConfigValue(configPath, tt.key)
|
||||
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: SetConfigValue sets values correctly
|
||||
func TestSetConfigValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialYAML string
|
||||
key string
|
||||
value string
|
||||
verifyFunc func(t *testing.T, configPath string)
|
||||
}{
|
||||
{
|
||||
name: "set simple value",
|
||||
initialYAML: `baseDomain: ""
|
||||
domain: ""
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "example.com",
|
||||
verifyFunc: func(t *testing.T, configPath string) {
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, "baseDomain")
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed: %v", err)
|
||||
}
|
||||
if got != "example.com" {
|
||||
t.Errorf("got %q, want %q", got, "example.com")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set nested value",
|
||||
initialYAML: `cluster:
|
||||
name: ""
|
||||
nodes:
|
||||
talos:
|
||||
version: ""
|
||||
`,
|
||||
key: "cluster.nodes.talos.version",
|
||||
value: "v1.8.0",
|
||||
verifyFunc: func(t *testing.T, configPath string) {
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, "cluster.nodes.talos.version")
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed: %v", err)
|
||||
}
|
||||
if got != "v1.8.0" {
|
||||
t.Errorf("got %q, want %q", got, "v1.8.0")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "update existing value",
|
||||
initialYAML: `baseDomain: "old.com"
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "new.com",
|
||||
verifyFunc: func(t *testing.T, configPath string) {
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, "baseDomain")
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed: %v", err)
|
||||
}
|
||||
if got != "new.com" {
|
||||
t.Errorf("got %q, want %q", got, "new.com")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "create new nested path",
|
||||
initialYAML: `cluster: {}
|
||||
`,
|
||||
key: "cluster.newField",
|
||||
value: "newValue",
|
||||
verifyFunc: func(t *testing.T, configPath string) {
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, "cluster.newField")
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed: %v", err)
|
||||
}
|
||||
if got != "newValue" {
|
||||
t.Errorf("got %q, want %q", got, "newValue")
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set value with special characters",
|
||||
initialYAML: `baseDomain: ""
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: `special"quotes'and\backslashes`,
|
||||
verifyFunc: func(t *testing.T, configPath string) {
|
||||
m := NewManager()
|
||||
got, err := m.GetConfigValue(configPath, "baseDomain")
|
||||
if err != nil {
|
||||
t.Fatalf("verify failed: %v", err)
|
||||
}
|
||||
if got != `special"quotes'and\backslashes` {
|
||||
t.Errorf("got %q, want %q", got, `special"quotes'and\backslashes`)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
if err := storage.WriteFile(configPath, []byte(tt.initialYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
if err := m.SetConfigValue(configPath, tt.key, tt.value); err != nil {
|
||||
t.Errorf("SetConfigValue failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the value was set correctly
|
||||
tt.verifyFunc(t, configPath)
|
||||
|
||||
// Verify config is still valid YAML
|
||||
if err := m.ValidateConfig(configPath); err != nil {
|
||||
t.Errorf("config validation failed after set: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: SetConfigValue error cases
|
||||
func TestSetConfigValue_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T) string
|
||||
key string
|
||||
value string
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "non-existent file",
|
||||
setupFunc: func(t *testing.T) string {
|
||||
return filepath.Join(t.TempDir(), "nonexistent.yaml")
|
||||
},
|
||||
key: "baseDomain",
|
||||
value: "example.com",
|
||||
errContains: "config file not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
configPath := tt.setupFunc(t)
|
||||
m := NewManager()
|
||||
|
||||
err := m.SetConfigValue(configPath, tt.key, tt.value)
|
||||
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: SetConfigValue with concurrent access
|
||||
func TestSetConfigValue_ConcurrentAccess(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
initialYAML := `counter: "0"
|
||||
`
|
||||
if err := storage.WriteFile(configPath, []byte(initialYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
const numGoroutines = 10
|
||||
|
||||
var wg sync.WaitGroup
|
||||
errors := make(chan error, numGoroutines)
|
||||
|
||||
// Launch multiple goroutines trying to write different values
|
||||
for i := 0; i < numGoroutines; i++ {
|
||||
wg.Add(1)
|
||||
go func(val int) {
|
||||
defer wg.Done()
|
||||
key := "counter"
|
||||
value := string(rune('0' + val))
|
||||
if err := m.SetConfigValue(configPath, key, value); err != nil {
|
||||
errors <- err
|
||||
}
|
||||
}(i)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(errors)
|
||||
|
||||
// Check if any errors occurred
|
||||
for err := range errors {
|
||||
t.Errorf("concurrent write error: %v", err)
|
||||
}
|
||||
|
||||
// Verify config is still valid after concurrent access
|
||||
if err := m.ValidateConfig(configPath); err != nil {
|
||||
t.Errorf("config validation failed after concurrent writes: %v", err)
|
||||
}
|
||||
|
||||
// Verify we can read the value (should be one of the written values)
|
||||
value, err := m.GetConfigValue(configPath, "counter")
|
||||
if err != nil {
|
||||
t.Errorf("failed to read value after concurrent writes: %v", err)
|
||||
}
|
||||
if value == "" || value == "null" {
|
||||
t.Error("counter value is empty after concurrent writes")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: EnsureConfigValue sets value only when not set
|
||||
func TestEnsureConfigValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
initialYAML string
|
||||
key string
|
||||
value string
|
||||
expectSet bool
|
||||
}{
|
||||
{
|
||||
name: "sets value when empty string",
|
||||
initialYAML: `baseDomain: ""
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "example.com",
|
||||
expectSet: true,
|
||||
},
|
||||
{
|
||||
name: "sets value when null",
|
||||
initialYAML: `baseDomain: null
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "example.com",
|
||||
expectSet: true,
|
||||
},
|
||||
{
|
||||
name: "does not set value when already set",
|
||||
initialYAML: `baseDomain: "existing.com"
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "new.com",
|
||||
expectSet: false,
|
||||
},
|
||||
{
|
||||
name: "sets value when key does not exist",
|
||||
initialYAML: `domain: "test"
|
||||
`,
|
||||
key: "baseDomain",
|
||||
value: "example.com",
|
||||
expectSet: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
|
||||
if err := storage.WriteFile(configPath, []byte(tt.initialYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
|
||||
// Get initial value
|
||||
initialVal, _ := m.GetConfigValue(configPath, tt.key)
|
||||
|
||||
// Call EnsureConfigValue
|
||||
if err := m.EnsureConfigValue(configPath, tt.key, tt.value); err != nil {
|
||||
t.Errorf("EnsureConfigValue failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Get final value
|
||||
finalVal, err := m.GetConfigValue(configPath, tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfigValue failed: %v", err)
|
||||
}
|
||||
|
||||
if tt.expectSet {
|
||||
if finalVal != tt.value {
|
||||
t.Errorf("expected value to be set to %q, got %q", tt.value, finalVal)
|
||||
}
|
||||
} else {
|
||||
if finalVal != initialVal {
|
||||
t.Errorf("expected value to remain %q, got %q", initialVal, finalVal)
|
||||
}
|
||||
}
|
||||
|
||||
// Call EnsureConfigValue again - should be idempotent
|
||||
if err := m.EnsureConfigValue(configPath, tt.key, "different.com"); err != nil {
|
||||
t.Errorf("second EnsureConfigValue failed: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Value should not change on second call
|
||||
secondVal, err := m.GetConfigValue(configPath, tt.key)
|
||||
if err != nil {
|
||||
t.Fatalf("GetConfigValue failed: %v", err)
|
||||
}
|
||||
if secondVal != finalVal {
|
||||
t.Errorf("value changed on second ensure: %q -> %q", finalVal, secondVal)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ValidateConfig validates YAML correctly
|
||||
func TestValidateConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configYAML string
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "valid yaml",
|
||||
configYAML: `baseDomain: "example.com"
|
||||
domain: "test"
|
||||
cluster:
|
||||
name: "my-cluster"
|
||||
`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid yaml - bad indentation",
|
||||
configYAML: `baseDomain: "example.com"\n domain: "test"`,
|
||||
wantErr: true,
|
||||
errContains: "yaml validation failed",
|
||||
},
|
||||
{
|
||||
name: "invalid yaml - unclosed bracket",
|
||||
configYAML: `cluster: { name: "test"`,
|
||||
wantErr: true,
|
||||
errContains: "yaml validation failed",
|
||||
},
|
||||
{
|
||||
name: "empty file",
|
||||
configYAML: "",
|
||||
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 := storage.WriteFile(configPath, []byte(tt.configYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
err := m.ValidateConfig(configPath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: ValidateConfig error cases
|
||||
func TestValidateConfig_Errors(t *testing.T) {
|
||||
t.Run("non-existent file", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
configPath := filepath.Join(tempDir, "nonexistent.yaml")
|
||||
|
||||
m := NewManager()
|
||||
err := m.ValidateConfig(configPath)
|
||||
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if !strings.Contains(err.Error(), "config file not found") {
|
||||
t.Errorf("error %q does not contain 'config file not found'", err.Error())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test: CopyConfig copies configuration correctly
|
||||
func TestCopyConfig(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
srcYAML string
|
||||
setupDst func(t *testing.T, dstPath string)
|
||||
wantErr bool
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "copies config successfully",
|
||||
srcYAML: `baseDomain: "example.com"
|
||||
domain: "test"
|
||||
cluster:
|
||||
name: "my-cluster"
|
||||
`,
|
||||
setupDst: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "creates destination directory",
|
||||
srcYAML: `baseDomain: "example.com"`,
|
||||
setupDst: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "overwrites existing destination",
|
||||
srcYAML: `baseDomain: "new.com"
|
||||
`,
|
||||
setupDst: func(t *testing.T, dstPath string) {
|
||||
oldContent := `baseDomain: "old.com"`
|
||||
if err := storage.WriteFile(dstPath, []byte(oldContent), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
srcPath := filepath.Join(tempDir, "source.yaml")
|
||||
dstPath := filepath.Join(tempDir, "subdir", "dest.yaml")
|
||||
|
||||
// Create source file
|
||||
if err := storage.WriteFile(srcPath, []byte(tt.srcYAML), 0644); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
|
||||
// Setup destination if needed
|
||||
if tt.setupDst != nil {
|
||||
if err := storage.EnsureDir(filepath.Dir(dstPath), 0755); err != nil {
|
||||
t.Fatalf("setup failed: %v", err)
|
||||
}
|
||||
tt.setupDst(t, dstPath)
|
||||
}
|
||||
|
||||
m := NewManager()
|
||||
err := m.CopyConfig(srcPath, dstPath)
|
||||
|
||||
if tt.wantErr {
|
||||
if err == nil {
|
||||
t.Error("expected error, got nil")
|
||||
} else if tt.errContains != "" && !strings.Contains(err.Error(), tt.errContains) {
|
||||
t.Errorf("error %q does not contain %q", err.Error(), tt.errContains)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify destination file exists
|
||||
if !storage.FileExists(dstPath) {
|
||||
t.Error("destination file not created")
|
||||
}
|
||||
|
||||
// Verify content matches source
|
||||
srcContent, err := storage.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read source: %v", err)
|
||||
}
|
||||
dstContent, err := storage.ReadFile(dstPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read destination: %v", err)
|
||||
}
|
||||
|
||||
if string(srcContent) != string(dstContent) {
|
||||
t.Error("destination content does not match source")
|
||||
}
|
||||
|
||||
// Verify destination is valid YAML
|
||||
if err := m.ValidateConfig(dstPath); err != nil {
|
||||
t.Errorf("destination config validation failed: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test: CopyConfig error cases
|
||||
func TestCopyConfig_Errors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFunc func(t *testing.T, tempDir string) (srcPath, dstPath string)
|
||||
errContains string
|
||||
}{
|
||||
{
|
||||
name: "source file does not exist",
|
||||
setupFunc: func(t *testing.T, tempDir string) (string, string) {
|
||||
return filepath.Join(tempDir, "nonexistent.yaml"),
|
||||
filepath.Join(tempDir, "dest.yaml")
|
||||
},
|
||||
errContains: "source config file not found",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
srcPath, dstPath := tt.setupFunc(t, tempDir)
|
||||
|
||||
m := NewManager()
|
||||
err := m.CopyConfig(srcPath, dstPath)
|
||||
|
||||
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: File permissions are preserved
|
||||
func TestEnsureInstanceConfig_FilePermissions(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if err := m.EnsureInstanceConfig(tempDir); err != nil {
|
||||
t.Fatalf("EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to stat config file: %v", err)
|
||||
}
|
||||
|
||||
// Verify file has 0644 permissions
|
||||
if info.Mode().Perm() != 0644 {
|
||||
t.Errorf("expected permissions 0644, got %v", info.Mode().Perm())
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Idempotent config creation
|
||||
func TestEnsureInstanceConfig_Idempotent(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
// First call creates config
|
||||
if err := m.EnsureInstanceConfig(tempDir); err != nil {
|
||||
t.Fatalf("first EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
firstContent, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
// Second call should not modify config
|
||||
if err := m.EnsureInstanceConfig(tempDir); err != nil {
|
||||
t.Fatalf("second EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
secondContent, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
if string(firstContent) != string(secondContent) {
|
||||
t.Error("config content changed on second call")
|
||||
}
|
||||
}
|
||||
|
||||
// Test: Config structure contains all required fields
|
||||
func TestEnsureInstanceConfig_RequiredFields(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
m := NewManager()
|
||||
|
||||
if err := m.EnsureInstanceConfig(tempDir); err != nil {
|
||||
t.Fatalf("EnsureInstanceConfig failed: %v", err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(tempDir, "config.yaml")
|
||||
content, err := storage.ReadFile(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to read config: %v", err)
|
||||
}
|
||||
|
||||
contentStr := string(content)
|
||||
requiredFields := []string{
|
||||
"baseDomain:",
|
||||
"domain:",
|
||||
"internalDomain:",
|
||||
"dhcpRange:",
|
||||
"backup:",
|
||||
"nfs:",
|
||||
"cluster:",
|
||||
"loadBalancerIp:",
|
||||
"ipAddressPool:",
|
||||
"hostnamePrefix:",
|
||||
"certManager:",
|
||||
"externalDns:",
|
||||
"nodes:",
|
||||
"talos:",
|
||||
"version:",
|
||||
"schematicId:",
|
||||
"control:",
|
||||
"vip:",
|
||||
"activeNodes:",
|
||||
}
|
||||
|
||||
for _, field := range requiredFields {
|
||||
if !strings.Contains(contentStr, field) {
|
||||
t.Errorf("config missing required field: %s", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user