2025-09-29 16:36:21 +02:00
|
|
|
// ABOUTME: Comprehensive tests for EdgeConnect deployment planner with mock scenarios
|
|
|
|
|
// ABOUTME: Tests planning logic, state comparison, and various deployment scenarios
|
|
|
|
|
package apply
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
2025-09-29 17:35:34 +02:00
|
|
|
"edp.buildth.ing/DevFW-CICD/edge-connect-client/internal/config"
|
2025-10-20 13:34:22 +02:00
|
|
|
v2 "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect/v2"
|
2025-09-29 16:36:21 +02:00
|
|
|
"github.com/stretchr/testify/assert"
|
|
|
|
|
"github.com/stretchr/testify/mock"
|
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MockEdgeConnectClient is a mock implementation of the EdgeConnect client
|
|
|
|
|
type MockEdgeConnectClient struct {
|
|
|
|
|
mock.Mock
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) ShowApp(ctx context.Context, appKey v2.AppKey, region string) (v2.App, error) {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, appKey, region)
|
|
|
|
|
if args.Get(0) == nil {
|
2025-10-20 13:34:22 +02:00
|
|
|
return v2.App{}, args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
2025-10-20 13:34:22 +02:00
|
|
|
return args.Get(0).(v2.App), args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) ShowAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) (v2.AppInstance, error) {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, instanceKey, region)
|
|
|
|
|
if args.Get(0) == nil {
|
2025-10-20 13:34:22 +02:00
|
|
|
return v2.AppInstance{}, args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
2025-10-20 13:34:22 +02:00
|
|
|
return args.Get(0).(v2.AppInstance), args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) CreateApp(ctx context.Context, input *v2.NewAppInput) error {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, input)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) CreateAppInstance(ctx context.Context, input *v2.NewAppInstanceInput) error {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, input)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) DeleteApp(ctx context.Context, appKey v2.AppKey, region string) error {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, appKey, region)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) UpdateApp(ctx context.Context, input *v2.UpdateAppInput) error {
|
2025-10-01 10:49:15 +02:00
|
|
|
args := m.Called(ctx, input)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) UpdateAppInstance(ctx context.Context, input *v2.UpdateAppInstanceInput) error {
|
2025-10-01 10:49:15 +02:00
|
|
|
args := m.Called(ctx, input)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) DeleteAppInstance(ctx context.Context, instanceKey v2.AppInstanceKey, region string) error {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, instanceKey, region)
|
|
|
|
|
return args.Error(0)
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) ShowApps(ctx context.Context, appKey v2.AppKey, region string) ([]v2.App, error) {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, appKey, region)
|
|
|
|
|
if args.Get(0) == nil {
|
|
|
|
|
return nil, args.Error(1)
|
|
|
|
|
}
|
2025-10-20 13:34:22 +02:00
|
|
|
return args.Get(0).([]v2.App), args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
func (m *MockEdgeConnectClient) ShowAppInstances(ctx context.Context, instanceKey v2.AppInstanceKey, region string) ([]v2.AppInstance, error) {
|
2025-09-29 16:36:21 +02:00
|
|
|
args := m.Called(ctx, instanceKey, region)
|
|
|
|
|
if args.Get(0) == nil {
|
|
|
|
|
return nil, args.Error(1)
|
|
|
|
|
}
|
2025-10-20 13:34:22 +02:00
|
|
|
return args.Get(0).([]v2.AppInstance), args.Error(1)
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNewPlanner(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
|
|
|
|
|
assert.NotNil(t, planner)
|
|
|
|
|
assert.IsType(t, &EdgeConnectPlanner{}, planner)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDefaultPlanOptions(t *testing.T) {
|
|
|
|
|
opts := DefaultPlanOptions()
|
|
|
|
|
|
|
|
|
|
assert.False(t, opts.DryRun)
|
|
|
|
|
assert.False(t, opts.Force)
|
|
|
|
|
assert.False(t, opts.SkipStateCheck)
|
|
|
|
|
assert.True(t, opts.ParallelQueries)
|
|
|
|
|
assert.Equal(t, 30*time.Second, opts.Timeout)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func createTestConfig(t *testing.T) *config.EdgeConnectConfig {
|
|
|
|
|
// Create temporary manifest file
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
|
manifestFile := filepath.Join(tempDir, "test-manifest.yaml")
|
|
|
|
|
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
|
|
|
|
err := os.WriteFile(manifestFile, []byte(manifestContent), 0644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
return &config.EdgeConnectConfig{
|
|
|
|
|
Kind: "edgeconnect-deployment",
|
|
|
|
|
Metadata: config.Metadata{
|
2025-10-07 17:09:36 +02:00
|
|
|
Name: "test-app",
|
|
|
|
|
AppVersion: "1.0.0",
|
|
|
|
|
Organization: "testorg",
|
2025-09-29 16:36:21 +02:00
|
|
|
},
|
|
|
|
|
Spec: config.Spec{
|
|
|
|
|
K8sApp: &config.K8sApp{
|
|
|
|
|
ManifestFile: manifestFile,
|
|
|
|
|
},
|
|
|
|
|
InfraTemplate: []config.InfraTemplate{
|
|
|
|
|
{
|
|
|
|
|
Region: "US",
|
|
|
|
|
CloudletOrg: "TestCloudletOrg",
|
|
|
|
|
CloudletName: "TestCloudlet",
|
|
|
|
|
FlavorName: "small",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
Network: &config.NetworkConfig{
|
|
|
|
|
OutboundConnections: []config.OutboundConnection{
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlanNewDeployment(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
testConfig := createTestConfig(t)
|
|
|
|
|
|
|
|
|
|
// Mock API calls to return "not found" errors
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
result, err := planner.Plan(ctx, testConfig)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotNil(t, result)
|
|
|
|
|
require.NotNil(t, result.Plan)
|
|
|
|
|
require.NoError(t, result.Error)
|
|
|
|
|
|
|
|
|
|
plan := result.Plan
|
|
|
|
|
assert.Equal(t, "test-app", plan.ConfigName)
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
|
|
|
|
assert.Equal(t, "Application does not exist", plan.AppAction.Reason)
|
|
|
|
|
|
|
|
|
|
require.Len(t, plan.InstanceActions, 1)
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
|
|
|
|
|
assert.Equal(t, "Instance does not exist", plan.InstanceActions[0].Reason)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, 2, plan.TotalActions) // 1 app + 1 instance
|
|
|
|
|
assert.False(t, plan.IsEmpty())
|
|
|
|
|
|
|
|
|
|
mockClient.AssertExpectations(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlanExistingDeploymentNoChanges(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
testConfig := createTestConfig(t)
|
|
|
|
|
|
|
|
|
|
// Note: We would calculate expected manifest hash here when API supports it
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
// Mock existing app with same manifest hash and outbound connections
|
2025-10-07 15:40:27 +02:00
|
|
|
manifestContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n"
|
2025-10-20 13:34:22 +02:00
|
|
|
existingApp := &v2.App{
|
|
|
|
|
Key: v2.AppKey{
|
2025-09-29 16:36:21 +02:00
|
|
|
Organization: "testorg",
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
},
|
2025-10-07 17:09:36 +02:00
|
|
|
Deployment: "kubernetes",
|
2025-10-07 15:40:27 +02:00
|
|
|
DeploymentManifest: manifestContent,
|
2025-10-20 13:34:22 +02:00
|
|
|
RequiredOutboundConnections: []v2.SecurityRule{
|
2025-10-01 10:49:15 +02:00
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-09-29 16:36:21 +02:00
|
|
|
// Note: Manifest hash tracking would be implemented when API supports annotations
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Mock existing instance
|
2025-10-20 13:34:22 +02:00
|
|
|
existingInstance := &v2.AppInstance{
|
|
|
|
|
Key: v2.AppInstanceKey{
|
2025-09-29 16:36:21 +02:00
|
|
|
Organization: "testorg",
|
|
|
|
|
Name: "test-app-1.0.0-instance",
|
2025-10-20 13:34:22 +02:00
|
|
|
CloudletKey: v2.CloudletKey{
|
2025-09-29 16:36:21 +02:00
|
|
|
Organization: "TestCloudletOrg",
|
|
|
|
|
Name: "TestCloudlet",
|
|
|
|
|
},
|
|
|
|
|
},
|
2025-10-20 13:34:22 +02:00
|
|
|
AppKey: v2.AppKey{
|
2025-09-29 16:36:21 +02:00
|
|
|
Organization: "testorg",
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
},
|
2025-10-20 13:34:22 +02:00
|
|
|
Flavor: v2.Flavor{
|
2025-09-29 16:36:21 +02:00
|
|
|
Name: "small",
|
|
|
|
|
},
|
|
|
|
|
State: "Ready",
|
|
|
|
|
PowerState: "PowerOn",
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
2025-09-29 16:36:21 +02:00
|
|
|
Return(*existingApp, nil)
|
|
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
2025-09-29 16:36:21 +02:00
|
|
|
Return(*existingInstance, nil)
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
result, err := planner.Plan(ctx, testConfig)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotNil(t, result)
|
|
|
|
|
require.NotNil(t, result.Plan)
|
|
|
|
|
|
|
|
|
|
plan := result.Plan
|
|
|
|
|
assert.Equal(t, ActionNone, plan.AppAction.Type)
|
|
|
|
|
assert.Len(t, plan.InstanceActions, 1)
|
|
|
|
|
assert.Equal(t, ActionNone, plan.InstanceActions[0].Type)
|
|
|
|
|
assert.Equal(t, 0, plan.TotalActions)
|
|
|
|
|
assert.True(t, plan.IsEmpty())
|
|
|
|
|
assert.Contains(t, plan.Summary, "No changes required")
|
|
|
|
|
|
|
|
|
|
mockClient.AssertExpectations(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlanWithOptions(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
testConfig := createTestConfig(t)
|
|
|
|
|
|
|
|
|
|
opts := PlanOptions{
|
|
|
|
|
DryRun: true,
|
|
|
|
|
SkipStateCheck: true,
|
|
|
|
|
Timeout: 10 * time.Second,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
result, err := planner.PlanWithOptions(ctx, testConfig, opts)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotNil(t, result)
|
|
|
|
|
require.NotNil(t, result.Plan)
|
|
|
|
|
|
|
|
|
|
plan := result.Plan
|
|
|
|
|
assert.True(t, plan.DryRun)
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
|
|
|
|
assert.Contains(t, plan.AppAction.Reason, "state check skipped")
|
|
|
|
|
|
|
|
|
|
// No API calls should be made when SkipStateCheck is true
|
|
|
|
|
mockClient.AssertNotCalled(t, "ShowApp")
|
|
|
|
|
mockClient.AssertNotCalled(t, "ShowAppInstance")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlanMultipleInfrastructures(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
testConfig := createTestConfig(t)
|
|
|
|
|
|
|
|
|
|
// Add a second infrastructure target
|
|
|
|
|
testConfig.Spec.InfraTemplate = append(testConfig.Spec.InfraTemplate, config.InfraTemplate{
|
|
|
|
|
Region: "EU",
|
|
|
|
|
CloudletOrg: "EUCloudletOrg",
|
|
|
|
|
CloudletName: "EUCloudlet",
|
|
|
|
|
FlavorName: "medium",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Mock API calls to return "not found" errors
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"App not found"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "US").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowAppInstance", mock.Anything, mock.AnythingOfType("v2.AppInstanceKey"), "EU").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 404, Messages: []string{"Instance not found"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
result, err := planner.Plan(ctx, testConfig)
|
|
|
|
|
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.NotNil(t, result)
|
|
|
|
|
require.NotNil(t, result.Plan)
|
|
|
|
|
|
|
|
|
|
plan := result.Plan
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.AppAction.Type)
|
|
|
|
|
|
|
|
|
|
// Should have 2 instance actions, one for each infrastructure
|
|
|
|
|
require.Len(t, plan.InstanceActions, 2)
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.InstanceActions[0].Type)
|
|
|
|
|
assert.Equal(t, ActionCreate, plan.InstanceActions[1].Type)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, 3, plan.TotalActions) // 1 app + 2 instances
|
|
|
|
|
|
|
|
|
|
// Test cloudlet and region aggregation
|
|
|
|
|
cloudlets := plan.GetTargetCloudlets()
|
|
|
|
|
regions := plan.GetTargetRegions()
|
|
|
|
|
assert.Len(t, cloudlets, 2)
|
|
|
|
|
assert.Len(t, regions, 2)
|
|
|
|
|
|
|
|
|
|
mockClient.AssertExpectations(t)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCalculateManifestHash(t *testing.T) {
|
|
|
|
|
planner := &EdgeConnectPlanner{}
|
|
|
|
|
tempDir := t.TempDir()
|
|
|
|
|
|
|
|
|
|
// Create test file
|
|
|
|
|
testFile := filepath.Join(tempDir, "test.yaml")
|
|
|
|
|
content := "test content for hashing"
|
|
|
|
|
err := os.WriteFile(testFile, []byte(content), 0644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
hash1, err := planner.calculateManifestHash(testFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.NotEmpty(t, hash1)
|
|
|
|
|
assert.Len(t, hash1, 64) // SHA256 hex string length
|
|
|
|
|
|
|
|
|
|
// Same content should produce same hash
|
|
|
|
|
hash2, err := planner.calculateManifestHash(testFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, hash1, hash2)
|
|
|
|
|
|
|
|
|
|
// Different content should produce different hash
|
|
|
|
|
err = os.WriteFile(testFile, []byte("different content"), 0644)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
hash3, err := planner.calculateManifestHash(testFile)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.NotEqual(t, hash1, hash3)
|
|
|
|
|
|
|
|
|
|
// Empty file path should return empty hash
|
|
|
|
|
hash4, err := planner.calculateManifestHash("")
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Empty(t, hash4)
|
|
|
|
|
|
|
|
|
|
// Non-existent file should return error
|
|
|
|
|
_, err = planner.calculateManifestHash("/non/existent/file")
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestCompareAppStates(t *testing.T) {
|
|
|
|
|
planner := &EdgeConnectPlanner{}
|
|
|
|
|
|
|
|
|
|
current := &AppState{
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
AppType: AppTypeK8s,
|
|
|
|
|
ManifestHash: "old-hash",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
desired := &AppState{
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
AppType: AppTypeK8s,
|
|
|
|
|
ManifestHash: "new-hash",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes, manifestChanged := planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 1)
|
|
|
|
|
assert.True(t, manifestChanged)
|
|
|
|
|
assert.Contains(t, changes[0], "Manifest hash changed")
|
|
|
|
|
|
|
|
|
|
// Test no changes
|
|
|
|
|
desired.ManifestHash = "old-hash"
|
|
|
|
|
changes, manifestChanged = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Empty(t, changes)
|
|
|
|
|
assert.False(t, manifestChanged)
|
|
|
|
|
|
|
|
|
|
// Test app type change
|
|
|
|
|
desired.AppType = AppTypeDocker
|
|
|
|
|
changes, manifestChanged = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 1)
|
|
|
|
|
assert.False(t, manifestChanged)
|
|
|
|
|
assert.Contains(t, changes[0], "App type changed")
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-01 10:49:15 +02:00
|
|
|
func TestCompareAppStatesOutboundConnections(t *testing.T) {
|
|
|
|
|
planner := &EdgeConnectPlanner{}
|
|
|
|
|
|
|
|
|
|
// Test with no outbound connections
|
|
|
|
|
current := &AppState{
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
AppType: AppTypeK8s,
|
|
|
|
|
OutboundConnections: nil,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
desired := &AppState{
|
|
|
|
|
Name: "test-app",
|
|
|
|
|
Version: "1.0.0",
|
|
|
|
|
AppType: AppTypeK8s,
|
|
|
|
|
OutboundConnections: nil,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes, _ := planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Empty(t, changes, "No changes expected when both have no outbound connections")
|
|
|
|
|
|
|
|
|
|
// Test adding outbound connections
|
|
|
|
|
desired.OutboundConnections = []SecurityRule{
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes, _ = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 1)
|
|
|
|
|
assert.Contains(t, changes[0], "Outbound connections changed")
|
|
|
|
|
|
|
|
|
|
// Test identical outbound connections
|
|
|
|
|
current.OutboundConnections = []SecurityRule{
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes, _ = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Empty(t, changes, "No changes expected when outbound connections are identical")
|
|
|
|
|
|
|
|
|
|
// Test different outbound connections (different port)
|
|
|
|
|
desired.OutboundConnections[0].PortRangeMin = 443
|
|
|
|
|
desired.OutboundConnections[0].PortRangeMax = 443
|
|
|
|
|
|
|
|
|
|
changes, _ = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 1)
|
|
|
|
|
assert.Contains(t, changes[0], "Outbound connections changed")
|
|
|
|
|
|
|
|
|
|
// Test same connections but different order (should be considered equal)
|
|
|
|
|
current.OutboundConnections = []SecurityRule{
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 443,
|
|
|
|
|
PortRangeMax: 443,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
desired.OutboundConnections = []SecurityRule{
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 443,
|
|
|
|
|
PortRangeMax: 443,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Protocol: "tcp",
|
|
|
|
|
PortRangeMin: 80,
|
|
|
|
|
PortRangeMax: 80,
|
|
|
|
|
RemoteCIDR: "0.0.0.0/0",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes, _ = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Empty(t, changes, "No changes expected when outbound connections are same but in different order")
|
|
|
|
|
|
|
|
|
|
// Test removing outbound connections
|
|
|
|
|
desired.OutboundConnections = nil
|
|
|
|
|
|
|
|
|
|
changes, _ = planner.compareAppStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 1)
|
|
|
|
|
assert.Contains(t, changes[0], "Outbound connections changed")
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-29 16:36:21 +02:00
|
|
|
func TestCompareInstanceStates(t *testing.T) {
|
|
|
|
|
planner := &EdgeConnectPlanner{}
|
|
|
|
|
|
|
|
|
|
current := &InstanceState{
|
|
|
|
|
Name: "test-instance",
|
|
|
|
|
FlavorName: "small",
|
|
|
|
|
CloudletName: "oldcloudlet",
|
|
|
|
|
CloudletOrg: "oldorg",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
desired := &InstanceState{
|
|
|
|
|
Name: "test-instance",
|
|
|
|
|
FlavorName: "medium",
|
|
|
|
|
CloudletName: "newcloudlet",
|
|
|
|
|
CloudletOrg: "neworg",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes := planner.compareInstanceStates(current, desired)
|
|
|
|
|
assert.Len(t, changes, 3)
|
|
|
|
|
assert.Contains(t, changes[0], "Flavor changed")
|
|
|
|
|
assert.Contains(t, changes[1], "Cloudlet changed")
|
|
|
|
|
assert.Contains(t, changes[2], "Cloudlet org changed")
|
|
|
|
|
|
|
|
|
|
// Test no changes
|
|
|
|
|
desired.FlavorName = "small"
|
|
|
|
|
desired.CloudletName = "oldcloudlet"
|
|
|
|
|
desired.CloudletOrg = "oldorg"
|
|
|
|
|
changes = planner.compareInstanceStates(current, desired)
|
|
|
|
|
assert.Empty(t, changes)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestDeploymentPlanMethods(t *testing.T) {
|
|
|
|
|
plan := &DeploymentPlan{
|
|
|
|
|
ConfigName: "test-plan",
|
|
|
|
|
AppAction: AppAction{
|
2025-09-29 17:35:34 +02:00
|
|
|
Type: ActionCreate,
|
2025-09-29 16:36:21 +02:00
|
|
|
Desired: &AppState{Name: "test-app"},
|
|
|
|
|
},
|
|
|
|
|
InstanceActions: []InstanceAction{
|
|
|
|
|
{
|
|
|
|
|
Type: ActionCreate,
|
|
|
|
|
Target: config.InfraTemplate{
|
|
|
|
|
CloudletOrg: "org1",
|
|
|
|
|
CloudletName: "cloudlet1",
|
|
|
|
|
Region: "US",
|
|
|
|
|
},
|
|
|
|
|
InstanceName: "instance1",
|
|
|
|
|
Desired: &InstanceState{Name: "instance1"},
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
Type: ActionUpdate,
|
|
|
|
|
Target: config.InfraTemplate{
|
|
|
|
|
CloudletOrg: "org2",
|
|
|
|
|
CloudletName: "cloudlet2",
|
|
|
|
|
Region: "EU",
|
|
|
|
|
},
|
|
|
|
|
InstanceName: "instance2",
|
|
|
|
|
Desired: &InstanceState{Name: "instance2"},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Test IsEmpty
|
|
|
|
|
assert.False(t, plan.IsEmpty())
|
|
|
|
|
|
|
|
|
|
// Test GetTargetCloudlets
|
|
|
|
|
cloudlets := plan.GetTargetCloudlets()
|
|
|
|
|
assert.Len(t, cloudlets, 2)
|
|
|
|
|
assert.Contains(t, cloudlets, "org1:cloudlet1")
|
|
|
|
|
assert.Contains(t, cloudlets, "org2:cloudlet2")
|
|
|
|
|
|
|
|
|
|
// Test GetTargetRegions
|
|
|
|
|
regions := plan.GetTargetRegions()
|
|
|
|
|
assert.Len(t, regions, 2)
|
|
|
|
|
assert.Contains(t, regions, "US")
|
|
|
|
|
assert.Contains(t, regions, "EU")
|
|
|
|
|
|
|
|
|
|
// Test GenerateSummary
|
|
|
|
|
summary := plan.GenerateSummary()
|
|
|
|
|
assert.Contains(t, summary, "test-plan")
|
|
|
|
|
assert.Contains(t, summary, "CREATE application")
|
|
|
|
|
assert.Contains(t, summary, "CREATE 1 instance")
|
|
|
|
|
assert.Contains(t, summary, "UPDATE 1 instance")
|
|
|
|
|
|
|
|
|
|
// Test Validate
|
|
|
|
|
err := plan.Validate()
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
// Test validation failure
|
|
|
|
|
plan.AppAction.Desired = nil
|
|
|
|
|
err = plan.Validate()
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.Contains(t, err.Error(), "must have desired state")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestEstimateDeploymentDuration(t *testing.T) {
|
|
|
|
|
planner := &EdgeConnectPlanner{}
|
|
|
|
|
|
|
|
|
|
plan := &DeploymentPlan{
|
|
|
|
|
AppAction: AppAction{Type: ActionCreate},
|
|
|
|
|
InstanceActions: []InstanceAction{
|
|
|
|
|
{Type: ActionCreate},
|
|
|
|
|
{Type: ActionUpdate},
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
duration := planner.estimateDeploymentDuration(plan)
|
|
|
|
|
assert.Greater(t, duration, time.Duration(0))
|
|
|
|
|
assert.Less(t, duration, 10*time.Minute) // Reasonable upper bound
|
|
|
|
|
|
|
|
|
|
// Test with no actions
|
|
|
|
|
emptyPlan := &DeploymentPlan{
|
|
|
|
|
AppAction: AppAction{Type: ActionNone},
|
|
|
|
|
InstanceActions: []InstanceAction{},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
emptyDuration := planner.estimateDeploymentDuration(emptyPlan)
|
|
|
|
|
assert.Greater(t, emptyDuration, time.Duration(0))
|
|
|
|
|
assert.Less(t, emptyDuration, duration) // Should be less than plan with actions
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestIsResourceNotFoundError(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
err error
|
|
|
|
|
expected bool
|
|
|
|
|
}{
|
|
|
|
|
{"nil error", nil, false},
|
2025-10-20 13:34:22 +02:00
|
|
|
{"not found error", &v2.APIError{StatusCode: 404, Messages: []string{"Resource not found"}}, true},
|
|
|
|
|
{"does not exist error", &v2.APIError{Messages: []string{"App does not exist"}}, true},
|
|
|
|
|
{"404 in message", &v2.APIError{Messages: []string{"HTTP 404 error"}}, true},
|
|
|
|
|
{"other error", &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}}, false},
|
2025-09-29 16:36:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
result := isResourceNotFoundError(tt.err)
|
|
|
|
|
assert.Equal(t, tt.expected, result)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPlanErrorHandling(t *testing.T) {
|
|
|
|
|
mockClient := &MockEdgeConnectClient{}
|
|
|
|
|
planner := NewPlanner(mockClient)
|
|
|
|
|
testConfig := createTestConfig(t)
|
|
|
|
|
|
|
|
|
|
// Mock API call to return a non-404 error
|
2025-10-20 13:34:22 +02:00
|
|
|
mockClient.On("ShowApp", mock.Anything, mock.AnythingOfType("v2.AppKey"), "US").
|
|
|
|
|
Return(nil, &v2.APIError{StatusCode: 500, Messages: []string{"Server error"}})
|
2025-09-29 16:36:21 +02:00
|
|
|
|
|
|
|
|
ctx := context.Background()
|
|
|
|
|
result, err := planner.Plan(ctx, testConfig)
|
|
|
|
|
|
|
|
|
|
assert.Error(t, err)
|
|
|
|
|
assert.NotNil(t, result)
|
|
|
|
|
assert.NotNil(t, result.Error)
|
|
|
|
|
assert.Contains(t, err.Error(), "failed to query current app state")
|
|
|
|
|
|
|
|
|
|
mockClient.AssertExpectations(t)
|
2025-09-29 17:35:34 +02:00
|
|
|
}
|