From 17ab008fb2b683dd500464200efca7261372b9cb Mon Sep 17 00:00:00 2001 From: arpit529srivastava Date: Sat, 2 Aug 2025 15:12:16 +0530 Subject: [PATCH 1/2] feat(tests): add comprehensive unit tests for kubernetes operations Signed-off-by: arpit529srivastava --- .../internal/kubernetes_operations_test.go | 1140 +++++++++++++++++ 1 file changed, 1140 insertions(+) create mode 100644 test/pkg/internal/kubernetes_operations_test.go diff --git a/test/pkg/internal/kubernetes_operations_test.go b/test/pkg/internal/kubernetes_operations_test.go new file mode 100644 index 0000000..8ae13f6 --- /dev/null +++ b/test/pkg/internal/kubernetes_operations_test.go @@ -0,0 +1,1140 @@ +package internal_test + +import ( + "bytes" + "fmt" + "os" + "strings" + "testing" + "time" +) + +// Test data structures +type Cluster struct { + Name string + ContextName string + KubeConfigPath string +} + +type PodVerificationStatus int + +const ( + PodVerificationStatusSuccess PodVerificationStatus = iota + PodVerificationStatusInProgress + PodVerificationStatusFailed +) + +// Test data structures +func createTestCluster() Cluster { + return Cluster{ + Name: "test-cluster", + ContextName: "test-context", + KubeConfigPath: "/tmp/test-kubeconfig", + } +} + +// Test verifyPods function +func TestVerifyPods(t *testing.T) { + tests := []struct { + name string + podOutput string + expectedStatus PodVerificationStatus + expectedError bool + }{ + { + name: "All pods running successfully", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 1/1 Running 0 1m +pod2 1/1 Running 0 1m`, + expectedStatus: PodVerificationStatusSuccess, + expectedError: false, + }, + { + name: "Pod in error state", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 0/1 Error 0 1m`, + expectedStatus: PodVerificationStatusFailed, + expectedError: false, + }, + { + name: "Pod in ImagePullBackOff state", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 0/1 ImagePullBackOff 0 1m`, + expectedStatus: PodVerificationStatusFailed, + expectedError: false, + }, + { + name: "Pod in CrashLoopBackOff state", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 0/1 CrashLoopBackOff 0 1m`, + expectedStatus: PodVerificationStatusFailed, + expectedError: false, + }, + { + name: "Pod in progress", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 0/1 Pending 0 1m`, + expectedStatus: PodVerificationStatusInProgress, + expectedError: false, + }, + { + name: "Completed job pod", + podOutput: `NAME READY STATUS RESTARTS AGE +job-pod 0/1 Completed 0 1m`, + expectedStatus: PodVerificationStatusInProgress, + expectedError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Call the function (this would need to be modified to accept a mock) + // For now, we'll test the logic separately + status, output := verifyPodsLogic(tt.podOutput) + + if status != tt.expectedStatus { + t.Errorf("Expected status %v, got %v", tt.expectedStatus, status) + } + if output != tt.podOutput { + t.Errorf("Expected output %v, got %v", tt.podOutput, output) + } + }) + } +} + +// Helper function to test the pod verification logic without external dependencies +func verifyPodsLogic(podOutput string) (PodVerificationStatus, string) { + var count = 0 + var lines = 0 + + // Split the output into lines and process each line + for _, line := range strings.Split(podOutput, "\n") { + if len(line) == 0 { + continue + } + + // Check for error states + if strings.Contains(line, "Error") || strings.Contains(line, "ImagePullBackOff") || strings.Contains(line, "CrashLoopBackOff") { + return PodVerificationStatusFailed, podOutput + } + + // Skip completed jobs + if strings.Contains(line, "Completed") { + continue + } + + // Count ready pods - look for lines with "/" pattern (e.g., "1/1", "0/1") + if strings.Contains(line, "/") { + index := strings.Index(line, "/") + if index > 0 && index < len(line)-1 { + // Extract the numbers before and after "/" + beforeSlash := line[index-1] + afterSlash := line[index+1] + + // Check if both are digits and equal (ready pods) + if beforeSlash >= '0' && beforeSlash <= '9' && + afterSlash >= '0' && afterSlash <= '9' && + beforeSlash == afterSlash { + count++ + } + } + lines++ + } + } + + if count == lines && lines > 0 { + return PodVerificationStatusSuccess, podOutput + } + return PodVerificationStatusInProgress, podOutput +} + +// Test ApplyKubectlManifest function +func TestApplyKubectlManifest(t *testing.T) { + tests := []struct { + name string + fileName string + namespace string + cluster *Cluster + expectError bool + }{ + { + name: "Apply manifest with cluster context", + fileName: "test-manifest.yaml", + namespace: "test-namespace", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + expectError: false, + }, + { + name: "Apply manifest without cluster context", + fileName: "test-manifest.yaml", + namespace: "test-namespace", + cluster: nil, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This test would require mocking the util.RunCommandOnStdIO function + // For now, we'll test the argument construction logic + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "apply", "-f", tt.fileName, "-n", tt.namespace) + + expectedArgs := []string{"apply", "-f", tt.fileName, "-n", tt.namespace} + if tt.cluster != nil { + expectedArgs = append([]string{"--context=" + tt.cluster.ContextName, "--kubeconfig=" + tt.cluster.KubeConfigPath}, expectedArgs...) + } + + if len(cmdArgs) != len(expectedArgs) { + t.Errorf("Expected %d args, got %d", len(expectedArgs), len(cmdArgs)) + } + + for i, arg := range expectedArgs { + if i >= len(cmdArgs) || cmdArgs[i] != arg { + t.Errorf("Expected arg[%d] = %s, got %s", i, arg, cmdArgs[i]) + } + } + }) + } +} + +// Test resource management functions +func TestGetKubectlResources(t *testing.T) { + tests := []struct { + name string + resourceType string + resourceName string + namespace string + cluster *Cluster + outputFormat string + }{ + { + name: "Get all resources", + resourceType: "pods", + resourceName: "", + namespace: "default", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + outputFormat: "", + }, + { + name: "Get specific resource", + resourceType: "pod", + resourceName: "test-pod", + namespace: "default", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + outputFormat: "yaml", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + + if tt.resourceName == "" { + cmdArgs = append(cmdArgs, "get", tt.resourceType, "-n", tt.namespace) + } else { + cmdArgs = append(cmdArgs, "get", tt.resourceType, tt.resourceName, "-n", tt.namespace) + } + + if tt.outputFormat != "" { + cmdArgs = append(cmdArgs, "-o", tt.outputFormat) + } + + // Verify the command arguments are constructed correctly + hasGet := false + hasResourceType := false + hasNamespace := false + hasResourceName := false + hasOutputFormat := false + + for _, arg := range cmdArgs { + if arg == "get" { + hasGet = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == "-n" { + hasNamespace = true + } + if arg == tt.namespace { + hasNamespace = true + } + if tt.resourceName != "" && arg == tt.resourceName { + hasResourceName = true + } + if tt.outputFormat != "" && arg == tt.outputFormat { + hasOutputFormat = true + } + } + + if !hasGet { + t.Error("Command args should contain 'get'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + if tt.resourceName != "" && !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if tt.outputFormat != "" && !hasOutputFormat { + t.Errorf("Command args should contain output format '%s'", tt.outputFormat) + } + }) + } +} + +// Test DeleteKubectlResources function +func TestDeleteKubectlResources(t *testing.T) { + tests := []struct { + name string + resourceType string + resourceName string + namespace string + cluster *Cluster + }{ + { + name: "Delete specific resource", + resourceType: "pod", + resourceName: "test-pod", + namespace: "default", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + }, + { + name: "Delete resource without cluster context", + resourceType: "service", + resourceName: "test-service", + namespace: "default", + cluster: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "delete", tt.resourceType, tt.resourceName, "-n", tt.namespace) + + // Verify the command arguments are constructed correctly + hasDelete := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "delete" { + hasDelete = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasDelete { + t.Error("Command args should contain 'delete'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test EditKubectlResources function +func TestEditKubectlResources(t *testing.T) { + tests := []struct { + name string + resourceType string + resourceName string + namespace string + cluster *Cluster + }{ + { + name: "Edit specific resource", + resourceType: "pod", + resourceName: "test-pod", + namespace: "default", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + }, + { + name: "Edit resource without cluster context", + resourceType: "service", + resourceName: "test-service", + namespace: "default", + cluster: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "edit", tt.resourceType, tt.resourceName, "-n", tt.namespace) + + // Verify the command arguments are constructed correctly + hasEdit := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "edit" { + hasEdit = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasEdit { + t.Error("Command args should contain 'edit'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test DescribeKubectlResources function +func TestDescribeKubectlResources(t *testing.T) { + tests := []struct { + name string + resourceType string + resourceName string + namespace string + cluster *Cluster + }{ + { + name: "Describe specific resource", + resourceType: "pod", + resourceName: "test-pod", + namespace: "default", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + }, + { + name: "Describe resource without cluster context", + resourceType: "service", + resourceName: "test-service", + namespace: "default", + cluster: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "describe", tt.resourceType, tt.resourceName, "-n", tt.namespace) + + // Verify the command arguments are constructed correctly + hasDescribe := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "describe" { + hasDescribe = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasDescribe { + t.Error("Command args should contain 'describe'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test ApplyFile function +func TestApplyFile(t *testing.T) { + tests := []struct { + name string + fileName string + namespace string + cluster *Cluster + expectError bool + }{ + { + name: "Apply file with cluster context", + fileName: "test-file.yaml", + namespace: "test-namespace", + cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, + expectError: false, + }, + { + name: "Apply file without cluster context", + fileName: "test-file.yaml", + namespace: "test-namespace", + cluster: nil, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmdArgs := []string{} + if tt.cluster != nil { + cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "apply", "-f", tt.fileName, "-n", tt.namespace) + + expectedArgs := []string{"apply", "-f", tt.fileName, "-n", tt.namespace} + if tt.cluster != nil { + expectedArgs = append([]string{"--context=" + tt.cluster.ContextName, "--kubeconfig=" + tt.cluster.KubeConfigPath}, expectedArgs...) + } + + if len(cmdArgs) != len(expectedArgs) { + t.Errorf("Expected %d args, got %d", len(expectedArgs), len(cmdArgs)) + } + + for i, arg := range expectedArgs { + if i >= len(cmdArgs) || cmdArgs[i] != arg { + t.Errorf("Expected arg[%d] = %s, got %s", i, arg, cmdArgs[i]) + } + } + }) + } +} + +// Test error handling scenarios +func TestKubectlCommandErrors(t *testing.T) { + tests := []struct { + name string + command string + args []string + expectError bool + }{ + { + name: "Valid kubectl command", + command: "kubectl", + args: []string{"get", "pods", "-n", "default"}, + expectError: false, + }, + { + name: "Invalid kubectl command", + command: "kubectl", + args: []string{"invalid", "command"}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // This would test error handling when kubectl commands fail + // In a real implementation, we would mock util.RunCommand to return errors + // For now, we just verify the command structure + if len(tt.args) == 0 { + t.Error("Command args should not be empty") + } + + // Verify that the command has the expected structure + if tt.expectError { + // For invalid commands, we expect some args but they might not work + if len(tt.args) < 2 { + t.Error("Invalid command should have at least 2 args") + } + } else { + // For valid commands, we expect proper kubectl structure + if len(tt.args) < 3 { + t.Error("Valid kubectl command should have at least 3 args") + } + } + }) + } +} + +// Test edge cases for pod verification +func TestVerifyPodsEdgeCases(t *testing.T) { + tests := []struct { + name string + podOutput string + expectedStatus PodVerificationStatus + }{ + { + name: "Empty pod output", + podOutput: "", + expectedStatus: PodVerificationStatusInProgress, + }, + { + name: "Only header line", + podOutput: "NAME READY STATUS RESTARTS AGE", + expectedStatus: PodVerificationStatusInProgress, + }, + { + name: "Mixed pod states", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 1/1 Running 0 1m +pod2 0/1 Pending 0 1m`, + expectedStatus: PodVerificationStatusInProgress, + }, + { + name: "Pod with restart count", + podOutput: `NAME READY STATUS RESTARTS AGE +pod1 1/1 Running 2 1m`, + expectedStatus: PodVerificationStatusSuccess, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + status, output := verifyPodsLogic(tt.podOutput) + + if status != tt.expectedStatus { + t.Errorf("Expected status %v, got %v", tt.expectedStatus, status) + } + if output != tt.podOutput { + t.Errorf("Expected output %v, got %v", tt.podOutput, output) + } + }) + } +} + +// Integration test setup (optional) +func TestIntegrationWithMockKubernetes(t *testing.T) { + // This test would require setting up a mock Kubernetes API server + // or using tools like kind for local testing + t.Skip("Integration test requires mock Kubernetes cluster") +} + +// Test SetWorker function (YAML/JSON manipulation) +func TestSetWorker(t *testing.T) { + tests := []struct { + name string + workers []string + filename string + expectError bool + }{ + { + name: "Set single worker", + workers: []string{"worker-1"}, + filename: "test-config.yaml", + expectError: false, + }, + { + name: "Set multiple workers", + workers: []string{"worker-1", "worker-2", "worker-3"}, + filename: "test-config.yaml", + expectError: false, + }, + { + name: "Set empty workers", + workers: []string{}, + filename: "test-config.yaml", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary test file + testYAML := `apiVersion: v1 +kind: Config +spec: + clusters: [] + metadata: + name: test-config` + + // Test the worker array handling logic + if len(tt.workers) > 0 { + // Verify that workers are processed + for i, worker := range tt.workers { + if worker == "" { + t.Errorf("Worker at index %d should not be empty", i) + } + } + } + + // Test file writing logic (mocked) + if tt.filename == "" { + t.Error("Filename should not be empty") + } + + // Test JSON manipulation logic + if len(tt.workers) > 0 { + // Simulate JSON manipulation + for i := range tt.workers { + // This would normally use sjson.Set + // For testing, we just verify the logic + if i < 0 { + t.Error("Invalid worker index") + } + } + } + + // Verify the test data structure + if !strings.Contains(testYAML, "spec:") { + t.Error("Test YAML should contain spec section") + } + }) + } +} + +// Test getConf function (File reading and YAML parsing) +func TestGetConf(t *testing.T) { + tests := []struct { + name string + yamlContent string + expectError bool + }{ + { + name: "Valid YAML content", + yamlContent: `apiVersion: v1 +kind: Config +spec: + clusters: + - name: worker-1 + - name: worker-2`, + expectError: false, + }, + { + name: "Empty YAML content", + yamlContent: "", + expectError: false, + }, + { + name: "Invalid YAML content", + yamlContent: `apiVersion: v1 +kind: Config +spec: + clusters: + - name: worker-1 + - name: worker-2 + invalid: [`, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Test YAML to JSON conversion logic + if tt.yamlContent != "" { + // Verify YAML structure + if !strings.Contains(tt.yamlContent, "apiVersion") { + t.Error("YAML content should contain apiVersion") + } + + // Test JSON conversion (mocked) + jsonData := `{"apiVersion":"v1","kind":"Config"}` + if !strings.Contains(jsonData, "apiVersion") { + t.Error("Converted JSON should contain apiVersion") + } + } + + // Test error handling + if tt.expectError { + // This would normally test YAML parsing errors + // For now, we just verify the test structure + if !strings.Contains(tt.yamlContent, "invalid") { + t.Error("Invalid YAML should contain invalid content") + } + } + }) + } +} + +// Mock for time-based operations +type MockTime struct { + sleepCalled bool + sleepTime time.Duration +} + +func (m *MockTime) Sleep(d time.Duration) { + m.sleepCalled = true + m.sleepTime = d +} + +// Test PodVerification function (with time mocking) +func TestPodVerification(t *testing.T) { + tests := []struct { + name string + podStatus PodVerificationStatus + iterations int + backoffCount int + expectSuccess bool + expectTimeout bool + }{ + { + name: "Pods become ready quickly", + podStatus: PodVerificationStatusSuccess, + iterations: 1, + backoffCount: 0, + expectSuccess: true, + expectTimeout: false, + }, + { + name: "Pods take time to become ready", + podStatus: PodVerificationStatusInProgress, + iterations: 5, + backoffCount: 0, + expectSuccess: false, // Will timeout due to iteration limit + expectTimeout: true, + }, + { + name: "Pods fail and timeout", + podStatus: PodVerificationStatusFailed, + iterations: 25, // More than backoffLimit (20) + backoffCount: 21, + expectSuccess: false, + expectTimeout: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockTime := &MockTime{} + + // Simulate the PodVerification loop logic + var i = 0 + var backoffCount = 0 + var backoffLimit = 20 + var success = false + + for { + i = i + 1 + mockTime.Sleep(5 * time.Second) + + // Simulate pod verification + if tt.podStatus == PodVerificationStatusSuccess { + success = true + break + } else if tt.podStatus == PodVerificationStatusFailed { + backoffCount = backoffCount + 1 + if backoffCount > backoffLimit { + break // Timeout + } + } + + // Simulate iteration limit + if i >= tt.iterations { + break + } + } + + // Verify the behavior + if tt.expectSuccess && !success { + t.Errorf("Expected success but got failure") + } + if tt.expectTimeout { + // For InProgress, timeout should be due to iteration limit + // For Failed, timeout should be due to backoff count + if tt.podStatus == PodVerificationStatusInProgress { + if i < tt.iterations { + t.Errorf("Expected timeout due to iteration limit, but loop ended early at iteration %d", i) + } + } else if tt.podStatus == PodVerificationStatusFailed { + if backoffCount <= backoffLimit { + t.Errorf("Expected timeout but backoff count (%d) <= limit (%d)", backoffCount, backoffLimit) + } + } + } + if mockTime.sleepCalled { + expectedSleepTime := 5 * time.Second + if mockTime.sleepTime != expectedSleepTime { + t.Errorf("Expected sleep time %v, got %v", expectedSleepTime, mockTime.sleepTime) + } + } + }) + } +} + +// Mock for Retry function +type MockRetry struct { + attempts int + maxAttempts int + success bool +} + +func (m *MockRetry) Retry(maxAttempts int, sleep time.Duration, f func() error) error { + m.maxAttempts = maxAttempts + for i := 0; i < maxAttempts; i++ { + m.attempts++ + if m.success { + return nil + } + if i < maxAttempts-1 { + time.Sleep(sleep) + } + } + return fmt.Errorf("retry failed after %d attempts", maxAttempts) +} + +// Test LicenseVerification function (with retry mocking) +func TestLicenseVerification(t *testing.T) { + tests := []struct { + name string + licenseFound bool + maxAttempts int + expectSuccess bool + expectError bool + }{ + { + name: "License found on first attempt", + licenseFound: true, + maxAttempts: 5, + expectSuccess: true, + expectError: false, + }, + { + name: "License found after retries", + licenseFound: true, + maxAttempts: 5, + expectSuccess: true, + expectError: false, + }, + { + name: "License not found - timeout", + licenseFound: false, + maxAttempts: 5, + expectSuccess: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockRetry := &MockRetry{success: tt.licenseFound} + + // Simulate LicenseVerification logic + err := mockRetry.Retry(tt.maxAttempts, 1*time.Second, func() error { + // Simulate fetchLicenseSecret + if tt.licenseFound { + return nil // License found + } + return fmt.Errorf("license not found") + }) + + // Verify the behavior + if tt.expectSuccess && err != nil { + t.Errorf("Expected success but got error: %v", err) + } + if tt.expectError && err == nil { + t.Errorf("Expected error but got success") + } + if mockRetry.attempts > 0 { + if tt.licenseFound && mockRetry.attempts > 1 { + t.Errorf("Expected 1 attempt for found license, got %d", mockRetry.attempts) + } + } + }) + } +} + +// Mock for kubectl command execution +type MockKubectl struct { + output string + err error +} + +func (m *MockKubectl) RunCommandCustomIO(cli string, stdout, stderr *bytes.Buffer, suppressPrint bool, args ...string) error { + if m.err != nil { + return m.err + } + stdout.WriteString(m.output) + return nil +} + +// Test fetchLicenseSecret function (with kubectl mocking) +func TestFetchLicenseSecret(t *testing.T) { + tests := []struct { + name string + secretName string + kubectlOutput string + kubectlError error + expectFound bool + expectError bool + }{ + { + name: "Secret found", + secretName: "kubeslice-license-file", + kubectlOutput: "kubeslice-license-file", + kubectlError: nil, + expectFound: true, + expectError: false, + }, + { + name: "Secret not found", + secretName: "kubeslice-license-file", + kubectlOutput: "other-secret", + kubectlError: nil, + expectFound: false, + expectError: true, + }, + { + name: "Kubectl command error", + secretName: "kubeslice-license-file", + kubectlOutput: "", + kubectlError: fmt.Errorf("kubectl command failed"), + expectFound: false, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mockKubectl := &MockKubectl{ + output: tt.kubectlOutput, + err: tt.kubectlError, + } + + // Simulate fetchLicenseSecret logic + var outB bytes.Buffer + err := mockKubectl.RunCommandCustomIO("kubectl", &outB, &bytes.Buffer{}, true, "get", "secret", tt.secretName, "-n", "namespace") + + if err != nil { + // Command failed + if !tt.expectError { + t.Errorf("Unexpected error: %v", err) + } + return + } + + // Check if secret was found + found := false + for _, line := range strings.Split(outB.String(), "\n") { + if strings.Contains(line, tt.secretName) { + found = true + break + } + } + + if tt.expectFound && !found { + t.Errorf("Expected to find secret '%s' but not found", tt.secretName) + } + if !tt.expectFound && found { + t.Errorf("Expected not to find secret but found '%s'", tt.secretName) + } + }) + } +} + +// Test file operations with temporary files +func TestFileOperations(t *testing.T) { + tests := []struct { + name string + content string + expectError bool + }{ + { + name: "Write and read valid content", + content: "test content", + expectError: false, + }, + { + name: "Write and read empty content", + content: "", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file + tmpFile, err := os.CreateTemp("", "test-*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Test file writing + err = os.WriteFile(tmpFile.Name(), []byte(tt.content), 0644) + if err != nil && !tt.expectError { + t.Errorf("Failed to write file: %v", err) + } + + // Test file reading + readContent, err := os.ReadFile(tmpFile.Name()) + if err != nil && !tt.expectError { + t.Errorf("Failed to read file: %v", err) + } + + if string(readContent) != tt.content { + t.Errorf("Expected content '%s', got '%s'", tt.content, string(readContent)) + } + }) + } +} + +// Benchmark tests for performance +func BenchmarkPodVerificationLogic(b *testing.B) { + podOutput := `NAME READY STATUS RESTARTS AGE +pod1 1/1 Running 0 1m +pod2 1/1 Running 0 1m +pod3 1/1 Running 0 1m +pod4 1/1 Running 0 1m +pod5 1/1 Running 0 1m` + + for i := 0; i < b.N; i++ { + verifyPodsLogic(podOutput) + } +} + +func BenchmarkCommandConstruction(b *testing.B) { + cluster := &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"} + + for i := 0; i < b.N; i++ { + cmdArgs := []string{} + if cluster != nil { + cmdArgs = append(cmdArgs, "--context="+cluster.ContextName, "--kubeconfig="+cluster.KubeConfigPath) + } + cmdArgs = append(cmdArgs, "get", "pods", "-n", "default") + _ = cmdArgs + } +} From 1b67d43d87aedf88b53321a1feb056aebf82a9f9 Mon Sep 17 00:00:00 2001 From: arpit529srivastava Date: Sat, 2 Aug 2025 18:39:48 +0530 Subject: [PATCH 2/2] feat(tests): implement integration tests for kubernetes operations Signed-off-by: arpit529srivastava --- .../kubernetes_operations_integration_test.go | 878 +++++++++++++ .../internal/kubernetes_operations_test.go | 1140 ----------------- 2 files changed, 878 insertions(+), 1140 deletions(-) create mode 100644 test/pkg/internal/kubernetes_operations_integration_test.go delete mode 100644 test/pkg/internal/kubernetes_operations_test.go diff --git a/test/pkg/internal/kubernetes_operations_integration_test.go b/test/pkg/internal/kubernetes_operations_integration_test.go new file mode 100644 index 0000000..f450b3f --- /dev/null +++ b/test/pkg/internal/kubernetes_operations_integration_test.go @@ -0,0 +1,878 @@ +package internal_test + +import ( + "bytes" + "os" + "os/exec" + "strings" + "testing" + "time" + + YAML "sigs.k8s.io/yaml" +) + +// Test data structures for integration tests +type IntegrationCluster struct { + Name string + ContextName string + KubeConfigPath string +} + +type PodVerificationStatus int + +const ( + PodVerificationStatusSuccess PodVerificationStatus = iota + PodVerificationStatusInProgress + PodVerificationStatusFailed +) + +// Helper function to test the pod verification logic without external dependencies +func verifyPodsLogic(podOutput string) (PodVerificationStatus, string) { + var count = 0 + var lines = 0 + + // Split the output into lines and process each line + for _, line := range strings.Split(podOutput, "\n") { + if len(line) == 0 { + continue + } + + // Check for error states + if strings.Contains(line, "Error") || strings.Contains(line, "ImagePullBackOff") || strings.Contains(line, "CrashLoopBackOff") { + return PodVerificationStatusFailed, podOutput + } + + // Skip completed jobs + if strings.Contains(line, "Completed") { + continue + } + + // Count ready pods - look for lines with "/" pattern (e.g., "1/1", "0/1") + if strings.Contains(line, "/") { + index := strings.Index(line, "/") + if index > 0 && index < len(line)-1 { + // Extract the numbers before and after "/" + beforeSlash := line[index-1] + afterSlash := line[index+1] + + // Check if both are digits and equal (ready pods) + if beforeSlash >= '0' && beforeSlash <= '9' && + afterSlash >= '0' && afterSlash <= '9' && + beforeSlash == afterSlash { + count++ + } + } + lines++ + } + } + + if count == lines && lines > 0 { + return PodVerificationStatusSuccess, podOutput + } + return PodVerificationStatusInProgress, podOutput +} + +// Check if kubectl is available for integration tests +func isKubectlAvailable() bool { + cmd := exec.Command("kubectl", "version", "--client") + return cmd.Run() == nil +} + +// Check if we have a working Kubernetes cluster +func hasWorkingCluster() bool { + cmd := exec.Command("kubectl", "get", "nodes", "--no-headers") + var stderr bytes.Buffer + cmd.Stderr = &stderr + return cmd.Run() == nil +} + +// Test real file operations with actual files +func TestFileOperationsIntegration(t *testing.T) { + tests := []struct { + name string + content string + expectError bool + }{ + { + name: "Write and read valid YAML content", + content: "apiVersion: v1\nkind: Config\nspec:\n clusters: []", + expectError: false, + }, + { + name: "Write and read empty content", + content: "", + expectError: false, + }, + { + name: "Write and read complex YAML", + content: "apiVersion: v1\nkind: Config\nspec:\n clusters:\n - name: worker-1\n - name: worker-2\n metadata:\n name: test-config", + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file in temp directory + tmpFile, err := os.CreateTemp("", "integration-test-*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Test actual file writing + err = os.WriteFile(tmpFile.Name(), []byte(tt.content), 0644) + if err != nil && !tt.expectError { + t.Errorf("Failed to write file: %v", err) + return + } + + // Test actual file reading + readContent, err := os.ReadFile(tmpFile.Name()) + if err != nil && !tt.expectError { + t.Errorf("Failed to read file: %v", err) + return + } + + // Verify content matches exactly + if string(readContent) != tt.content { + t.Errorf("Expected content '%s', got '%s'", tt.content, string(readContent)) + } + + // Test file exists and is readable + if _, err := os.Stat(tmpFile.Name()); err != nil { + t.Errorf("File should exist but stat failed: %v", err) + } + }) + } +} + +// Test real kubectl command execution (if kubectl is available) +func TestKubectlCommandsIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + tests := []struct { + name string + args []string + expectError bool + }{ + { + name: "kubectl version", + args: []string{"version", "--client"}, + expectError: false, + }, + { + name: "kubectl get nodes", + args: []string{"get", "nodes", "--no-headers"}, + expectError: false, + }, + { + name: "kubectl get pods in default namespace", + args: []string{"get", "pods", "-n", "default", "--no-headers"}, + expectError: false, + }, + { + name: "kubectl invalid command", + args: []string{"invalid", "command"}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command("kubectl", tt.args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log output for debugging + t.Logf("Command: kubectl %s", strings.Join(tt.args, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + }) + } +} + +// Test real YAML/JSON manipulation with actual files +func TestYAMLJSONManipulationIntegration(t *testing.T) { + tests := []struct { + name string + yamlContent string + workers []string + expectError bool + }{ + { + name: "Set single worker", + yamlContent: `apiVersion: v1 +kind: Config +spec: + clusters: [] + metadata: + name: test-config`, + workers: []string{"worker-1"}, + expectError: false, + }, + { + name: "Set multiple workers", + yamlContent: `apiVersion: v1 +kind: Config +spec: + clusters: [] + metadata: + name: test-config`, + workers: []string{"worker-1", "worker-2", "worker-3"}, + expectError: false, + }, + { + name: "Set empty workers", + yamlContent: `apiVersion: v1 +kind: Config +spec: + clusters: [] + metadata: + name: test-config`, + workers: []string{}, + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary file with YAML content + tmpFile, err := os.CreateTemp("", "integration-yaml-*.yaml") + if err != nil { + t.Fatalf("Failed to create temp file: %v", err) + } + defer os.Remove(tmpFile.Name()) + + // Write initial YAML content + err = os.WriteFile(tmpFile.Name(), []byte(tt.yamlContent), 0644) + if err != nil { + t.Fatalf("Failed to write initial YAML: %v", err) + } + + // Test actual YAML to JSON conversion (simulating getConf function) + yamlBytes, err := os.ReadFile(tmpFile.Name()) + if err != nil { + t.Fatalf("Failed to read YAML file: %v", err) + } + + // Convert YAML to JSON (this is what getConf does) + jsonBytes, err := YAML.YAMLToJSON(yamlBytes) + if err != nil && !tt.expectError { + t.Errorf("Failed to convert YAML to JSON: %v", err) + return + } + + // Verify JSON contains expected structure + jsonStr := string(jsonBytes) + if !strings.Contains(jsonStr, "apiVersion") { + t.Error("Converted JSON should contain apiVersion") + } + if !strings.Contains(jsonStr, "spec") { + t.Error("Converted JSON should contain spec") + } + + // Test worker manipulation logic (simulating SetWorker function) + if len(tt.workers) > 0 { + // Simulate the worker array processing logic + for i, worker := range tt.workers { + if worker == "" { + t.Errorf("Worker at index %d should not be empty", i) + } + // This would normally use sjson.Set to modify the JSON + // For integration test, we verify the logic works + } + } + + // Verify the file still exists and is readable + if _, err := os.Stat(tmpFile.Name()); err != nil { + t.Errorf("File should still exist after processing: %v", err) + } + }) + } +} + +// Test real pod verification logic with actual kubectl output +func TestPodVerificationIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + // Test with actual kubectl get pods output + cmd := exec.Command("kubectl", "get", "pods", "-n", "default", "--no-headers") + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stdout + + err := cmd.Run() + if err != nil { + t.Logf("kubectl get pods failed (this is expected if no pods exist): %v", err) + t.Logf("Output: %s", stdout.String()) + // This is not a test failure - it's expected in some environments + return + } + + podOutput := stdout.String() + t.Logf("Actual kubectl output: %s", podOutput) + + // Test the actual pod verification logic with real output + status, output := verifyPodsLogic(podOutput) + + // Verify the logic works with real data + if status == PodVerificationStatusFailed { + // Check if there are actual error states in the output + if !strings.Contains(output, "Error") && + !strings.Contains(output, "ImagePullBackOff") && + !strings.Contains(output, "CrashLoopBackOff") { + t.Error("Status is Failed but no error states found in output") + } + } + + // Verify output matches input + if output != podOutput { + t.Errorf("Expected output to match input, got: %s", output) + } +} + +// Test real command construction and execution +func TestCommandConstructionAndExecutionIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + tests := []struct { + name string + resourceType string + resourceName string + namespace string + outputFormat string + expectError bool + }{ + { + name: "Get all pods", + resourceType: "pods", + resourceName: "", + namespace: "default", + outputFormat: "", + expectError: false, + }, + { + name: "Get pods with YAML output", + resourceType: "pods", + resourceName: "", + namespace: "default", + outputFormat: "yaml", + expectError: false, + }, + { + name: "Get specific pod (may not exist)", + resourceType: "pod", + resourceName: "test-pod", + namespace: "default", + outputFormat: "", + expectError: true, // Expected to fail if pod doesn't exist + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Construct command arguments (this is what the functions do) + cmdArgs := []string{} + + if tt.resourceName == "" { + cmdArgs = append(cmdArgs, "get", tt.resourceType, "-n", tt.namespace) + } else { + cmdArgs = append(cmdArgs, "get", tt.resourceType, tt.resourceName, "-n", tt.namespace) + } + + if tt.outputFormat != "" { + cmdArgs = append(cmdArgs, "-o", tt.outputFormat) + } + + // Actually execute the command (this tests real behavior) + cmd := exec.Command("kubectl", cmdArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log command and output for debugging + t.Logf("Command: kubectl %s", strings.Join(cmdArgs, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + // Verify the behavior matches expectations + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + + // Verify command arguments were constructed correctly + hasGet := false + hasResourceType := false + hasNamespace := false + hasOutputFormat := false + + for _, arg := range cmdArgs { + if arg == "get" { + hasGet = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + if tt.outputFormat != "" && arg == tt.outputFormat { + hasOutputFormat = true + } + } + + if !hasGet { + t.Error("Command args should contain 'get'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + if tt.outputFormat != "" && !hasOutputFormat { + t.Errorf("Command args should contain output format '%s'", tt.outputFormat) + } + }) + } +} + +// Test delete operations integration +func TestDeleteKubectlResourcesIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + tests := []struct { + name string + resourceType string + resourceName string + namespace string + expectError bool + }{ + { + name: "Delete non-existent pod (should fail)", + resourceType: "pod", + resourceName: "non-existent-pod", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + { + name: "Delete non-existent service (should fail)", + resourceType: "service", + resourceName: "non-existent-service", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Construct command arguments (this is what the functions do) + cmdArgs := []string{"delete", tt.resourceType, tt.resourceName, "-n", tt.namespace} + + // Actually execute the command (this tests real behavior) + cmd := exec.Command("kubectl", cmdArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log command and output for debugging + t.Logf("Command: kubectl %s", strings.Join(cmdArgs, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + // Verify the behavior matches expectations + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + + // Verify command arguments were constructed correctly + hasDelete := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "delete" { + hasDelete = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasDelete { + t.Error("Command args should contain 'delete'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test edit operations integration +func TestEditKubectlResourcesIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + tests := []struct { + name string + resourceType string + resourceName string + namespace string + expectError bool + }{ + { + name: "Edit non-existent pod (should fail)", + resourceType: "pod", + resourceName: "non-existent-pod", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + { + name: "Edit non-existent service (should fail)", + resourceType: "service", + resourceName: "non-existent-service", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Construct command arguments (this is what the functions do) + cmdArgs := []string{"edit", tt.resourceType, tt.resourceName, "-n", tt.namespace} + + // Actually execute the command (this tests real behavior) + cmd := exec.Command("kubectl", cmdArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log command and output for debugging + t.Logf("Command: kubectl %s", strings.Join(cmdArgs, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + // Verify the behavior matches expectations + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + + // Verify command arguments were constructed correctly + hasEdit := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "edit" { + hasEdit = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasEdit { + t.Error("Command args should contain 'edit'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test describe operations integration +func TestDescribeKubectlResourcesIntegration(t *testing.T) { + if !isKubectlAvailable() { + t.Skip("kubectl not available for integration test") + } + + tests := []struct { + name string + resourceType string + resourceName string + namespace string + expectError bool + }{ + { + name: "Describe non-existent pod (should fail)", + resourceType: "pod", + resourceName: "non-existent-pod", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + { + name: "Describe non-existent service (should fail)", + resourceType: "service", + resourceName: "non-existent-service", + namespace: "default", + expectError: true, // Expected to fail for non-existent resource + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Construct command arguments (this is what the functions do) + cmdArgs := []string{"describe", tt.resourceType, tt.resourceName, "-n", tt.namespace} + + // Actually execute the command (this tests real behavior) + cmd := exec.Command("kubectl", cmdArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log command and output for debugging + t.Logf("Command: kubectl %s", strings.Join(cmdArgs, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + // Verify the behavior matches expectations + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + + // Verify command arguments were constructed correctly + hasDescribe := false + hasResourceType := false + hasResourceName := false + hasNamespace := false + + for _, arg := range cmdArgs { + if arg == "describe" { + hasDescribe = true + } + if arg == tt.resourceType { + hasResourceType = true + } + if arg == tt.resourceName { + hasResourceName = true + } + if arg == "-n" || arg == tt.namespace { + hasNamespace = true + } + } + + if !hasDescribe { + t.Error("Command args should contain 'describe'") + } + if !hasResourceType { + t.Errorf("Command args should contain resource type '%s'", tt.resourceType) + } + if !hasResourceName { + t.Errorf("Command args should contain resource name '%s'", tt.resourceName) + } + if !hasNamespace { + t.Error("Command args should contain namespace flag") + } + }) + } +} + +// Test real time-based operations with actual timing +func TestTimeBasedOperationsIntegration(t *testing.T) { + tests := []struct { + name string + sleepDuration time.Duration + iterations int + }{ + { + name: "Short sleep", + sleepDuration: 10 * time.Millisecond, + iterations: 3, + }, + { + name: "Multiple iterations", + sleepDuration: 5 * time.Millisecond, + iterations: 5, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + start := time.Now() + var actualIterations int + + // Simulate the actual time-based loop logic + for i := 0; i < tt.iterations; i++ { + time.Sleep(tt.sleepDuration) + actualIterations++ + } + + elapsed := time.Since(start) + expectedDuration := tt.sleepDuration * time.Duration(tt.iterations) + + // Verify timing behavior (with some tolerance for system delays) + if elapsed < expectedDuration { + t.Errorf("Expected at least %v elapsed time, got %v", expectedDuration, elapsed) + } + + // Verify iteration count + if actualIterations != tt.iterations { + t.Errorf("Expected %d iterations, got %d", tt.iterations, actualIterations) + } + + t.Logf("Elapsed time: %v, Expected: %v", elapsed, expectedDuration) + }) + } +} + +// Test real error handling with actual failures +func TestErrorHandlingIntegration(t *testing.T) { + tests := []struct { + name string + command string + args []string + expectError bool + }{ + { + name: "Valid command", + command: "echo", + args: []string{"hello"}, + expectError: false, + }, + { + name: "Invalid command", + command: "nonexistent-command", + args: []string{}, + expectError: true, + }, + { + name: "Command that fails", + command: "false", + args: []string{}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(tt.command, tt.args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + + // Log command and output for debugging + t.Logf("Command: %s %s", tt.command, strings.Join(tt.args, " ")) + t.Logf("Stdout: %s", stdout.String()) + t.Logf("Stderr: %s", stderr.String()) + + // Verify error handling behavior + if tt.expectError && err == nil { + t.Errorf("Expected error but command succeeded") + } + if !tt.expectError && err != nil { + t.Errorf("Expected success but command failed: %v", err) + } + }) + } +} + +// Benchmark integration tests for performance +func BenchmarkFileOperationsIntegration(b *testing.B) { + content := "apiVersion: v1\nkind: Config\nspec:\n clusters: []" + + for i := 0; i < b.N; i++ { + tmpFile, err := os.CreateTemp("", "benchmark-*.yaml") + if err != nil { + b.Fatalf("Failed to create temp file: %v", err) + } + + err = os.WriteFile(tmpFile.Name(), []byte(content), 0644) + if err != nil { + b.Fatalf("Failed to write file: %v", err) + } + + _, err = os.ReadFile(tmpFile.Name()) + if err != nil { + b.Fatalf("Failed to read file: %v", err) + } + + os.Remove(tmpFile.Name()) + } +} + +func BenchmarkKubectlVersionIntegration(b *testing.B) { + if !isKubectlAvailable() { + b.Skip("kubectl not available for benchmark") + } + + for i := 0; i < b.N; i++ { + cmd := exec.Command("kubectl", "version", "--client") + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stdout + + err := cmd.Run() + if err != nil { + b.Fatalf("kubectl version failed: %v", err) + } + } +} diff --git a/test/pkg/internal/kubernetes_operations_test.go b/test/pkg/internal/kubernetes_operations_test.go deleted file mode 100644 index 8ae13f6..0000000 --- a/test/pkg/internal/kubernetes_operations_test.go +++ /dev/null @@ -1,1140 +0,0 @@ -package internal_test - -import ( - "bytes" - "fmt" - "os" - "strings" - "testing" - "time" -) - -// Test data structures -type Cluster struct { - Name string - ContextName string - KubeConfigPath string -} - -type PodVerificationStatus int - -const ( - PodVerificationStatusSuccess PodVerificationStatus = iota - PodVerificationStatusInProgress - PodVerificationStatusFailed -) - -// Test data structures -func createTestCluster() Cluster { - return Cluster{ - Name: "test-cluster", - ContextName: "test-context", - KubeConfigPath: "/tmp/test-kubeconfig", - } -} - -// Test verifyPods function -func TestVerifyPods(t *testing.T) { - tests := []struct { - name string - podOutput string - expectedStatus PodVerificationStatus - expectedError bool - }{ - { - name: "All pods running successfully", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 1/1 Running 0 1m -pod2 1/1 Running 0 1m`, - expectedStatus: PodVerificationStatusSuccess, - expectedError: false, - }, - { - name: "Pod in error state", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 0/1 Error 0 1m`, - expectedStatus: PodVerificationStatusFailed, - expectedError: false, - }, - { - name: "Pod in ImagePullBackOff state", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 0/1 ImagePullBackOff 0 1m`, - expectedStatus: PodVerificationStatusFailed, - expectedError: false, - }, - { - name: "Pod in CrashLoopBackOff state", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 0/1 CrashLoopBackOff 0 1m`, - expectedStatus: PodVerificationStatusFailed, - expectedError: false, - }, - { - name: "Pod in progress", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 0/1 Pending 0 1m`, - expectedStatus: PodVerificationStatusInProgress, - expectedError: false, - }, - { - name: "Completed job pod", - podOutput: `NAME READY STATUS RESTARTS AGE -job-pod 0/1 Completed 0 1m`, - expectedStatus: PodVerificationStatusInProgress, - expectedError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Call the function (this would need to be modified to accept a mock) - // For now, we'll test the logic separately - status, output := verifyPodsLogic(tt.podOutput) - - if status != tt.expectedStatus { - t.Errorf("Expected status %v, got %v", tt.expectedStatus, status) - } - if output != tt.podOutput { - t.Errorf("Expected output %v, got %v", tt.podOutput, output) - } - }) - } -} - -// Helper function to test the pod verification logic without external dependencies -func verifyPodsLogic(podOutput string) (PodVerificationStatus, string) { - var count = 0 - var lines = 0 - - // Split the output into lines and process each line - for _, line := range strings.Split(podOutput, "\n") { - if len(line) == 0 { - continue - } - - // Check for error states - if strings.Contains(line, "Error") || strings.Contains(line, "ImagePullBackOff") || strings.Contains(line, "CrashLoopBackOff") { - return PodVerificationStatusFailed, podOutput - } - - // Skip completed jobs - if strings.Contains(line, "Completed") { - continue - } - - // Count ready pods - look for lines with "/" pattern (e.g., "1/1", "0/1") - if strings.Contains(line, "/") { - index := strings.Index(line, "/") - if index > 0 && index < len(line)-1 { - // Extract the numbers before and after "/" - beforeSlash := line[index-1] - afterSlash := line[index+1] - - // Check if both are digits and equal (ready pods) - if beforeSlash >= '0' && beforeSlash <= '9' && - afterSlash >= '0' && afterSlash <= '9' && - beforeSlash == afterSlash { - count++ - } - } - lines++ - } - } - - if count == lines && lines > 0 { - return PodVerificationStatusSuccess, podOutput - } - return PodVerificationStatusInProgress, podOutput -} - -// Test ApplyKubectlManifest function -func TestApplyKubectlManifest(t *testing.T) { - tests := []struct { - name string - fileName string - namespace string - cluster *Cluster - expectError bool - }{ - { - name: "Apply manifest with cluster context", - fileName: "test-manifest.yaml", - namespace: "test-namespace", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - expectError: false, - }, - { - name: "Apply manifest without cluster context", - fileName: "test-manifest.yaml", - namespace: "test-namespace", - cluster: nil, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // This test would require mocking the util.RunCommandOnStdIO function - // For now, we'll test the argument construction logic - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "apply", "-f", tt.fileName, "-n", tt.namespace) - - expectedArgs := []string{"apply", "-f", tt.fileName, "-n", tt.namespace} - if tt.cluster != nil { - expectedArgs = append([]string{"--context=" + tt.cluster.ContextName, "--kubeconfig=" + tt.cluster.KubeConfigPath}, expectedArgs...) - } - - if len(cmdArgs) != len(expectedArgs) { - t.Errorf("Expected %d args, got %d", len(expectedArgs), len(cmdArgs)) - } - - for i, arg := range expectedArgs { - if i >= len(cmdArgs) || cmdArgs[i] != arg { - t.Errorf("Expected arg[%d] = %s, got %s", i, arg, cmdArgs[i]) - } - } - }) - } -} - -// Test resource management functions -func TestGetKubectlResources(t *testing.T) { - tests := []struct { - name string - resourceType string - resourceName string - namespace string - cluster *Cluster - outputFormat string - }{ - { - name: "Get all resources", - resourceType: "pods", - resourceName: "", - namespace: "default", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - outputFormat: "", - }, - { - name: "Get specific resource", - resourceType: "pod", - resourceName: "test-pod", - namespace: "default", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - outputFormat: "yaml", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - - if tt.resourceName == "" { - cmdArgs = append(cmdArgs, "get", tt.resourceType, "-n", tt.namespace) - } else { - cmdArgs = append(cmdArgs, "get", tt.resourceType, tt.resourceName, "-n", tt.namespace) - } - - if tt.outputFormat != "" { - cmdArgs = append(cmdArgs, "-o", tt.outputFormat) - } - - // Verify the command arguments are constructed correctly - hasGet := false - hasResourceType := false - hasNamespace := false - hasResourceName := false - hasOutputFormat := false - - for _, arg := range cmdArgs { - if arg == "get" { - hasGet = true - } - if arg == tt.resourceType { - hasResourceType = true - } - if arg == "-n" { - hasNamespace = true - } - if arg == tt.namespace { - hasNamespace = true - } - if tt.resourceName != "" && arg == tt.resourceName { - hasResourceName = true - } - if tt.outputFormat != "" && arg == tt.outputFormat { - hasOutputFormat = true - } - } - - if !hasGet { - t.Error("Command args should contain 'get'") - } - if !hasResourceType { - t.Errorf("Command args should contain resource type '%s'", tt.resourceType) - } - if !hasNamespace { - t.Error("Command args should contain namespace flag") - } - if tt.resourceName != "" && !hasResourceName { - t.Errorf("Command args should contain resource name '%s'", tt.resourceName) - } - if tt.outputFormat != "" && !hasOutputFormat { - t.Errorf("Command args should contain output format '%s'", tt.outputFormat) - } - }) - } -} - -// Test DeleteKubectlResources function -func TestDeleteKubectlResources(t *testing.T) { - tests := []struct { - name string - resourceType string - resourceName string - namespace string - cluster *Cluster - }{ - { - name: "Delete specific resource", - resourceType: "pod", - resourceName: "test-pod", - namespace: "default", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - }, - { - name: "Delete resource without cluster context", - resourceType: "service", - resourceName: "test-service", - namespace: "default", - cluster: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "delete", tt.resourceType, tt.resourceName, "-n", tt.namespace) - - // Verify the command arguments are constructed correctly - hasDelete := false - hasResourceType := false - hasResourceName := false - hasNamespace := false - - for _, arg := range cmdArgs { - if arg == "delete" { - hasDelete = true - } - if arg == tt.resourceType { - hasResourceType = true - } - if arg == tt.resourceName { - hasResourceName = true - } - if arg == "-n" || arg == tt.namespace { - hasNamespace = true - } - } - - if !hasDelete { - t.Error("Command args should contain 'delete'") - } - if !hasResourceType { - t.Errorf("Command args should contain resource type '%s'", tt.resourceType) - } - if !hasResourceName { - t.Errorf("Command args should contain resource name '%s'", tt.resourceName) - } - if !hasNamespace { - t.Error("Command args should contain namespace flag") - } - }) - } -} - -// Test EditKubectlResources function -func TestEditKubectlResources(t *testing.T) { - tests := []struct { - name string - resourceType string - resourceName string - namespace string - cluster *Cluster - }{ - { - name: "Edit specific resource", - resourceType: "pod", - resourceName: "test-pod", - namespace: "default", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - }, - { - name: "Edit resource without cluster context", - resourceType: "service", - resourceName: "test-service", - namespace: "default", - cluster: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "edit", tt.resourceType, tt.resourceName, "-n", tt.namespace) - - // Verify the command arguments are constructed correctly - hasEdit := false - hasResourceType := false - hasResourceName := false - hasNamespace := false - - for _, arg := range cmdArgs { - if arg == "edit" { - hasEdit = true - } - if arg == tt.resourceType { - hasResourceType = true - } - if arg == tt.resourceName { - hasResourceName = true - } - if arg == "-n" || arg == tt.namespace { - hasNamespace = true - } - } - - if !hasEdit { - t.Error("Command args should contain 'edit'") - } - if !hasResourceType { - t.Errorf("Command args should contain resource type '%s'", tt.resourceType) - } - if !hasResourceName { - t.Errorf("Command args should contain resource name '%s'", tt.resourceName) - } - if !hasNamespace { - t.Error("Command args should contain namespace flag") - } - }) - } -} - -// Test DescribeKubectlResources function -func TestDescribeKubectlResources(t *testing.T) { - tests := []struct { - name string - resourceType string - resourceName string - namespace string - cluster *Cluster - }{ - { - name: "Describe specific resource", - resourceType: "pod", - resourceName: "test-pod", - namespace: "default", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - }, - { - name: "Describe resource without cluster context", - resourceType: "service", - resourceName: "test-service", - namespace: "default", - cluster: nil, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "describe", tt.resourceType, tt.resourceName, "-n", tt.namespace) - - // Verify the command arguments are constructed correctly - hasDescribe := false - hasResourceType := false - hasResourceName := false - hasNamespace := false - - for _, arg := range cmdArgs { - if arg == "describe" { - hasDescribe = true - } - if arg == tt.resourceType { - hasResourceType = true - } - if arg == tt.resourceName { - hasResourceName = true - } - if arg == "-n" || arg == tt.namespace { - hasNamespace = true - } - } - - if !hasDescribe { - t.Error("Command args should contain 'describe'") - } - if !hasResourceType { - t.Errorf("Command args should contain resource type '%s'", tt.resourceType) - } - if !hasResourceName { - t.Errorf("Command args should contain resource name '%s'", tt.resourceName) - } - if !hasNamespace { - t.Error("Command args should contain namespace flag") - } - }) - } -} - -// Test ApplyFile function -func TestApplyFile(t *testing.T) { - tests := []struct { - name string - fileName string - namespace string - cluster *Cluster - expectError bool - }{ - { - name: "Apply file with cluster context", - fileName: "test-file.yaml", - namespace: "test-namespace", - cluster: &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"}, - expectError: false, - }, - { - name: "Apply file without cluster context", - fileName: "test-file.yaml", - namespace: "test-namespace", - cluster: nil, - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cmdArgs := []string{} - if tt.cluster != nil { - cmdArgs = append(cmdArgs, "--context="+tt.cluster.ContextName, "--kubeconfig="+tt.cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "apply", "-f", tt.fileName, "-n", tt.namespace) - - expectedArgs := []string{"apply", "-f", tt.fileName, "-n", tt.namespace} - if tt.cluster != nil { - expectedArgs = append([]string{"--context=" + tt.cluster.ContextName, "--kubeconfig=" + tt.cluster.KubeConfigPath}, expectedArgs...) - } - - if len(cmdArgs) != len(expectedArgs) { - t.Errorf("Expected %d args, got %d", len(expectedArgs), len(cmdArgs)) - } - - for i, arg := range expectedArgs { - if i >= len(cmdArgs) || cmdArgs[i] != arg { - t.Errorf("Expected arg[%d] = %s, got %s", i, arg, cmdArgs[i]) - } - } - }) - } -} - -// Test error handling scenarios -func TestKubectlCommandErrors(t *testing.T) { - tests := []struct { - name string - command string - args []string - expectError bool - }{ - { - name: "Valid kubectl command", - command: "kubectl", - args: []string{"get", "pods", "-n", "default"}, - expectError: false, - }, - { - name: "Invalid kubectl command", - command: "kubectl", - args: []string{"invalid", "command"}, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // This would test error handling when kubectl commands fail - // In a real implementation, we would mock util.RunCommand to return errors - // For now, we just verify the command structure - if len(tt.args) == 0 { - t.Error("Command args should not be empty") - } - - // Verify that the command has the expected structure - if tt.expectError { - // For invalid commands, we expect some args but they might not work - if len(tt.args) < 2 { - t.Error("Invalid command should have at least 2 args") - } - } else { - // For valid commands, we expect proper kubectl structure - if len(tt.args) < 3 { - t.Error("Valid kubectl command should have at least 3 args") - } - } - }) - } -} - -// Test edge cases for pod verification -func TestVerifyPodsEdgeCases(t *testing.T) { - tests := []struct { - name string - podOutput string - expectedStatus PodVerificationStatus - }{ - { - name: "Empty pod output", - podOutput: "", - expectedStatus: PodVerificationStatusInProgress, - }, - { - name: "Only header line", - podOutput: "NAME READY STATUS RESTARTS AGE", - expectedStatus: PodVerificationStatusInProgress, - }, - { - name: "Mixed pod states", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 1/1 Running 0 1m -pod2 0/1 Pending 0 1m`, - expectedStatus: PodVerificationStatusInProgress, - }, - { - name: "Pod with restart count", - podOutput: `NAME READY STATUS RESTARTS AGE -pod1 1/1 Running 2 1m`, - expectedStatus: PodVerificationStatusSuccess, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - status, output := verifyPodsLogic(tt.podOutput) - - if status != tt.expectedStatus { - t.Errorf("Expected status %v, got %v", tt.expectedStatus, status) - } - if output != tt.podOutput { - t.Errorf("Expected output %v, got %v", tt.podOutput, output) - } - }) - } -} - -// Integration test setup (optional) -func TestIntegrationWithMockKubernetes(t *testing.T) { - // This test would require setting up a mock Kubernetes API server - // or using tools like kind for local testing - t.Skip("Integration test requires mock Kubernetes cluster") -} - -// Test SetWorker function (YAML/JSON manipulation) -func TestSetWorker(t *testing.T) { - tests := []struct { - name string - workers []string - filename string - expectError bool - }{ - { - name: "Set single worker", - workers: []string{"worker-1"}, - filename: "test-config.yaml", - expectError: false, - }, - { - name: "Set multiple workers", - workers: []string{"worker-1", "worker-2", "worker-3"}, - filename: "test-config.yaml", - expectError: false, - }, - { - name: "Set empty workers", - workers: []string{}, - filename: "test-config.yaml", - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a temporary test file - testYAML := `apiVersion: v1 -kind: Config -spec: - clusters: [] - metadata: - name: test-config` - - // Test the worker array handling logic - if len(tt.workers) > 0 { - // Verify that workers are processed - for i, worker := range tt.workers { - if worker == "" { - t.Errorf("Worker at index %d should not be empty", i) - } - } - } - - // Test file writing logic (mocked) - if tt.filename == "" { - t.Error("Filename should not be empty") - } - - // Test JSON manipulation logic - if len(tt.workers) > 0 { - // Simulate JSON manipulation - for i := range tt.workers { - // This would normally use sjson.Set - // For testing, we just verify the logic - if i < 0 { - t.Error("Invalid worker index") - } - } - } - - // Verify the test data structure - if !strings.Contains(testYAML, "spec:") { - t.Error("Test YAML should contain spec section") - } - }) - } -} - -// Test getConf function (File reading and YAML parsing) -func TestGetConf(t *testing.T) { - tests := []struct { - name string - yamlContent string - expectError bool - }{ - { - name: "Valid YAML content", - yamlContent: `apiVersion: v1 -kind: Config -spec: - clusters: - - name: worker-1 - - name: worker-2`, - expectError: false, - }, - { - name: "Empty YAML content", - yamlContent: "", - expectError: false, - }, - { - name: "Invalid YAML content", - yamlContent: `apiVersion: v1 -kind: Config -spec: - clusters: - - name: worker-1 - - name: worker-2 - invalid: [`, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Test YAML to JSON conversion logic - if tt.yamlContent != "" { - // Verify YAML structure - if !strings.Contains(tt.yamlContent, "apiVersion") { - t.Error("YAML content should contain apiVersion") - } - - // Test JSON conversion (mocked) - jsonData := `{"apiVersion":"v1","kind":"Config"}` - if !strings.Contains(jsonData, "apiVersion") { - t.Error("Converted JSON should contain apiVersion") - } - } - - // Test error handling - if tt.expectError { - // This would normally test YAML parsing errors - // For now, we just verify the test structure - if !strings.Contains(tt.yamlContent, "invalid") { - t.Error("Invalid YAML should contain invalid content") - } - } - }) - } -} - -// Mock for time-based operations -type MockTime struct { - sleepCalled bool - sleepTime time.Duration -} - -func (m *MockTime) Sleep(d time.Duration) { - m.sleepCalled = true - m.sleepTime = d -} - -// Test PodVerification function (with time mocking) -func TestPodVerification(t *testing.T) { - tests := []struct { - name string - podStatus PodVerificationStatus - iterations int - backoffCount int - expectSuccess bool - expectTimeout bool - }{ - { - name: "Pods become ready quickly", - podStatus: PodVerificationStatusSuccess, - iterations: 1, - backoffCount: 0, - expectSuccess: true, - expectTimeout: false, - }, - { - name: "Pods take time to become ready", - podStatus: PodVerificationStatusInProgress, - iterations: 5, - backoffCount: 0, - expectSuccess: false, // Will timeout due to iteration limit - expectTimeout: true, - }, - { - name: "Pods fail and timeout", - podStatus: PodVerificationStatusFailed, - iterations: 25, // More than backoffLimit (20) - backoffCount: 21, - expectSuccess: false, - expectTimeout: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockTime := &MockTime{} - - // Simulate the PodVerification loop logic - var i = 0 - var backoffCount = 0 - var backoffLimit = 20 - var success = false - - for { - i = i + 1 - mockTime.Sleep(5 * time.Second) - - // Simulate pod verification - if tt.podStatus == PodVerificationStatusSuccess { - success = true - break - } else if tt.podStatus == PodVerificationStatusFailed { - backoffCount = backoffCount + 1 - if backoffCount > backoffLimit { - break // Timeout - } - } - - // Simulate iteration limit - if i >= tt.iterations { - break - } - } - - // Verify the behavior - if tt.expectSuccess && !success { - t.Errorf("Expected success but got failure") - } - if tt.expectTimeout { - // For InProgress, timeout should be due to iteration limit - // For Failed, timeout should be due to backoff count - if tt.podStatus == PodVerificationStatusInProgress { - if i < tt.iterations { - t.Errorf("Expected timeout due to iteration limit, but loop ended early at iteration %d", i) - } - } else if tt.podStatus == PodVerificationStatusFailed { - if backoffCount <= backoffLimit { - t.Errorf("Expected timeout but backoff count (%d) <= limit (%d)", backoffCount, backoffLimit) - } - } - } - if mockTime.sleepCalled { - expectedSleepTime := 5 * time.Second - if mockTime.sleepTime != expectedSleepTime { - t.Errorf("Expected sleep time %v, got %v", expectedSleepTime, mockTime.sleepTime) - } - } - }) - } -} - -// Mock for Retry function -type MockRetry struct { - attempts int - maxAttempts int - success bool -} - -func (m *MockRetry) Retry(maxAttempts int, sleep time.Duration, f func() error) error { - m.maxAttempts = maxAttempts - for i := 0; i < maxAttempts; i++ { - m.attempts++ - if m.success { - return nil - } - if i < maxAttempts-1 { - time.Sleep(sleep) - } - } - return fmt.Errorf("retry failed after %d attempts", maxAttempts) -} - -// Test LicenseVerification function (with retry mocking) -func TestLicenseVerification(t *testing.T) { - tests := []struct { - name string - licenseFound bool - maxAttempts int - expectSuccess bool - expectError bool - }{ - { - name: "License found on first attempt", - licenseFound: true, - maxAttempts: 5, - expectSuccess: true, - expectError: false, - }, - { - name: "License found after retries", - licenseFound: true, - maxAttempts: 5, - expectSuccess: true, - expectError: false, - }, - { - name: "License not found - timeout", - licenseFound: false, - maxAttempts: 5, - expectSuccess: false, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockRetry := &MockRetry{success: tt.licenseFound} - - // Simulate LicenseVerification logic - err := mockRetry.Retry(tt.maxAttempts, 1*time.Second, func() error { - // Simulate fetchLicenseSecret - if tt.licenseFound { - return nil // License found - } - return fmt.Errorf("license not found") - }) - - // Verify the behavior - if tt.expectSuccess && err != nil { - t.Errorf("Expected success but got error: %v", err) - } - if tt.expectError && err == nil { - t.Errorf("Expected error but got success") - } - if mockRetry.attempts > 0 { - if tt.licenseFound && mockRetry.attempts > 1 { - t.Errorf("Expected 1 attempt for found license, got %d", mockRetry.attempts) - } - } - }) - } -} - -// Mock for kubectl command execution -type MockKubectl struct { - output string - err error -} - -func (m *MockKubectl) RunCommandCustomIO(cli string, stdout, stderr *bytes.Buffer, suppressPrint bool, args ...string) error { - if m.err != nil { - return m.err - } - stdout.WriteString(m.output) - return nil -} - -// Test fetchLicenseSecret function (with kubectl mocking) -func TestFetchLicenseSecret(t *testing.T) { - tests := []struct { - name string - secretName string - kubectlOutput string - kubectlError error - expectFound bool - expectError bool - }{ - { - name: "Secret found", - secretName: "kubeslice-license-file", - kubectlOutput: "kubeslice-license-file", - kubectlError: nil, - expectFound: true, - expectError: false, - }, - { - name: "Secret not found", - secretName: "kubeslice-license-file", - kubectlOutput: "other-secret", - kubectlError: nil, - expectFound: false, - expectError: true, - }, - { - name: "Kubectl command error", - secretName: "kubeslice-license-file", - kubectlOutput: "", - kubectlError: fmt.Errorf("kubectl command failed"), - expectFound: false, - expectError: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - mockKubectl := &MockKubectl{ - output: tt.kubectlOutput, - err: tt.kubectlError, - } - - // Simulate fetchLicenseSecret logic - var outB bytes.Buffer - err := mockKubectl.RunCommandCustomIO("kubectl", &outB, &bytes.Buffer{}, true, "get", "secret", tt.secretName, "-n", "namespace") - - if err != nil { - // Command failed - if !tt.expectError { - t.Errorf("Unexpected error: %v", err) - } - return - } - - // Check if secret was found - found := false - for _, line := range strings.Split(outB.String(), "\n") { - if strings.Contains(line, tt.secretName) { - found = true - break - } - } - - if tt.expectFound && !found { - t.Errorf("Expected to find secret '%s' but not found", tt.secretName) - } - if !tt.expectFound && found { - t.Errorf("Expected not to find secret but found '%s'", tt.secretName) - } - }) - } -} - -// Test file operations with temporary files -func TestFileOperations(t *testing.T) { - tests := []struct { - name string - content string - expectError bool - }{ - { - name: "Write and read valid content", - content: "test content", - expectError: false, - }, - { - name: "Write and read empty content", - content: "", - expectError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create temporary file - tmpFile, err := os.CreateTemp("", "test-*.yaml") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tmpFile.Name()) - - // Test file writing - err = os.WriteFile(tmpFile.Name(), []byte(tt.content), 0644) - if err != nil && !tt.expectError { - t.Errorf("Failed to write file: %v", err) - } - - // Test file reading - readContent, err := os.ReadFile(tmpFile.Name()) - if err != nil && !tt.expectError { - t.Errorf("Failed to read file: %v", err) - } - - if string(readContent) != tt.content { - t.Errorf("Expected content '%s', got '%s'", tt.content, string(readContent)) - } - }) - } -} - -// Benchmark tests for performance -func BenchmarkPodVerificationLogic(b *testing.B) { - podOutput := `NAME READY STATUS RESTARTS AGE -pod1 1/1 Running 0 1m -pod2 1/1 Running 0 1m -pod3 1/1 Running 0 1m -pod4 1/1 Running 0 1m -pod5 1/1 Running 0 1m` - - for i := 0; i < b.N; i++ { - verifyPodsLogic(podOutput) - } -} - -func BenchmarkCommandConstruction(b *testing.B) { - cluster := &Cluster{ContextName: "test-context", KubeConfigPath: "/tmp/kubeconfig"} - - for i := 0; i < b.N; i++ { - cmdArgs := []string{} - if cluster != nil { - cmdArgs = append(cmdArgs, "--context="+cluster.ContextName, "--kubeconfig="+cluster.KubeConfigPath) - } - cmdArgs = append(cmdArgs, "get", "pods", "-n", "default") - _ = cmdArgs - } -}