// ABOUTME: Comprehensive tests for EdgeConnect configuration parser with validation scenarios // ABOUTME: Tests all validation rules, error conditions, and successful parsing cases package config import ( "os" "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestNewParser(t *testing.T) { parser := NewParser() assert.NotNil(t, parser) assert.IsType(t, &ConfigParser{}, parser) } func TestConfigParser_ParseBytes(t *testing.T) { parser := NewParser() tests := []struct { name string yaml string wantErr bool errMsg string }{ { name: "valid k8s config", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: k8sApp: appName: "test-app" appVersion: "1.0.0" manifestFile: "./test-manifest.yaml" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: true, // Will fail because manifest file doesn't exist errMsg: "manifestFile does not exist", }, { name: "valid docker config", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: false, }, { name: "missing kind", yaml: ` metadata: name: "test-app" spec: k8sApp: appName: "test-app" appVersion: "1.0.0" manifestFile: "./test-manifest.yaml" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: true, errMsg: "kind is required", }, { name: "invalid kind", yaml: ` kind: invalid-kind metadata: name: "test-app" spec: dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: true, errMsg: "unsupported kind: invalid-kind", }, { name: "missing app definition", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: true, errMsg: "spec must define either k8sApp or dockerApp", }, { name: "both k8s and docker apps", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: k8sApp: appName: "test-app" appVersion: "1.0.0" manifestFile: "./test-manifest.yaml" dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" `, wantErr: true, errMsg: "spec cannot define both k8sApp and dockerApp", }, { name: "empty infrastructure template", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: [] `, wantErr: true, errMsg: "infraTemplate is required and must contain at least one target", }, { name: "with network config", yaml: ` kind: edgeconnect-deployment metadata: name: "test-app" spec: dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" network: outboundConnections: - protocol: "tcp" portRangeMin: 80 portRangeMax: 80 remoteCIDR: "0.0.0.0/0" `, wantErr: false, }, { name: "empty data", yaml: "", wantErr: true, errMsg: "configuration data cannot be empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config, err := parser.ParseBytes([]byte(tt.yaml)) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } assert.Nil(t, config) } else { assert.NoError(t, err) assert.NotNil(t, config) } }) } } func TestConfigParser_ParseFile(t *testing.T) { parser := NewParser() // Create temporary directory for test files tempDir := t.TempDir() // Create a valid config file validConfig := ` kind: edgeconnect-deployment metadata: name: "test-app" spec: dockerApp: appName: "test-app" appVersion: "1.0.0" image: "nginx:latest" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" ` validFile := filepath.Join(tempDir, "valid.yaml") err := os.WriteFile(validFile, []byte(validConfig), 0644) require.NoError(t, err) // Test valid file parsing config, err := parser.ParseFile(validFile) assert.NoError(t, err) assert.NotNil(t, config) assert.Equal(t, "edgeconnect-deployment", config.Kind) assert.Equal(t, "test-app", config.Metadata.Name) // Test non-existent file nonExistentFile := filepath.Join(tempDir, "nonexistent.yaml") config, err = parser.ParseFile(nonExistentFile) assert.Error(t, err) assert.Contains(t, err.Error(), "does not exist") assert.Nil(t, config) // Test empty filename config, err = parser.ParseFile("") assert.Error(t, err) assert.Contains(t, err.Error(), "filename cannot be empty") assert.Nil(t, config) // Test invalid YAML invalidFile := filepath.Join(tempDir, "invalid.yaml") err = os.WriteFile(invalidFile, []byte("invalid: yaml: content: ["), 0644) require.NoError(t, err) config, err = parser.ParseFile(invalidFile) assert.Error(t, err) assert.Contains(t, err.Error(), "YAML parsing failed") assert.Nil(t, config) } func TestConfigParser_RelativePathResolution(t *testing.T) { parser := NewParser() tempDir := t.TempDir() // Create a manifest file manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n" manifestFile := filepath.Join(tempDir, "manifest.yaml") err := os.WriteFile(manifestFile, []byte(manifestContent), 0644) require.NoError(t, err) // Create config with relative path configContent := ` kind: edgeconnect-deployment metadata: name: "test-app" spec: k8sApp: appName: "test-app" appVersion: "1.0.0" manifestFile: "./manifest.yaml" infraTemplate: - organization: "testorg" region: "US" cloudletOrg: "TestOP" cloudletName: "TestCloudlet" flavorName: "small" ` configFile := filepath.Join(tempDir, "config.yaml") err = os.WriteFile(configFile, []byte(configContent), 0644) require.NoError(t, err) config, err := parser.ParseFile(configFile) assert.NoError(t, err) assert.NotNil(t, config) // Check that relative path was resolved to absolute expectedPath := filepath.Join(tempDir, "manifest.yaml") assert.Equal(t, expectedPath, config.Spec.K8sApp.ManifestFile) } func TestEdgeConnectConfig_Validate(t *testing.T) { tests := []struct { name string config EdgeConnectConfig wantErr bool errMsg string }{ { name: "valid config", config: EdgeConnectConfig{ Kind: "edgeconnect-deployment", Metadata: Metadata{ Name: "test-app", }, Spec: Spec{ DockerApp: &DockerApp{ AppName: "test-app", AppVersion: "1.0.0", Image: "nginx:latest", }, InfraTemplate: []InfraTemplate{ { Organization: "testorg", Region: "US", CloudletOrg: "TestOP", CloudletName: "TestCloudlet", FlavorName: "small", }, }, }, }, wantErr: false, }, { name: "missing kind", config: EdgeConnectConfig{ Metadata: Metadata{Name: "test"}, }, wantErr: true, errMsg: "kind is required", }, { name: "invalid kind", config: EdgeConnectConfig{ Kind: "invalid", Metadata: Metadata{Name: "test"}, }, wantErr: true, errMsg: "unsupported kind", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.config.Validate() if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestMetadata_Validate(t *testing.T) { tests := []struct { name string metadata Metadata wantErr bool errMsg string }{ { name: "valid metadata", metadata: Metadata{Name: "test-app"}, wantErr: false, }, { name: "empty name", metadata: Metadata{Name: ""}, wantErr: true, errMsg: "metadata.name is required", }, { name: "name with leading whitespace", metadata: Metadata{Name: " test-app"}, wantErr: true, errMsg: "cannot have leading/trailing whitespace", }, { name: "name with trailing whitespace", metadata: Metadata{Name: "test-app "}, wantErr: true, errMsg: "cannot have leading/trailing whitespace", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.metadata.Validate() if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestOutboundConnection_Validate(t *testing.T) { tests := []struct { name string connection OutboundConnection wantErr bool errMsg string }{ { name: "valid connection", connection: OutboundConnection{ Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, wantErr: false, }, { name: "missing protocol", connection: OutboundConnection{ PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, wantErr: true, errMsg: "protocol is required", }, { name: "invalid protocol", connection: OutboundConnection{ Protocol: "invalid", PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, wantErr: true, errMsg: "protocol must be one of: tcp, udp, icmp", }, { name: "invalid port range min", connection: OutboundConnection{ Protocol: "tcp", PortRangeMin: 0, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, wantErr: true, errMsg: "portRangeMin must be between 1 and 65535", }, { name: "invalid port range max", connection: OutboundConnection{ Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 65536, RemoteCIDR: "0.0.0.0/0", }, wantErr: true, errMsg: "portRangeMax must be between 1 and 65535", }, { name: "min greater than max", connection: OutboundConnection{ Protocol: "tcp", PortRangeMin: 443, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, wantErr: true, errMsg: "portRangeMin (443) cannot be greater than portRangeMax (80)", }, { name: "missing remote CIDR", connection: OutboundConnection{ Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 80, }, wantErr: true, errMsg: "remoteCIDR is required", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := tt.connection.Validate() if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestConfigParser_ValidateInfrastructureUniqueness(t *testing.T) { parser := &ConfigParser{} tests := []struct { name string config *EdgeConnectConfig wantErr bool errMsg string }{ { name: "unique infrastructure", config: &EdgeConnectConfig{ Spec: Spec{ InfraTemplate: []InfraTemplate{ { Organization: "org1", Region: "US", CloudletOrg: "cloudlet1", CloudletName: "name1", }, { Organization: "org1", Region: "EU", CloudletOrg: "cloudlet1", CloudletName: "name1", }, }, }, }, wantErr: false, }, { name: "duplicate infrastructure", config: &EdgeConnectConfig{ Spec: Spec{ InfraTemplate: []InfraTemplate{ { Organization: "org1", Region: "US", CloudletOrg: "cloudlet1", CloudletName: "name1", }, { Organization: "org1", Region: "US", CloudletOrg: "cloudlet1", CloudletName: "name1", }, }, }, }, wantErr: true, errMsg: "duplicate infrastructure target", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parser.ValidateInfrastructureUniqueness(tt.config) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestConfigParser_ValidatePortRanges(t *testing.T) { parser := &ConfigParser{} tests := []struct { name string config *EdgeConnectConfig wantErr bool errMsg string }{ { name: "no network config", config: &EdgeConnectConfig{ Spec: Spec{ Network: nil, }, }, wantErr: false, }, { name: "non-overlapping ports", config: &EdgeConnectConfig{ Spec: Spec{ Network: &NetworkConfig{ OutboundConnections: []OutboundConnection{ { Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, { Protocol: "tcp", PortRangeMin: 443, PortRangeMax: 443, RemoteCIDR: "0.0.0.0/0", }, }, }, }, }, wantErr: false, }, { name: "overlapping ports same protocol and CIDR", config: &EdgeConnectConfig{ Spec: Spec{ Network: &NetworkConfig{ OutboundConnections: []OutboundConnection{ { Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 90, RemoteCIDR: "0.0.0.0/0", }, { Protocol: "tcp", PortRangeMin: 85, PortRangeMax: 95, RemoteCIDR: "0.0.0.0/0", }, }, }, }, }, wantErr: true, errMsg: "overlapping port ranges", }, { name: "overlapping ports different protocol", config: &EdgeConnectConfig{ Spec: Spec{ Network: &NetworkConfig{ OutboundConnections: []OutboundConnection{ { Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 90, RemoteCIDR: "0.0.0.0/0", }, { Protocol: "udp", PortRangeMin: 85, PortRangeMax: 95, RemoteCIDR: "0.0.0.0/0", }, }, }, }, }, wantErr: false, // Different protocols can overlap }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parser.ValidatePortRanges(tt.config) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestGetInstanceName(t *testing.T) { tests := []struct { appName string appVersion string expected string }{ {"myapp", "1.0.0", "myapp-1.0.0-instance"}, {"test-app", "v2.1", "test-app-v2.1-instance"}, {"app", "latest", "app-latest-instance"}, } for _, tt := range tests { t.Run(tt.appName+"-"+tt.appVersion, func(t *testing.T) { result := GetInstanceName(tt.appName, tt.appVersion) assert.Equal(t, tt.expected, result) }) } } func TestSpec_GetMethods(t *testing.T) { k8sSpec := &Spec{ K8sApp: &K8sApp{ AppName: "k8s-app", AppVersion: "1.0.0", ManifestFile: "k8s.yaml", }, } dockerSpec := &Spec{ DockerApp: &DockerApp{ AppName: "docker-app", AppVersion: "2.0.0", ManifestFile: "docker.yaml", }, } assert.Equal(t, "k8s-app", k8sSpec.GetAppName()) assert.Equal(t, "1.0.0", k8sSpec.GetAppVersion()) assert.Equal(t, "k8s.yaml", k8sSpec.GetManifestFile()) assert.True(t, k8sSpec.IsK8sApp()) assert.False(t, k8sSpec.IsDockerApp()) assert.Equal(t, "docker-app", dockerSpec.GetAppName()) assert.Equal(t, "2.0.0", dockerSpec.GetAppVersion()) assert.Equal(t, "docker.yaml", dockerSpec.GetManifestFile()) assert.False(t, dockerSpec.IsK8sApp()) assert.True(t, dockerSpec.IsDockerApp()) } func TestPortRangesOverlap(t *testing.T) { tests := []struct { name string min1 int max1 int min2 int max2 int expected bool }{ {"no overlap", 10, 20, 30, 40, false}, {"overlap", 10, 20, 15, 25, true}, {"adjacent", 10, 20, 21, 30, false}, {"touching", 10, 20, 20, 30, true}, {"contained", 10, 30, 15, 25, true}, {"same range", 10, 20, 10, 20, true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := portRangesOverlap(tt.min1, tt.max1, tt.min2, tt.max2) assert.Equal(t, tt.expected, result) }) } }