edge-connect-client/internal/config/parser_test.go

776 lines
16 KiB
Go
Raw Normal View History

// 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:
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:
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:
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:
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:
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:
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:
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:
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:
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{
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{
AppVersion: "1.0.0",
ManifestFile: "k8s.yaml",
},
}
dockerSpec := &Spec{
DockerApp: &DockerApp{
AppVersion: "2.0.0",
ManifestFile: "docker.yaml",
},
}
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, "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)
})
}
}