// ABOUTME: Comprehensive tests for EdgeConnect resource manager with deployment scenarios // ABOUTME: Tests deployment execution, rollback functionality, and error handling with mock clients package apply import ( "context" "fmt" "os" "path/filepath" "testing" "time" "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/edgeconnect" "edp.buildth.ing/DevFW-CICD/edge-connect-client/sdk/config" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) // MockResourceClient extends MockEdgeConnectClient with resource management methods type MockResourceClient struct { MockEdgeConnectClient } func (m *MockResourceClient) CreateApp(ctx context.Context, input *edgeconnect.NewAppInput) error { args := m.Called(ctx, input) return args.Error(0) } func (m *MockResourceClient) CreateAppInstance(ctx context.Context, input *edgeconnect.NewAppInstanceInput) error { args := m.Called(ctx, input) return args.Error(0) } func (m *MockResourceClient) DeleteApp(ctx context.Context, appKey edgeconnect.AppKey, region string) error { args := m.Called(ctx, appKey, region) return args.Error(0) } func (m *MockResourceClient) DeleteAppInstance(ctx context.Context, instanceKey edgeconnect.AppInstanceKey, region string) error { args := m.Called(ctx, instanceKey, region) return args.Error(0) } // TestLogger implements Logger interface for testing type TestLogger struct { messages []string } func (l *TestLogger) Printf(format string, v ...interface{}) { l.messages = append(l.messages, fmt.Sprintf(format, v...)) } func TestNewResourceManager(t *testing.T) { mockClient := &MockResourceClient{} manager := NewResourceManager(mockClient) assert.NotNil(t, manager) assert.IsType(t, &EdgeConnectResourceManager{}, manager) } func TestDefaultResourceManagerOptions(t *testing.T) { opts := DefaultResourceManagerOptions() assert.Equal(t, 5, opts.ParallelLimit) assert.True(t, opts.RollbackOnFail) assert.Equal(t, 2*time.Minute, opts.OperationTimeout) } func TestWithOptions(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithParallelLimit(10), WithRollbackOnFail(false), WithLogger(logger), ) // Cast to implementation to check options were applied impl := manager.(*EdgeConnectResourceManager) assert.Equal(t, 10, impl.parallelLimit) assert.False(t, impl.rollbackOnFail) assert.Equal(t, logger, impl.logger) } func createTestDeploymentPlan() *DeploymentPlan { return &DeploymentPlan{ ConfigName: "test-deployment", AppAction: AppAction{ Type: ActionCreate, Desired: &AppState{ Name: "test-app", Version: "1.0.0", Organization: "testorg", Region: "US", }, }, InstanceActions: []InstanceAction{ { Type: ActionCreate, Target: config.InfraTemplate{ Organization: "testorg", Region: "US", CloudletOrg: "cloudletorg", CloudletName: "cloudlet1", FlavorName: "small", }, Desired: &InstanceState{ Name: "test-app-1.0.0-instance", AppName: "test-app", }, InstanceName: "test-app-1.0.0-instance", }, }, } } func createTestManagerConfig(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{ Name: "test-app", }, Spec: config.Spec{ K8sApp: &config.K8sApp{ AppName: "test-app", AppVersion: "1.0.0", ManifestFile: manifestFile, }, InfraTemplate: []config.InfraTemplate{ { Organization: "testorg", Region: "US", CloudletOrg: "cloudletorg", CloudletName: "cloudlet1", FlavorName: "small", }, }, Network: &config.NetworkConfig{ OutboundConnections: []config.OutboundConnection{ { Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, }, }, }, } } func TestApplyDeploymentSuccess(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithLogger(logger)) plan := createTestDeploymentPlan() config := createTestManagerConfig(t) // Mock successful operations mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")). Return(nil) mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")). Return(nil) ctx := context.Background() result, err := manager.ApplyDeployment(ctx, plan, config) require.NoError(t, err) require.NotNil(t, result) assert.True(t, result.Success) assert.Len(t, result.CompletedActions, 2) // 1 app + 1 instance assert.Len(t, result.FailedActions, 0) assert.False(t, result.RollbackPerformed) assert.Greater(t, result.Duration, time.Duration(0)) // Check that operations were logged assert.Greater(t, len(logger.messages), 0) mockClient.AssertExpectations(t) } func TestApplyDeploymentAppFailure(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithLogger(logger)) plan := createTestDeploymentPlan() config := createTestManagerConfig(t) // Mock app creation failure mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")). Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Server error"}}) ctx := context.Background() result, err := manager.ApplyDeployment(ctx, plan, config) require.Error(t, err) require.NotNil(t, result) assert.False(t, result.Success) assert.Len(t, result.CompletedActions, 0) assert.Len(t, result.FailedActions, 1) assert.Contains(t, err.Error(), "failed to create application") mockClient.AssertExpectations(t) } func TestApplyDeploymentInstanceFailureWithRollback(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithLogger(logger), WithRollbackOnFail(true)) plan := createTestDeploymentPlan() config := createTestManagerConfig(t) // Mock successful app creation but failed instance creation mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")). Return(nil) mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")). Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Instance creation failed"}}) // Mock rollback operations mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US"). Return(nil) ctx := context.Background() result, err := manager.ApplyDeployment(ctx, plan, config) require.Error(t, err) require.NotNil(t, result) assert.False(t, result.Success) assert.Len(t, result.CompletedActions, 1) // App was created assert.Len(t, result.FailedActions, 1) // Instance failed assert.True(t, result.RollbackPerformed) assert.True(t, result.RollbackSuccess) assert.Contains(t, err.Error(), "instance actions failed") mockClient.AssertExpectations(t) } func TestApplyDeploymentNoActions(t *testing.T) { mockClient := &MockResourceClient{} manager := NewResourceManager(mockClient) // Create empty plan plan := &DeploymentPlan{ ConfigName: "empty-plan", AppAction: AppAction{Type: ActionNone}, } config := createTestManagerConfig(t) ctx := context.Background() result, err := manager.ApplyDeployment(ctx, plan, config) require.Error(t, err) require.NotNil(t, result) assert.Contains(t, err.Error(), "deployment plan is empty") mockClient.AssertNotCalled(t, "CreateApp") mockClient.AssertNotCalled(t, "CreateAppInstance") } func TestApplyDeploymentMultipleInstances(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithLogger(logger), WithParallelLimit(2)) // Create plan with multiple instances plan := &DeploymentPlan{ ConfigName: "multi-instance", AppAction: AppAction{ Type: ActionCreate, Desired: &AppState{ Name: "test-app", Version: "1.0.0", Organization: "testorg", Region: "US", }, }, InstanceActions: []InstanceAction{ { Type: ActionCreate, Target: config.InfraTemplate{ Organization: "testorg", Region: "US", CloudletOrg: "cloudletorg1", CloudletName: "cloudlet1", FlavorName: "small", }, Desired: &InstanceState{Name: "instance1"}, InstanceName: "instance1", }, { Type: ActionCreate, Target: config.InfraTemplate{ Organization: "testorg", Region: "EU", CloudletOrg: "cloudletorg2", CloudletName: "cloudlet2", FlavorName: "medium", }, Desired: &InstanceState{Name: "instance2"}, InstanceName: "instance2", }, }, } config := createTestManagerConfig(t) // Mock successful operations mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")). Return(nil) mockClient.On("CreateAppInstance", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInstanceInput")). Return(nil) ctx := context.Background() result, err := manager.ApplyDeployment(ctx, plan, config) require.NoError(t, err) require.NotNil(t, result) assert.True(t, result.Success) assert.Len(t, result.CompletedActions, 3) // 1 app + 2 instances assert.Len(t, result.FailedActions, 0) mockClient.AssertExpectations(t) } func TestValidatePrerequisites(t *testing.T) { mockClient := &MockResourceClient{} manager := NewResourceManager(mockClient) tests := []struct { name string plan *DeploymentPlan wantErr bool errMsg string }{ { name: "valid plan", plan: &DeploymentPlan{ ConfigName: "test", AppAction: AppAction{Type: ActionCreate, Desired: &AppState{}}, }, wantErr: false, }, { name: "empty plan", plan: &DeploymentPlan{ ConfigName: "test", AppAction: AppAction{Type: ActionNone}, }, wantErr: true, errMsg: "deployment plan is empty", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() err := manager.ValidatePrerequisites(ctx, tt.plan) if tt.wantErr { assert.Error(t, err) if tt.errMsg != "" { assert.Contains(t, err.Error(), tt.errMsg) } } else { assert.NoError(t, err) } }) } } func TestRollbackDeployment(t *testing.T) { mockClient := &MockResourceClient{} logger := &TestLogger{} manager := NewResourceManager(mockClient, WithLogger(logger)) // Create result with completed actions plan := createTestDeploymentPlan() result := &ExecutionResult{ Plan: plan, CompletedActions: []ActionResult{ { Type: ActionCreate, Target: "test-app", Success: true, }, { Type: ActionCreate, Target: "test-app-1.0.0-instance", Success: true, }, }, FailedActions: []ActionResult{}, } // Mock rollback operations mockClient.On("DeleteAppInstance", mock.Anything, mock.AnythingOfType("edgeconnect.AppInstanceKey"), "US"). Return(nil) mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US"). Return(nil) ctx := context.Background() err := manager.RollbackDeployment(ctx, result) require.NoError(t, err) mockClient.AssertExpectations(t) // Check rollback was logged assert.Greater(t, len(logger.messages), 0) } func TestRollbackDeploymentFailure(t *testing.T) { mockClient := &MockResourceClient{} manager := NewResourceManager(mockClient) plan := createTestDeploymentPlan() result := &ExecutionResult{ Plan: plan, CompletedActions: []ActionResult{ { Type: ActionCreate, Target: "test-app", Success: true, }, }, } // Mock rollback failure mockClient.On("DeleteApp", mock.Anything, mock.AnythingOfType("edgeconnect.AppKey"), "US"). Return(&edgeconnect.APIError{StatusCode: 500, Messages: []string{"Delete failed"}}) ctx := context.Background() err := manager.RollbackDeployment(ctx, result) require.Error(t, err) assert.Contains(t, err.Error(), "rollback encountered") mockClient.AssertExpectations(t) } func TestReadManifestFile(t *testing.T) { manager := &EdgeConnectResourceManager{} tempDir := t.TempDir() // Create test file testFile := filepath.Join(tempDir, "test.yaml") expectedContent := "apiVersion: v1\nkind: Pod\nmetadata:\n name: test\n" err := os.WriteFile(testFile, []byte(expectedContent), 0644) require.NoError(t, err) content, err := manager.readManifestFile(testFile) require.NoError(t, err) assert.Equal(t, expectedContent, content) // Test empty path content, err = manager.readManifestFile("") require.NoError(t, err) assert.Empty(t, content) // Test non-existent file _, err = manager.readManifestFile("/non/existent/file") assert.Error(t, err) assert.Contains(t, err.Error(), "failed to open manifest file") } func TestGetDeploymentType(t *testing.T) { manager := &EdgeConnectResourceManager{} // Test k8s app k8sConfig := &config.EdgeConnectConfig{ Spec: config.Spec{ K8sApp: &config.K8sApp{}, }, } assert.Equal(t, "kubernetes", manager.getDeploymentType(k8sConfig)) // Test docker app dockerConfig := &config.EdgeConnectConfig{ Spec: config.Spec{ DockerApp: &config.DockerApp{}, }, } assert.Equal(t, "docker", manager.getDeploymentType(dockerConfig)) } func TestGetImagePath(t *testing.T) { manager := &EdgeConnectResourceManager{} // Test docker app with image dockerConfig := &config.EdgeConnectConfig{ Spec: config.Spec{ DockerApp: &config.DockerApp{ Image: "my-custom-image:latest", }, }, } assert.Equal(t, "my-custom-image:latest", manager.getImagePath(dockerConfig)) // Test k8s app (should use default) k8sConfig := &config.EdgeConnectConfig{ Spec: config.Spec{ K8sApp: &config.K8sApp{}, }, } assert.Equal(t, "https://registry-1.docker.io/library/nginx:latest", manager.getImagePath(k8sConfig)) } func TestConvertNetworkRules(t *testing.T) { manager := &EdgeConnectResourceManager{} network := &config.NetworkConfig{ OutboundConnections: []config.OutboundConnection{ { Protocol: "tcp", PortRangeMin: 80, PortRangeMax: 80, RemoteCIDR: "0.0.0.0/0", }, { Protocol: "tcp", PortRangeMin: 443, PortRangeMax: 443, RemoteCIDR: "10.0.0.0/8", }, }, } rules := manager.convertNetworkRules(network) require.Len(t, rules, 2) assert.Equal(t, "tcp", rules[0].Protocol) assert.Equal(t, 80, rules[0].PortRangeMin) assert.Equal(t, 80, rules[0].PortRangeMax) assert.Equal(t, "0.0.0.0/0", rules[0].RemoteCIDR) assert.Equal(t, "tcp", rules[1].Protocol) assert.Equal(t, 443, rules[1].PortRangeMin) assert.Equal(t, 443, rules[1].PortRangeMax) assert.Equal(t, "10.0.0.0/8", rules[1].RemoteCIDR) } func TestCreateApplicationInput(t *testing.T) { mockClient := &MockResourceClient{} manager := NewResourceManager(mockClient) config := createTestManagerConfig(t) action := AppAction{ Type: ActionCreate, Desired: &AppState{ Name: "test-app", Version: "1.0.0", Organization: "testorg", Region: "US", }, } // Capture the input passed to CreateApp var capturedInput *edgeconnect.NewAppInput mockClient.On("CreateApp", mock.Anything, mock.AnythingOfType("*edgeconnect.NewAppInput")). Run(func(args mock.Arguments) { capturedInput = args.Get(1).(*edgeconnect.NewAppInput) }). Return(nil) ctx := context.Background() success, err := manager.(*EdgeConnectResourceManager).createApplication(ctx, action, config) require.NoError(t, err) assert.True(t, success) require.NotNil(t, capturedInput) // Verify the input was constructed correctly assert.Equal(t, "US", capturedInput.Region) assert.Equal(t, "testorg", capturedInput.App.Key.Organization) assert.Equal(t, "test-app", capturedInput.App.Key.Name) assert.Equal(t, "1.0.0", capturedInput.App.Key.Version) assert.Equal(t, "kubernetes", capturedInput.App.Deployment) assert.Equal(t, "ImageTypeDocker", capturedInput.App.ImageType) assert.True(t, capturedInput.App.AllowServerless) assert.NotEmpty(t, capturedInput.App.DeploymentManifest) assert.Len(t, capturedInput.App.RequiredOutboundConnections, 1) mockClient.AssertExpectations(t) }