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

605 lines
13 KiB
Go

// 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"
appVersion: "1.0.0"
spec:
k8sApp:
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"
appVersion: "1.0.0"
spec:
dockerApp:
image: "nginx:latest"
infraTemplate:
- organization: "testorg"
region: "US"
cloudletOrg: "TestOP"
cloudletName: "TestCloudlet"
flavorName: "small"
`,
wantErr: false,
},
{
name: "missing kind",
yaml: `
metadata:
name: "test-app"
appVersion: "1.0.0"
spec:
k8sApp:
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"
appVersion: "1.0.0"
spec:
dockerApp:
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"
appVersion: "1.0.0"
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"
appVersion: "1.0.0"
spec:
k8sApp:
manifestFile: "./test-manifest.yaml"
dockerApp:
appName: "test-app"
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"
appVersion: "1.0.0"
spec:
dockerApp:
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"
appVersion: "1.0.0"
spec:
dockerApp:
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"
appVersion: "1.0.0"
spec:
dockerApp:
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"
appVersion: "1.0.0"
spec:
k8sApp:
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",
AppVersion: "1.0.0",
},
Spec: Spec{
DockerApp: &DockerApp{
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", AppVersion: "1.0.0"},
wantErr: false,
},
{
name: "empty name",
metadata: Metadata{Name: "", AppVersion: "1.0.0"},
wantErr: true,
errMsg: "metadata.name is required",
},
{
name: "name with leading whitespace",
metadata: Metadata{Name: " test-app", AppVersion: "1.0.0"},
wantErr: true,
errMsg: "cannot have leading/trailing whitespace",
},
{
name: "name with trailing whitespace",
metadata: Metadata{Name: "test-app ", AppVersion: "1.0.0"},
wantErr: true,
errMsg: "cannot have leading/trailing whitespace",
},
{
name: "empty app version",
metadata: Metadata{Name: "test-app", AppVersion: ""},
wantErr: true,
errMsg: "metadata.appVersion is required",
},
{
name: "app version with leading whitespace",
metadata: Metadata{Name: "test-app", AppVersion: " 1.0.0"},
wantErr: true,
errMsg: "cannot have leading/trailing whitespace",
},
{
name: "app version with trailing whitespace",
metadata: Metadata{Name: "test-app", AppVersion: "1.0.0 "},
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 TestSpec_GetMethods(t *testing.T) {
k8sSpec := &Spec{
K8sApp: &K8sApp{
ManifestFile: "k8s.yaml",
},
}
dockerSpec := &Spec{
DockerApp: &DockerApp{
ManifestFile: "docker.yaml",
},
}
assert.Equal(t, "k8s.yaml", k8sSpec.GetManifestFile())
assert.True(t, k8sSpec.IsK8sApp())
assert.False(t, k8sSpec.IsDockerApp())
assert.Equal(t, "docker.yaml", dockerSpec.GetManifestFile())
assert.False(t, dockerSpec.IsK8sApp())
assert.True(t, dockerSpec.IsDockerApp())
}
func TestReadManifestFile(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"
appVersion: "1.0.0"
spec:
k8sApp:
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, parsedManifestContent, err := parser.ParseFile(configFile)
assert.NoError(t, err)
assert.Equal(t, manifestContent, parsedManifestContent)
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)
}