From 7e67ca5c0d2c2183e64d1df8bd5777ed04308c3e Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:37:15 -0500 Subject: [PATCH 1/4] chore(provisioner): Enhance test coverage Enhances test coverage and standardizes provisioner package. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/provisioner/provisioner_test.go | 375 +++++++++++++++++++++++++++- 1 file changed, 371 insertions(+), 4 deletions(-) diff --git a/pkg/provisioner/provisioner_test.go b/pkg/provisioner/provisioner_test.go index cacaac311..ff48f2484 100644 --- a/pkg/provisioner/provisioner_test.go +++ b/pkg/provisioner/provisioner_test.go @@ -52,7 +52,8 @@ func createTestBlueprint() *blueprintv1alpha1.Blueprint { } } -type Mocks struct { +// ProvisionerTestMocks contains all the mock dependencies for testing the Provisioner +type ProvisionerTestMocks struct { ConfigHandler config.ConfigHandler Shell *shell.MockShell TerraformStack *terraforminfra.MockStack @@ -63,8 +64,8 @@ type Mocks struct { BlueprintHandler blueprint.BlueprintHandler } -// setupProvisionerMocks creates mock components for testing the Provisioner -func setupProvisionerMocks(t *testing.T) *Mocks { +// setupProvisionerMocks creates mock components for testing the Provisioner with optional overrides +func setupProvisionerMocks(t *testing.T, opts ...func(*ProvisionerTestMocks)) *ProvisionerTestMocks { t.Helper() configHandler := config.NewMockConfigHandler() @@ -113,7 +114,7 @@ func setupProvisionerMocks(t *testing.T) *Mocks { Shell: mockShell, } - return &Mocks{ + mocks := &ProvisionerTestMocks{ ConfigHandler: configHandler, Shell: mockShell, TerraformStack: terraformStack, @@ -123,6 +124,13 @@ func setupProvisionerMocks(t *testing.T) *Mocks { Runtime: rt, BlueprintHandler: mockBlueprintHandler, } + + // Apply any overrides + for _, opt := range opts { + opt(mocks) + } + + return mocks } // ============================================================================= @@ -245,6 +253,25 @@ func TestNewProvisioner(t *testing.T) { t.Error("Expected existing cluster client to be used") } }) + + t.Run("SkipsTerraformStackWhenDisabled", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "terraform.enabled" { + return false + } + return false + } + + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler) + + if provisioner.TerraformStack != nil { + t.Error("Expected terraform stack to be nil when terraform is disabled") + } + }) + } // ============================================================================= @@ -1076,4 +1103,344 @@ func TestProvisioner_CheckNodeHealth(t *testing.T) { t.Errorf("Expected no error, got: %v", err) } }) + + t.Run("SuccessWithZeroTimeout", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.ClusterClient.WaitForNodesHealthyFunc = func(ctx stdcontext.Context, nodeAddresses []string, expectedVersion string) error { + return nil + } + opts := &Provisioner{ + ClusterClient: mocks.ClusterClient, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + Timeout: 0, + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + }) + + t.Run("SuccessWithK8SEndpointTrue", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + if endpoint != "" { + t.Errorf("Expected empty endpoint when K8SEndpoint is 'true', got: %q", endpoint) + } + return nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "true", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if len(outputMessages) != 1 { + t.Errorf("Expected 1 output message, got: %d", len(outputMessages)) + } + + if !strings.Contains(outputMessages[0], "kubeconfig default") { + t.Errorf("Expected output about kubeconfig default, got: %q", outputMessages[0]) + } + }) + + t.Run("SuccessWithNodeReadinessCheckAllReady", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"10.0.0.1": true, "10.0.0.2": true}, nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1", "10.0.0.2"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + foundReadyMessage := false + for _, msg := range outputMessages { + if strings.Contains(msg, "all nodes are Ready") { + foundReadyMessage = true + break + } + } + + if !foundReadyMessage { + t.Error("Expected output message about all nodes being Ready") + } + }) + + t.Run("SuccessWithNodeReadinessCheckNotAllReady", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"10.0.0.1": false}, nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + foundHealthyMessage := false + for _, msg := range outputMessages { + if strings.Contains(msg, "is healthy") && !strings.Contains(msg, "all nodes are Ready") { + foundHealthyMessage = true + break + } + } + + if !foundHealthyMessage { + t.Error("Expected output message about endpoint being healthy without all nodes Ready") + } + }) + + t.Run("SuccessWithNodeReadinessCheckGetNodeReadyStatusError", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return nil, fmt.Errorf("get node ready status failed") + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + foundHealthyMessage := false + for _, msg := range outputMessages { + if strings.Contains(msg, "is healthy") && !strings.Contains(msg, "all nodes are Ready") { + foundHealthyMessage = true + break + } + } + + if !foundHealthyMessage { + t.Error("Expected output message about endpoint being healthy when GetNodeReadyStatus fails") + } + }) + + t.Run("SuccessWithNodeReadinessCheckPartialReadyStatus", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"10.0.0.1": true}, nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1", "10.0.0.2"}, + K8SEndpoint: "https://test:6443", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + foundHealthyMessage := false + for _, msg := range outputMessages { + if strings.Contains(msg, "is healthy") && !strings.Contains(msg, "all nodes are Ready") { + foundHealthyMessage = true + break + } + } + + if !foundHealthyMessage { + t.Error("Expected output message about endpoint being healthy when not all nodes are ready") + } + }) + + t.Run("ErrorNoHealthChecksWhenNodesProvidedButNoClusterClient", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler) + provisioner.ClusterClient = nil + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpointProvided: false, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, nil) + + if err == nil { + t.Error("Expected error when nodes provided but no cluster client") + } + + if !strings.Contains(err.Error(), "no health checks specified") { + t.Errorf("Expected error about no health checks, got: %v", err) + } + }) + + t.Run("SuccessWithK8SEndpointEmptyString", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + if endpoint != "" { + t.Errorf("Expected empty endpoint, got: %q", endpoint) + } + return nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + K8SEndpoint: "", + K8SEndpointProvided: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if len(outputMessages) != 1 { + t.Errorf("Expected 1 output message, got: %d", len(outputMessages)) + } + + if !strings.Contains(outputMessages[0], "kubeconfig default") { + t.Errorf("Expected output about kubeconfig default, got: %q", outputMessages[0]) + } + }) + + t.Run("SuccessWithK8SEndpointAndNodeReadinessCheckDefaultEndpoint", func(t *testing.T) { + mocks := setupProvisionerMocks(t) + mocks.KubernetesManager.WaitForKubernetesHealthyFunc = func(ctx stdcontext.Context, endpoint string, outputFunc func(string), nodeNames ...string) error { + return nil + } + mocks.KubernetesManager.GetNodeReadyStatusFunc = func(ctx stdcontext.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"10.0.0.1": true}, nil + } + opts := &Provisioner{ + KubernetesManager: mocks.KubernetesManager, + } + provisioner := NewProvisioner(mocks.Runtime, mocks.BlueprintHandler, opts) + + var outputMessages []string + outputFunc := func(msg string) { + outputMessages = append(outputMessages, msg) + } + + options := NodeHealthCheckOptions{ + Nodes: []string{"10.0.0.1"}, + K8SEndpoint: "", + K8SEndpointProvided: true, + CheckNodeReady: true, + } + + err := provisioner.CheckNodeHealth(stdcontext.Background(), options, outputFunc) + + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + foundReadyMessage := false + for _, msg := range outputMessages { + if strings.Contains(msg, "kubeconfig default") && strings.Contains(msg, "all nodes are Ready") { + foundReadyMessage = true + break + } + } + + if !foundReadyMessage { + t.Error("Expected output message about kubeconfig default and all nodes Ready") + } + }) } From 27355110c4369f87c3341a5d439174e986c89aa5 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:46:38 -0500 Subject: [PATCH 2/4] Enhance cluster client tests Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../cluster/cluster_client_test.go | 31 ++- .../cluster/talos_cluster_client_test.go | 260 +++++++++++++++++- 2 files changed, 268 insertions(+), 23 deletions(-) diff --git a/pkg/provisioner/cluster/cluster_client_test.go b/pkg/provisioner/cluster/cluster_client_test.go index 7a181a165..5d9b35a86 100644 --- a/pkg/provisioner/cluster/cluster_client_test.go +++ b/pkg/provisioner/cluster/cluster_client_test.go @@ -7,18 +7,6 @@ import ( "github.com/windsorcli/cli/pkg/constants" ) -// ============================================================================= -// Test Setup -// ============================================================================= - -type Mocks struct { - // Add mocks as needed -} - -type SetupOptions struct { - // Add setup options as needed -} - // ============================================================================= // Test Constructor // ============================================================================= @@ -50,12 +38,25 @@ func TestNewBaseClusterClient(t *testing.T) { func TestBaseClusterClient_Close(t *testing.T) { t.Run("Success", func(t *testing.T) { - // Given a base cluster client client := NewBaseClusterClient() - // When calling Close - // Then it should not panic client.Close() + + if client == nil { + t.Error("Expected client to remain non-nil after Close") + } + }) + + t.Run("CanBeCalledMultipleTimes", func(t *testing.T) { + client := NewBaseClusterClient() + + client.Close() + client.Close() + client.Close() + + if client == nil { + t.Error("Expected client to remain non-nil after multiple Close calls") + } }) } diff --git a/pkg/provisioner/cluster/talos_cluster_client_test.go b/pkg/provisioner/cluster/talos_cluster_client_test.go index ff3dd0104..9e15c3713 100644 --- a/pkg/provisioner/cluster/talos_cluster_client_test.go +++ b/pkg/provisioner/cluster/talos_cluster_client_test.go @@ -17,9 +17,8 @@ import ( // Test Setup // ============================================================================= -// setupShims initializes and returns shims for tests -func setupShims(t *testing.T) *Shims { - t.Helper() +// setupDefaultShims initializes and returns shims with default test configurations +func setupDefaultShims() *Shims { shims := NewShims() shims.TalosConfigOpen = func(configPath string) (*clientconfig.Config, error) { @@ -102,7 +101,7 @@ func TestTalosClusterClient_WaitForNodesHealthy(t *testing.T) { setup := func(t *testing.T) *TalosClusterClient { t.Helper() client := NewTalosClusterClient() - client.shims = setupShims(t) + client.shims = setupDefaultShims() client.healthCheckTimeout = 100 * time.Millisecond client.healthCheckPollInterval = 10 * time.Millisecond return client @@ -277,6 +276,213 @@ func TestTalosClusterClient_WaitForNodesHealthy(t *testing.T) { t.Errorf("Expected timeout error, got %v", err) } }) + + t.Run("ContextWithDeadline", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + client.healthCheckTimeout = 200 * time.Millisecond + client.healthCheckPollInterval = 10 * time.Millisecond + + nodeAddresses := []string{"10.0.0.1"} + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, "") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("MultipleNodesWithMixedHealth", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + callCount := 0 + client.shims.TalosServiceList = func(ctx context.Context, client *talosclient.Client) (*machine.ServiceListResponse, error) { + callCount++ + if callCount == 1 { + return &machine.ServiceListResponse{ + Messages: []*machine.ServiceList{ + { + Services: []*machine.ServiceInfo{ + { + Id: "apid", + State: "Running", + Health: &machine.ServiceHealth{ + Healthy: true, + }, + }, + }, + }, + }, + }, nil + } + return &machine.ServiceListResponse{ + Messages: []*machine.ServiceList{ + { + Services: []*machine.ServiceInfo{ + { + Id: "apid", + State: "Running", + Health: &machine.ServiceHealth{ + Healthy: true, + }, + }, + }, + }, + }, + }, nil + } + + ctx := context.Background() + nodeAddresses := []string{"10.0.0.1", "10.0.0.2"} + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, "") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("TimeoutWithUnhealthyAndVersionMismatch", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + client.healthCheckTimeout = 50 * time.Millisecond + client.healthCheckPollInterval = 10 * time.Millisecond + + client.shims.TalosServiceList = func(ctx context.Context, client *talosclient.Client) (*machine.ServiceListResponse, error) { + return &machine.ServiceListResponse{ + Messages: []*machine.ServiceList{ + { + Services: []*machine.ServiceInfo{ + { + Id: "apid", + State: "Stopped", + Health: &machine.ServiceHealth{ + Healthy: false, + }, + }, + }, + }, + }, + }, nil + } + + client.shims.TalosVersion = func(ctx context.Context, client *talosclient.Client) (*machine.VersionResponse, error) { + return &machine.VersionResponse{ + Messages: []*machine.Version{ + { + Version: &machine.VersionInfo{ + Tag: "v1.1.0", + }, + }, + }, + }, nil + } + + ctx := context.Background() + nodeAddresses := []string{"10.0.0.1"} + expectedVersion := "1.0.0" + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, expectedVersion) + if err == nil { + t.Error("Expected error, got nil") + } + + if !strings.Contains(err.Error(), "unhealthy nodes") && !strings.Contains(err.Error(), "version mismatch nodes") { + t.Errorf("Expected error about unhealthy or version mismatch nodes, got %v", err) + } + }) + + t.Run("EmptyNodeAddresses", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + ctx := context.Background() + nodeAddresses := []string{} + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, "") + if err != nil { + t.Errorf("Expected no error for empty node addresses, got %v", err) + } + }) + + t.Run("MultipleNodesOneHealthyOneUnhealthy", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + client.healthCheckTimeout = 50 * time.Millisecond + client.healthCheckPollInterval = 10 * time.Millisecond + + client.shims.TalosServiceList = func(ctx context.Context, client *talosclient.Client) (*machine.ServiceListResponse, error) { + return &machine.ServiceListResponse{ + Messages: []*machine.ServiceList{ + { + Services: []*machine.ServiceInfo{ + { + Id: "apid", + State: "Stopped", + Health: &machine.ServiceHealth{ + Healthy: false, + }, + }, + }, + }, + }, + }, nil + } + + ctx := context.Background() + nodeAddresses := []string{"10.0.0.1", "10.0.0.2"} + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, "") + if err == nil { + t.Error("Expected error when nodes are unhealthy, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for nodes") { + t.Errorf("Expected timeout error, got %v", err) + } + }) + + t.Run("MultipleNodesWithVersionMismatch", func(t *testing.T) { + client := setup(t) + os.Setenv("TALOSCONFIG", "/tmp/talosconfig") + defer os.Unsetenv("TALOSCONFIG") + + client.healthCheckTimeout = 50 * time.Millisecond + client.healthCheckPollInterval = 10 * time.Millisecond + + client.shims.TalosVersion = func(ctx context.Context, client *talosclient.Client) (*machine.VersionResponse, error) { + return &machine.VersionResponse{ + Messages: []*machine.Version{ + { + Version: &machine.VersionInfo{ + Tag: "v1.1.0", + }, + }, + }, + }, nil + } + + ctx := context.Background() + nodeAddresses := []string{"10.0.0.1", "10.0.0.2"} + expectedVersion := "1.0.0" + + err := client.WaitForNodesHealthy(ctx, nodeAddresses, expectedVersion) + if err == nil { + t.Error("Expected error when nodes have version mismatch, got nil") + } + if !strings.Contains(err.Error(), "version mismatch nodes") { + t.Errorf("Expected version mismatch error, got %v", err) + } + }) } func TestTalosClusterClient_Close(t *testing.T) { @@ -295,7 +501,7 @@ func TestTalosClusterClient_Close(t *testing.T) { t.Run("SuccessWithClient", func(t *testing.T) { client := NewTalosClusterClient() - client.shims = setupShims(t) + client.shims = setupDefaultShims() // Set up a mock client (we'll use a non-nil pointer to simulate having a client) mockClient := &talosclient.Client{} @@ -335,7 +541,7 @@ func TestTalosClusterClient_ensureClient(t *testing.T) { setup := func(t *testing.T) *TalosClusterClient { t.Helper() client := NewTalosClusterClient() - client.shims = setupShims(t) + client.shims = setupDefaultShims() return client } @@ -419,7 +625,7 @@ func TestTalosClusterClient_getNodeHealthDetails(t *testing.T) { setup := func(t *testing.T) *TalosClusterClient { t.Helper() client := NewTalosClusterClient() - client.shims = setupShims(t) + client.shims = setupDefaultShims() return client } @@ -585,7 +791,7 @@ func TestTalosClusterClient_getNodeVersion(t *testing.T) { setup := func(t *testing.T) *TalosClusterClient { t.Helper() client := NewTalosClusterClient() - client.shims = setupShims(t) + client.shims = setupDefaultShims() return client } @@ -647,3 +853,41 @@ func TestTalosClusterClient_getNodeVersion(t *testing.T) { } }) } + +// ============================================================================= +// Test Shims +// ============================================================================= + +func TestNewShims(t *testing.T) { + t.Run("InitializesAllFields", func(t *testing.T) { + shims := NewShims() + + if shims == nil { + t.Fatal("Expected non-nil Shims") + } + + if shims.TalosConfigOpen == nil { + t.Error("Expected TalosConfigOpen to be initialized") + } + + if shims.TalosNewClient == nil { + t.Error("Expected TalosNewClient to be initialized") + } + + if shims.TalosVersion == nil { + t.Error("Expected TalosVersion to be initialized") + } + + if shims.TalosWithNodes == nil { + t.Error("Expected TalosWithNodes to be initialized") + } + + if shims.TalosServiceList == nil { + t.Error("Expected TalosServiceList to be initialized") + } + + if shims.TalosClose == nil { + t.Error("Expected TalosClose to be initialized") + } + }) +} From 14b6cbc38ab80f201ba9f6dbe3f063e014fe892d Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:18:20 -0500 Subject: [PATCH 3/4] Enhance kubernetes manager tests Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/provisioner/kubernetes/client/client.go | 2 - .../kubernetes/kubernetes_manager.go | 46 +- .../kubernetes_manager_private_test.go | 456 ++++++++ ...t.go => kubernetes_manager_public_test.go} | 991 +++++++++++++----- 4 files changed, 1224 insertions(+), 271 deletions(-) create mode 100644 pkg/provisioner/kubernetes/kubernetes_manager_private_test.go rename pkg/provisioner/kubernetes/{kubernetes_manager_test.go => kubernetes_manager_public_test.go} (80%) diff --git a/pkg/provisioner/kubernetes/client/client.go b/pkg/provisioner/kubernetes/client/client.go index f20ac3703..c3a97ee5d 100644 --- a/pkg/provisioner/kubernetes/client/client.go +++ b/pkg/provisioner/kubernetes/client/client.go @@ -24,14 +24,12 @@ import ( // KubernetesClient defines methods for Kubernetes resource operations type KubernetesClient interface { - // Resource operations GetResource(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) ListResources(gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) ApplyResource(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) DeleteResource(gvr schema.GroupVersionResource, namespace, name string, opts metav1.DeleteOptions) error PatchResource(gvr schema.GroupVersionResource, namespace, name string, pt types.PatchType, data []byte, opts metav1.PatchOptions) (*unstructured.Unstructured, error) CheckHealth(ctx context.Context, endpoint string) error - // Node health operations GetNodeReadyStatus(ctx context.Context, nodeNames []string) (map[string]bool, error) } diff --git a/pkg/provisioner/kubernetes/kubernetes_manager.go b/pkg/provisioner/kubernetes/kubernetes_manager.go index 0a4a53268..ef5eeb2d1 100644 --- a/pkg/provisioner/kubernetes/kubernetes_manager.go +++ b/pkg/provisioner/kubernetes/kubernetes_manager.go @@ -64,6 +64,9 @@ type BaseKubernetesManager struct { kustomizationWaitPollInterval time.Duration kustomizationReconcileTimeout time.Duration kustomizationReconcileSleep time.Duration + + healthCheckPollInterval time.Duration + nodeReadyPollInterval time.Duration } // NewKubernetesManager creates a new instance of BaseKubernetesManager @@ -74,6 +77,8 @@ func NewKubernetesManager(kubernetesClient client.KubernetesClient) *BaseKuberne kustomizationWaitPollInterval: 2 * time.Second, kustomizationReconcileTimeout: 5 * time.Minute, kustomizationReconcileSleep: 2 * time.Second, + healthCheckPollInterval: 10 * time.Second, + nodeReadyPollInterval: 5 * time.Second, } return manager @@ -543,7 +548,10 @@ func (k *BaseKubernetesManager) WaitForKubernetesHealthy(ctx context.Context, en deadline = time.Now().Add(5 * time.Minute) } - pollInterval := 10 * time.Second + pollInterval := k.healthCheckPollInterval + if pollInterval == 0 { + pollInterval = 10 * time.Second + } for time.Now().Before(deadline) { select { @@ -551,14 +559,22 @@ func (k *BaseKubernetesManager) WaitForKubernetesHealthy(ctx context.Context, en return fmt.Errorf("timeout waiting for Kubernetes API to be healthy") default: if err := k.client.CheckHealth(ctx, endpoint); err != nil { - time.Sleep(pollInterval) - continue + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for Kubernetes API to be healthy") + case <-time.After(pollInterval): + continue + } } if len(nodeNames) > 0 { if err := k.waitForNodesReady(ctx, nodeNames, outputFunc); err != nil { - time.Sleep(pollInterval) - continue + select { + case <-ctx.Done(): + return fmt.Errorf("timeout waiting for Kubernetes API to be healthy") + case <-time.After(pollInterval): + continue + } } } @@ -801,7 +817,10 @@ func (k *BaseKubernetesManager) waitForNodesReady(ctx context.Context, nodeNames deadline = time.Now().Add(5 * time.Minute) } - pollInterval := 5 * time.Second + pollInterval := k.nodeReadyPollInterval + if pollInterval == 0 { + pollInterval = 5 * time.Second + } lastStatus := make(map[string]string) for time.Now().Before(deadline) { @@ -811,8 +830,12 @@ func (k *BaseKubernetesManager) waitForNodesReady(ctx context.Context, nodeNames default: readyStatus, err := k.client.GetNodeReadyStatus(ctx, nodeNames) if err != nil { - time.Sleep(pollInterval) - continue + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for nodes to be ready") + case <-time.After(pollInterval): + continue + } } var missingNodes []string @@ -848,7 +871,12 @@ func (k *BaseKubernetesManager) waitForNodesReady(ctx context.Context, nodeNames return nil } - time.Sleep(pollInterval) + select { + case <-ctx.Done(): + return fmt.Errorf("context cancelled while waiting for nodes to be ready") + case <-time.After(pollInterval): + continue + } } } diff --git a/pkg/provisioner/kubernetes/kubernetes_manager_private_test.go b/pkg/provisioner/kubernetes/kubernetes_manager_private_test.go new file mode 100644 index 000000000..38f02dcdc --- /dev/null +++ b/pkg/provisioner/kubernetes/kubernetes_manager_private_test.go @@ -0,0 +1,456 @@ +package kubernetes + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) + +func TestBaseKubernetesManager_waitForNodesReady(t *testing.T) { + setup := func(t *testing.T) *BaseKubernetesManager { + t.Helper() + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + manager.nodeReadyPollInterval = 50 * time.Millisecond + return manager + } + + t.Run("Success", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": true, + "node2": true, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, outputFunc) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(output) == 0 { + t.Error("Expected output messages, got none") + } + }) + + t.Run("ContextCancelled", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": false, + "node2": false, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithCancel(context.Background()) + cancel() // Cancel immediately + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "context cancelled while waiting for nodes to be ready") { + t.Errorf("Expected context cancelled error, got: %v", err) + } + }) + + t.Run("MissingNodes", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": true, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for nodes to appear") && !strings.Contains(err.Error(), "context cancelled") { + t.Errorf("Expected missing nodes or context cancelled error, got: %v", err) + } + }) + + t.Run("NotReadyNodes", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": false, + "node2": false, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for nodes to be ready") && !strings.Contains(err.Error(), "context cancelled") { + t.Errorf("Expected not ready nodes or context cancelled error, got: %v", err) + } + }) + + t.Run("ContextWithoutDeadline", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": true, + "node2": true, + }, nil + } + manager.client = kubernetesClient + + ctx := context.Background() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, outputFunc) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("GetNodeReadyStatusErrorDuringPolling", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + callCount++ + if callCount == 1 { + return nil, fmt.Errorf("temporary error") + } + return map[string]bool{ + "node1": true, + "node2": true, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err != nil { + t.Errorf("Expected no error after retry, got %v", err) + } + if callCount < 2 { + t.Error("Expected GetNodeReadyStatus to be called multiple times") + } + }) + + t.Run("OutputFuncWithStatusTransitions", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + callCount++ + switch callCount { + case 1: + return map[string]bool{}, nil + case 2: + return map[string]bool{"node1": false}, nil + case 3: + return map[string]bool{"node1": true}, nil + default: + return map[string]bool{"node1": true}, nil + } + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.waitForNodesReady(ctx, []string{"node1"}, outputFunc) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + foundNotFound := false + foundNotReady := false + foundReady := false + for _, msg := range output { + if strings.Contains(msg, "NOT FOUND") { + foundNotFound = true + } + if strings.Contains(msg, "NOT READY") { + foundNotReady = true + } + if strings.Contains(msg, "READY") { + foundReady = true + } + } + + if !foundNotFound { + t.Error("Expected output message for NOT FOUND status") + } + if !foundNotReady { + t.Error("Expected output message for NOT READY status") + } + if !foundReady { + t.Error("Expected output message for READY status") + } + }) + + t.Run("OutputFuncNoStatusChange", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{"node1": false}, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.waitForNodesReady(ctx, []string{"node1"}, outputFunc) + if err == nil { + t.Error("Expected error, got nil") + } + + notReadyCount := 0 + for _, msg := range output { + if strings.Contains(msg, "NOT READY") { + notReadyCount++ + } + } + + if notReadyCount != 1 { + t.Errorf("Expected 1 NOT READY message, got %d", notReadyCount) + } + }) + + t.Run("TimeoutWithFinalGetNodeReadyStatusError", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + callCount++ + if callCount == 1 { + return map[string]bool{"node1": false}, nil + } + return nil, fmt.Errorf("final status error") + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get final status") && !strings.Contains(err.Error(), "context cancelled") { + t.Errorf("Expected error about final status or context cancelled, got: %v", err) + } + }) + + t.Run("TimeoutWithBothMissingAndNotReadyNodes", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": false, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for nodes to appear") && !strings.Contains(err.Error(), "context cancelled") { + t.Errorf("Expected missing nodes or context cancelled error, got: %v", err) + } + }) + + t.Run("TimeoutFallbackError", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": true, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for nodes") && !strings.Contains(err.Error(), "context cancelled") { + t.Errorf("Expected timeout or context cancelled error, got: %v", err) + } + }) + + t.Run("MultipleNodesWithMixedStatus", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + callCount++ + if callCount == 1 { + return map[string]bool{ + "node1": false, + "node2": true, + }, nil + } + return map[string]bool{ + "node1": true, + "node2": true, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, outputFunc) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + + foundNotReady := false + for _, msg := range output { + if strings.Contains(msg, "node1") && strings.Contains(msg, "NOT READY") { + foundNotReady = true + } + } + + if !foundNotReady { + t.Error("Expected output message for node1 NOT READY status") + } + }) + + t.Run("EmptyNodeNames", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{}, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + err := manager.waitForNodesReady(ctx, []string{}, nil) + if err != nil { + t.Errorf("Expected no error for empty node names, got %v", err) + } + }) + +} + +func TestBaseKubernetesManager_getHelmRelease(t *testing.T) { + setup := func(t *testing.T) *BaseKubernetesManager { + t.Helper() + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + return manager + } + + t.Run("Success", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + expectedObj := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "helm.toolkit.fluxcd.io/v2", + "kind": "HelmRelease", + "metadata": map[string]any{ + "name": "test-release", + "namespace": "test-namespace", + }, + "spec": map[string]any{ + "chart": map[string]any{ + "spec": map[string]any{ + "chart": "test-chart", + }, + }, + }, + }, + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { + return expectedObj, nil + } + manager.client = kubernetesClient + + release, err := manager.getHelmRelease("test-release", "test-namespace") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if release == nil { + t.Error("Expected helm release, got nil") + } + if release.Name != "test-release" { + t.Errorf("Expected name 'test-release', got %s", release.Name) + } + }) + + t.Run("GetResourceError", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("get resource error") + } + manager.client = kubernetesClient + + _, err := manager.getHelmRelease("test-release", "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get helm release") { + t.Errorf("Expected get resource error, got: %v", err) + } + }) +} diff --git a/pkg/provisioner/kubernetes/kubernetes_manager_test.go b/pkg/provisioner/kubernetes/kubernetes_manager_public_test.go similarity index 80% rename from pkg/provisioner/kubernetes/kubernetes_manager_test.go rename to pkg/provisioner/kubernetes/kubernetes_manager_public_test.go index 547a35e4a..d9ef5ea39 100644 --- a/pkg/provisioner/kubernetes/kubernetes_manager_test.go +++ b/pkg/provisioner/kubernetes/kubernetes_manager_public_test.go @@ -16,26 +16,20 @@ import ( "github.com/windsorcli/cli/pkg/provisioner/kubernetes/client" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" ) -// SetupOptions holds test-specific options for setup -type SetupOptions struct { -} - -// Mocks holds all mock dependencies for tests -type Mocks struct { +// KubernetesTestMocks contains all the mock dependencies for testing the KubernetesManager +type KubernetesTestMocks struct { Shims *Shims KubernetesClient client.KubernetesClient } -// setupMocks initializes and returns mock dependencies for tests -func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +// setupKubernetesMocks creates mock components for testing the KubernetesManager with optional overrides +func setupKubernetesMocks(t *testing.T, opts ...func(*KubernetesTestMocks)) *KubernetesTestMocks { t.Helper() - if opts == nil { - opts = []*SetupOptions{{}} - } kubernetesClient := client.NewMockKubernetesClient() kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { @@ -45,17 +39,21 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return &unstructured.Unstructured{}, nil } - mocks := &Mocks{ - Shims: setupShims(t), + mocks := &KubernetesTestMocks{ + Shims: setupDefaultShims(), KubernetesClient: kubernetesClient, } + // Apply any overrides + for _, opt := range opts { + opt(mocks) + } + return mocks } -// setupShims initializes and returns shims for tests -func setupShims(t *testing.T) *Shims { - t.Helper() +// setupDefaultShims initializes and returns shims with default test configurations +func setupDefaultShims() *Shims { shims := NewShims() shims.ToUnstructured = func(obj any) (map[string]any, error) { return nil, fmt.Errorf("forced conversion error") @@ -66,7 +64,7 @@ func setupShims(t *testing.T) *Shims { func TestBaseKubernetesManager_ApplyKustomization(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) // Use shorter timeouts for tests manager.kustomizationWaitPollInterval = 50 * time.Millisecond @@ -113,6 +111,9 @@ func TestBaseKubernetesManager_ApplyKustomization(t *testing.T) { if err == nil { t.Error("Expected error, got nil") } + if !strings.Contains(err.Error(), "failed to convert kustomization to unstructured") { + t.Errorf("Expected conversion error, got: %v", err) + } }) t.Run("ApplyWithRetryError", func(t *testing.T) { @@ -141,12 +142,59 @@ func TestBaseKubernetesManager_ApplyKustomization(t *testing.T) { t.Error("Expected error, got nil") } }) + + t.Run("ApplyWithRetryExistingResourceConversionError", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + existingObj := &unstructured.Unstructured{ + Object: map[string]any{ + "apiVersion": "kustomize.toolkit.fluxcd.io/v1", + "kind": "Kustomization", + "metadata": map[string]any{ + "name": "test-kustomization", + "namespace": "test-namespace", + "resourceVersion": "123", + }, + }, + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return existingObj, nil + } + callCount := 0 + originalToUnstructured := manager.shims.ToUnstructured + manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { + callCount++ + if callCount == 1 { + return originalToUnstructured(obj) + } + return nil, fmt.Errorf("conversion error") + } + manager.client = kubernetesClient + + kustomization := kustomizev1.Kustomization{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-kustomization", + Namespace: "test-namespace", + }, + Spec: kustomizev1.KustomizationSpec{ + Path: "./test-path", + }, + } + + err := manager.ApplyKustomization(kustomization) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to convert existing object to unstructured") { + t.Errorf("Expected conversion error, got: %v", err) + } + }) } func TestBaseKubernetesManager_DeleteKustomization(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) // Use shorter timeouts for tests manager.kustomizationWaitPollInterval = 50 * time.Millisecond @@ -201,6 +249,50 @@ func TestBaseKubernetesManager_DeleteKustomization(t *testing.T) { } }) + t.Run("TimeoutWaitingForDeletion", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.DeleteResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string, opts metav1.DeleteOptions) error { + return nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { + return &unstructured.Unstructured{}, nil + } + manager.client = kubernetesClient + manager.kustomizationReconcileTimeout = 100 * time.Millisecond + manager.kustomizationWaitPollInterval = 50 * time.Millisecond + + err := manager.DeleteKustomization("test-kustomization", "test-namespace") + if err == nil { + t.Error("Expected timeout error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for kustomization") { + t.Errorf("Expected timeout error, got: %v", err) + } + }) + + t.Run("ErrorCheckingDeletionStatus", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.DeleteResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string, opts metav1.DeleteOptions) error { + return nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("error checking status") + } + manager.client = kubernetesClient + manager.kustomizationReconcileTimeout = 100 * time.Millisecond + manager.kustomizationWaitPollInterval = 50 * time.Millisecond + + err := manager.DeleteKustomization("test-kustomization", "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error checking kustomization deletion status") { + t.Errorf("Expected error checking status, got: %v", err) + } + }) + t.Run("UsesCorrectDeleteOptions", func(t *testing.T) { manager := setup(t) kubernetesClient := client.NewMockKubernetesClient() @@ -232,7 +324,7 @@ func TestBaseKubernetesManager_DeleteKustomization(t *testing.T) { func TestBaseKubernetesManager_WaitForKustomizations(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) // Use shorter timeouts for tests manager.kustomizationWaitPollInterval = 50 * time.Millisecond @@ -408,7 +500,7 @@ func TestBaseKubernetesManager_WaitForKustomizations(t *testing.T) { func TestBaseKubernetesManager_CreateNamespace(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -451,7 +543,7 @@ func TestBaseKubernetesManager_CreateNamespace(t *testing.T) { func TestBaseKubernetesManager_DeleteNamespace(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -513,7 +605,7 @@ func TestBaseKubernetesManager_DeleteNamespace(t *testing.T) { func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) // Use shorter timeouts for tests manager.kustomizationWaitPollInterval = 50 * time.Millisecond @@ -688,7 +780,7 @@ func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { t.Run("FromUnstructuredError", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -735,7 +827,7 @@ func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { t.Run("KustomizationNotReady", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -776,7 +868,7 @@ func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { t.Run("KustomizationFailed", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -822,7 +914,7 @@ func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { t.Run("KustomizationMissing", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1030,7 +1122,7 @@ func TestBaseKubernetesManager_ApplyConfigMap(t *testing.T) { func TestBaseKubernetesManager_GetHelmReleasesForKustomization(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -1138,7 +1230,7 @@ func TestBaseKubernetesManager_GetHelmReleasesForKustomization(t *testing.T) { func TestBaseKubernetesManager_SuspendKustomization(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -1233,7 +1325,7 @@ func TestBaseKubernetesManager_SuspendKustomization(t *testing.T) { func TestBaseKubernetesManager_SuspendHelmRelease(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -1328,7 +1420,7 @@ func TestBaseKubernetesManager_SuspendHelmRelease(t *testing.T) { func TestBaseKubernetesManager_ApplyGitRepository(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -1461,7 +1553,7 @@ func TestBaseKubernetesManager_ApplyGitRepository(t *testing.T) { func TestBaseKubernetesManager_CheckGitRepositoryStatus(t *testing.T) { t.Run("Success", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1500,7 +1592,7 @@ func TestBaseKubernetesManager_CheckGitRepositoryStatus(t *testing.T) { t.Run("ListResourcesError", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1521,7 +1613,7 @@ func TestBaseKubernetesManager_CheckGitRepositoryStatus(t *testing.T) { t.Run("FromUnstructuredError", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1566,7 +1658,7 @@ func TestBaseKubernetesManager_CheckGitRepositoryStatus(t *testing.T) { t.Run("RepositoryNotReady", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1605,12 +1697,123 @@ func TestBaseKubernetesManager_CheckGitRepositoryStatus(t *testing.T) { t.Errorf("Expected error containing repo name and message, got %v", err) } }) + + t.Run("OCIRepositoryNotReady", func(t *testing.T) { + manager := func(t *testing.T) *BaseKubernetesManager { + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + return manager + }(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.ListResourcesFunc = func(gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) { + callCount++ + if gvr.Resource == "gitrepositories" { + return &unstructured.UnstructuredList{Items: []unstructured.Unstructured{}}, nil + } + return &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]any{ + "apiVersion": "source.toolkit.fluxcd.io/v1", + "kind": "OCIRepository", + "metadata": map[string]any{ + "name": "oci-repo1", + }, + "status": map[string]any{ + "conditions": []any{ + map[string]any{ + "type": "Ready", + "status": "False", + "message": "oci repo not ready", + }, + }, + }, + }, + }, + }, + }, nil + } + manager.client = kubernetesClient + + err := manager.CheckGitRepositoryStatus() + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "oci-repo1") || !strings.Contains(err.Error(), "oci repo not ready") { + t.Errorf("Expected error containing oci repo name and message, got %v", err) + } + }) + + t.Run("OCIRepositoryListError", func(t *testing.T) { + manager := func(t *testing.T) *BaseKubernetesManager { + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + return manager + }(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.ListResourcesFunc = func(gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) { + callCount++ + if gvr.Resource == "gitrepositories" { + return &unstructured.UnstructuredList{Items: []unstructured.Unstructured{}}, nil + } + return nil, fmt.Errorf("oci list error") + } + manager.client = kubernetesClient + + err := manager.CheckGitRepositoryStatus() + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to list oci repositories") { + t.Errorf("Expected oci list error, got %v", err) + } + }) + + t.Run("OCIRepositoryFromUnstructuredError", func(t *testing.T) { + manager := func(t *testing.T) *BaseKubernetesManager { + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + return manager + }(t) + kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 + kubernetesClient.ListResourcesFunc = func(gvr schema.GroupVersionResource, namespace string) (*unstructured.UnstructuredList, error) { + callCount++ + if gvr.Resource == "gitrepositories" { + return &unstructured.UnstructuredList{Items: []unstructured.Unstructured{}}, nil + } + return &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]any{ + "apiVersion": "source.toolkit.fluxcd.io/v1", + "kind": "OCIRepository", + }, + }, + }, + }, nil + } + manager.client = kubernetesClient + manager.shims.FromUnstructured = func(obj map[string]any, target any) error { + return fmt.Errorf("forced conversion error") + } + + err := manager.CheckGitRepositoryStatus() + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to convert oci repository") { + t.Errorf("Expected oci conversion error, got %v", err) + } + }) } func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("Success", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1651,7 +1854,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("ListResourcesError", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1675,7 +1878,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("FromUnstructuredError", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1722,7 +1925,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("KustomizationNotReady", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1763,7 +1966,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("KustomizationFailed", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1809,7 +2012,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("KustomizationArtifactFailed", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1855,7 +2058,7 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { t.Run("KustomizationMissing", func(t *testing.T) { manager := func(t *testing.T) *BaseKubernetesManager { - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager }(t) @@ -1901,8 +2104,10 @@ func TestBaseKubernetesManager_GetKustomizationStatus(t *testing.T) { func TestBaseKubernetesManager_WaitForKubernetesHealthy(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) + manager.healthCheckPollInterval = 50 * time.Millisecond + manager.nodeReadyPollInterval = 50 * time.Millisecond return manager } @@ -1942,8 +2147,15 @@ func TestBaseKubernetesManager_WaitForKubernetesHealthy(t *testing.T) { t.Run("ContextCancelled", func(t *testing.T) { manager := setup(t) kubernetesClient := client.NewMockKubernetesClient() + callCount := 0 kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { - return fmt.Errorf("health check failed") + callCount++ + select { + case <-ctx.Done(): + return ctx.Err() + default: + return fmt.Errorf("health check failed") + } } manager.client = kubernetesClient @@ -1958,100 +2170,221 @@ func TestBaseKubernetesManager_WaitForKubernetesHealthy(t *testing.T) { t.Errorf("Expected timeout error, got: %v", err) } }) -} - -func TestBaseKubernetesManager_ApplyOCIRepository(t *testing.T) { - setup := func(t *testing.T) *BaseKubernetesManager { - t.Helper() - mocks := setupMocks(t) - manager := NewKubernetesManager(mocks.KubernetesClient) - return manager - } - t.Run("Success", func(t *testing.T) { + t.Run("HealthCheckFailsThenSucceeds", func(t *testing.T) { manager := setup(t) kubernetesClient := client.NewMockKubernetesClient() - kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { - return nil, fmt.Errorf("not found") - } - kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { - return obj, nil + callCount := 0 + kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { + callCount++ + if callCount == 1 { + return fmt.Errorf("health check failed") + } + return nil } manager.client = kubernetesClient - manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { - return map[string]any{ - "apiVersion": "source.toolkit.fluxcd.io/v1", - "kind": "OCIRepository", - "metadata": map[string]any{ - "name": "test-repo", - "namespace": "test-namespace", - }, - "spec": map[string]any{ - "url": "oci://test-registry.com/test-image", - }, - }, nil - } - repo := &sourcev1.OCIRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - Spec: sourcev1.OCIRepositorySpec{ - URL: "oci://test-registry.com/test-image", - }, - } + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() - err := manager.ApplyOCIRepository(repo) + err := manager.WaitForKubernetesHealthy(ctx, "https://test-endpoint:6443", nil) if err != nil { - t.Errorf("Expected no error, got %v", err) + t.Errorf("Expected no error after retry, got %v", err) + } + if callCount < 2 { + t.Error("Expected CheckHealth to be called multiple times") } }) - t.Run("ToUnstructuredError", func(t *testing.T) { + t.Run("TimeoutWaitingForHealth", func(t *testing.T) { manager := setup(t) - manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { - return nil, fmt.Errorf("conversion error") + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { + return fmt.Errorf("health check failed") } + manager.client = kubernetesClient - repo := &sourcev1.OCIRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "test-repo", - Namespace: "test-namespace", - }, - Spec: sourcev1.OCIRepositorySpec{ - URL: "oci://test-registry.com/test-image", - }, - } + ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) + defer cancel() - err := manager.ApplyOCIRepository(repo) + err := manager.WaitForKubernetesHealthy(ctx, "https://test-endpoint:6443", nil) if err == nil { t.Error("Expected error, got nil") } - if !strings.Contains(err.Error(), "failed to convert ocirepository to unstructured") { - t.Errorf("Expected conversion error, got: %v", err) + if !strings.Contains(err.Error(), "timeout waiting for Kubernetes API to be healthy") { + t.Errorf("Expected timeout error, got: %v", err) } }) - t.Run("ValidationError", func(t *testing.T) { + t.Run("SuccessWithNodeNames", func(t *testing.T) { manager := setup(t) - manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { - return map[string]any{ - "metadata": map[string]any{ - "name": "", - }, + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { + return nil + } + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": true, + "node2": true, }, nil } + manager.client = kubernetesClient - repo := &sourcev1.OCIRepository{ - ObjectMeta: metav1.ObjectMeta{ - Name: "", - Namespace: "test-namespace", - }, - Spec: sourcev1.OCIRepositorySpec{ - URL: "oci://test-registry.com/test-image", - }, - } + ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) + defer cancel() + + var output []string + outputFunc := func(msg string) { + output = append(output, msg) + } + + err := manager.WaitForKubernetesHealthy(ctx, "https://test-endpoint:6443", outputFunc, "node1", "node2") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("HealthCheckSucceedsButNodesNotReady", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { + return nil + } + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return map[string]bool{ + "node1": false, + }, nil + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.WaitForKubernetesHealthy(ctx, "https://test-endpoint:6443", nil, "node1") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for Kubernetes API to be healthy") { + t.Errorf("Expected timeout error, got: %v", err) + } + }) + + t.Run("HealthCheckSucceedsButWaitForNodesReadyError", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.CheckHealthFunc = func(ctx context.Context, endpoint string) error { + return nil + } + kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { + return nil, fmt.Errorf("node status error") + } + manager.client = kubernetesClient + + ctx, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond) + defer cancel() + + err := manager.WaitForKubernetesHealthy(ctx, "https://test-endpoint:6443", nil, "node1") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "timeout waiting for Kubernetes API to be healthy") { + t.Errorf("Expected timeout error, got: %v", err) + } + }) +} + +func TestBaseKubernetesManager_ApplyOCIRepository(t *testing.T) { + setup := func(t *testing.T) *BaseKubernetesManager { + t.Helper() + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + return manager + } + + t.Run("Success", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + return obj, nil + } + manager.client = kubernetesClient + manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { + return map[string]any{ + "apiVersion": "source.toolkit.fluxcd.io/v1", + "kind": "OCIRepository", + "metadata": map[string]any{ + "name": "test-repo", + "namespace": "test-namespace", + }, + "spec": map[string]any{ + "url": "oci://test-registry.com/test-image", + }, + }, nil + } + + repo := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "test-namespace", + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: "oci://test-registry.com/test-image", + }, + } + + err := manager.ApplyOCIRepository(repo) + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ToUnstructuredError", func(t *testing.T) { + manager := setup(t) + manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { + return nil, fmt.Errorf("conversion error") + } + + repo := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + Namespace: "test-namespace", + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: "oci://test-registry.com/test-image", + }, + } + + err := manager.ApplyOCIRepository(repo) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to convert ocirepository to unstructured") { + t.Errorf("Expected conversion error, got: %v", err) + } + }) + + t.Run("ValidationError", func(t *testing.T) { + manager := setup(t) + manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { + return map[string]any{ + "metadata": map[string]any{ + "name": "", + }, + }, nil + } + + repo := &sourcev1.OCIRepository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "", + Namespace: "test-namespace", + }, + Spec: sourcev1.OCIRepositorySpec{ + URL: "oci://test-registry.com/test-image", + }, + } err := manager.ApplyOCIRepository(repo) if err == nil { @@ -2120,7 +2453,7 @@ func TestBaseKubernetesManager_ApplyOCIRepository(t *testing.T) { func TestBaseKubernetesManager_GetNodeReadyStatus(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) return manager } @@ -2180,156 +2513,10 @@ func TestBaseKubernetesManager_GetNodeReadyStatus(t *testing.T) { }) } -func TestBaseKubernetesManager_waitForNodesReady(t *testing.T) { - setup := func(t *testing.T) *BaseKubernetesManager { - t.Helper() - mocks := setupMocks(t) - manager := NewKubernetesManager(mocks.KubernetesClient) - return manager - } - - t.Run("Success", func(t *testing.T) { - manager := setup(t) - kubernetesClient := client.NewMockKubernetesClient() - kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { - return map[string]bool{ - "node1": true, - "node2": true, - }, nil - } - manager.client = kubernetesClient - - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - defer cancel() - - var output []string - outputFunc := func(msg string) { - output = append(output, msg) - } - - err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, outputFunc) - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if len(output) == 0 { - t.Error("Expected output messages, got none") - } - }) - - t.Run("ContextCancelled", func(t *testing.T) { - manager := setup(t) - kubernetesClient := client.NewMockKubernetesClient() - kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { - return map[string]bool{ - "node1": false, - "node2": false, - }, nil - } - manager.client = kubernetesClient - - ctx, cancel := context.WithCancel(context.Background()) - cancel() // Cancel immediately - - err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "context cancelled while waiting for nodes to be ready") { - t.Errorf("Expected context cancelled error, got: %v", err) - } - }) - - t.Run("MissingNodes", func(t *testing.T) { - manager := setup(t) - kubernetesClient := client.NewMockKubernetesClient() - kubernetesClient.GetNodeReadyStatusFunc = func(ctx context.Context, nodeNames []string) (map[string]bool, error) { - return map[string]bool{ - "node1": true, - }, nil - } - manager.client = kubernetesClient - - ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) - defer cancel() - - err := manager.waitForNodesReady(ctx, []string{"node1", "node2"}, nil) - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "timeout waiting for nodes to appear") { - t.Errorf("Expected missing nodes error, got: %v", err) - } - }) - -} - -func TestBaseKubernetesManager_getHelmRelease(t *testing.T) { - setup := func(t *testing.T) *BaseKubernetesManager { - t.Helper() - mocks := setupMocks(t) - manager := NewKubernetesManager(mocks.KubernetesClient) - return manager - } - - t.Run("Success", func(t *testing.T) { - manager := setup(t) - kubernetesClient := client.NewMockKubernetesClient() - expectedObj := &unstructured.Unstructured{ - Object: map[string]any{ - "apiVersion": "helm.toolkit.fluxcd.io/v2", - "kind": "HelmRelease", - "metadata": map[string]any{ - "name": "test-release", - "namespace": "test-namespace", - }, - "spec": map[string]any{ - "chart": map[string]any{ - "spec": map[string]any{ - "chart": "test-chart", - }, - }, - }, - }, - } - kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { - return expectedObj, nil - } - manager.client = kubernetesClient - - release, err := manager.getHelmRelease("test-release", "test-namespace") - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if release == nil { - t.Error("Expected helm release, got nil") - } - if release.Name != "test-release" { - t.Errorf("Expected name 'test-release', got %s", release.Name) - } - }) - - t.Run("GetResourceError", func(t *testing.T) { - manager := setup(t) - kubernetesClient := client.NewMockKubernetesClient() - kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, namespace, name string) (*unstructured.Unstructured, error) { - return nil, fmt.Errorf("get resource error") - } - manager.client = kubernetesClient - - _, err := manager.getHelmRelease("test-release", "test-namespace") - if err == nil { - t.Error("Expected error, got nil") - } - if !strings.Contains(err.Error(), "failed to get helm release") { - t.Errorf("Expected get resource error, got: %v", err) - } - }) -} - func TestBaseKubernetesManager_DeleteBlueprint(t *testing.T) { setup := func(t *testing.T) *BaseKubernetesManager { t.Helper() - mocks := setupMocks(t) + mocks := setupKubernetesMocks(t) manager := NewKubernetesManager(mocks.KubernetesClient) manager.kustomizationWaitPollInterval = 50 * time.Millisecond manager.kustomizationReconcileTimeout = 100 * time.Millisecond @@ -2723,3 +2910,287 @@ func TestBaseKubernetesManager_DeleteBlueprint(t *testing.T) { } }) } + +func TestBaseKubernetesManager_ApplyBlueprint(t *testing.T) { + setup := func(t *testing.T) *BaseKubernetesManager { + t.Helper() + mocks := setupKubernetesMocks(t) + manager := NewKubernetesManager(mocks.KubernetesClient) + manager.shims = mocks.Shims + manager.shims.ToUnstructured = func(obj any) (map[string]any, error) { + return runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + } + manager.shims.FromUnstructured = func(obj map[string]any, target any) error { + return runtime.DefaultUnstructuredConverter.FromUnstructured(obj, target) + } + return manager + } + + t.Run("Success", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Sources: []blueprintv1alpha1.Source{ + { + Name: "test-source", + Url: "https://github.com/example/repo.git", + Ref: blueprintv1alpha1.Reference{Branch: "main"}, + }, + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ErrorOnCreateNamespace", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + if obj.GetKind() == "Namespace" { + return nil, fmt.Errorf("namespace creation failed") + } + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to create namespace") { + t.Errorf("Expected error about namespace creation, got %v", err) + } + }) + + t.Run("SuccessWithRepository", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Repository: blueprintv1alpha1.Repository{ + Url: "https://github.com/example/repo.git", + Ref: blueprintv1alpha1.Reference{Branch: "main"}, + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("ErrorOnApplyBlueprintRepository", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + if obj.GetKind() == "GitRepository" { + return nil, fmt.Errorf("git repository apply failed") + } + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Repository: blueprintv1alpha1.Repository{ + Url: "https://github.com/example/repo.git", + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to apply blueprint repository") { + t.Errorf("Expected error about blueprint repository, got %v", err) + } + }) + + t.Run("ErrorOnApplySource", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + if obj.GetKind() == "GitRepository" { + return nil, fmt.Errorf("git repository apply failed") + } + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Sources: []blueprintv1alpha1.Source{ + { + Name: "test-source", + Url: "https://github.com/example/repo.git", + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to apply source") { + t.Errorf("Expected error about applying source, got %v", err) + } + }) + + t.Run("ErrorOnApplyConfigMap", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + if obj.GetKind() == "ConfigMap" { + return nil, fmt.Errorf("configmap apply failed") + } + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + Substitutions: map[string]string{ + "key": "value", + }, + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to create ConfigMap") { + t.Errorf("Expected error about ConfigMap, got %v", err) + } + }) + + t.Run("ErrorOnApplyKustomization", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + if obj.GetKind() == "Kustomization" { + return nil, fmt.Errorf("kustomization apply failed") + } + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to apply kustomization") { + t.Errorf("Expected error about kustomization, got %v", err) + } + }) + + t.Run("SuccessWithOCISource", func(t *testing.T) { + manager := setup(t) + kubernetesClient := client.NewMockKubernetesClient() + kubernetesClient.ApplyResourceFunc = func(gvr schema.GroupVersionResource, obj *unstructured.Unstructured, opts metav1.ApplyOptions) (*unstructured.Unstructured, error) { + return obj, nil + } + kubernetesClient.GetResourceFunc = func(gvr schema.GroupVersionResource, ns, name string) (*unstructured.Unstructured, error) { + return nil, fmt.Errorf("not found") + } + manager.client = kubernetesClient + + blueprint := &blueprintv1alpha1.Blueprint{ + Metadata: blueprintv1alpha1.Metadata{ + Name: "test-blueprint", + }, + Sources: []blueprintv1alpha1.Source{ + { + Name: "oci-source", + Url: "oci://example.com/repo", + }, + }, + Kustomizations: []blueprintv1alpha1.Kustomization{ + { + Name: "test-kustomization", + }, + }, + } + + err := manager.ApplyBlueprint(blueprint, "test-namespace") + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) +} From aab2ebe68accd040aed0543f9fd43645e903a169 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sat, 15 Nov 2025 16:27:55 -0500 Subject: [PATCH 4/4] Enhance provisioner/terraform coverage Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/provisioner/terraform/stack_test.go | 244 ++++++++++++++++++++++-- 1 file changed, 225 insertions(+), 19 deletions(-) diff --git a/pkg/provisioner/terraform/stack_test.go b/pkg/provisioner/terraform/stack_test.go index 98f5c0b09..53dc77444 100644 --- a/pkg/provisioner/terraform/stack_test.go +++ b/pkg/provisioner/terraform/stack_test.go @@ -56,7 +56,7 @@ func createTestBlueprint() *blueprintv1alpha1.Blueprint { } } -type Mocks struct { +type TerraformTestMocks struct { ConfigHandler config.ConfigHandler Shell *shell.MockShell Blueprint *blueprint.MockBlueprintHandler @@ -70,8 +70,8 @@ type SetupOptions struct { ConfigStr string } -// setupMocks creates mock components for testing the stack -func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +// setupTerraformMocks creates mock components for testing the stack +func setupTerraformMocks(t *testing.T, opts ...*SetupOptions) *TerraformTestMocks { t.Helper() origDir, err := os.Getwd() @@ -177,7 +177,7 @@ contexts: Shell: mockShell, } - return &Mocks{ + return &TerraformTestMocks{ ConfigHandler: configHandler, Shell: mockShell, Blueprint: mockBlueprint, @@ -188,9 +188,9 @@ contexts: } // setupWindsorStackMocks creates mock components for testing the WindsorStack -func setupWindsorStackMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupWindsorStackMocks(t *testing.T, opts ...*SetupOptions) *TerraformTestMocks { t.Helper() - mocks := setupMocks(t, opts...) + mocks := setupTerraformMocks(t, opts...) projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") tfModulesDir := filepath.Join(projectRoot, ".windsor", ".tf_modules", "remote", "path") @@ -221,9 +221,9 @@ func setupWindsorStackMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // ============================================================================= func TestStack_NewStack(t *testing.T) { - setup := func(t *testing.T) (*BaseStack, *Mocks) { + setup := func(t *testing.T) (*BaseStack, *TerraformTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) stack.shims = mocks.Shims return stack, mocks @@ -239,9 +239,9 @@ func TestStack_NewStack(t *testing.T) { } func TestStack_Initialize(t *testing.T) { - setup := func(t *testing.T) (*BaseStack, *Mocks) { + setup := func(t *testing.T) (*BaseStack, *TerraformTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) stack.shims = mocks.Shims return stack, mocks @@ -257,9 +257,9 @@ func TestStack_Initialize(t *testing.T) { } func TestStack_Up(t *testing.T) { - setup := func(t *testing.T) (*BaseStack, *Mocks) { + setup := func(t *testing.T) (*BaseStack, *TerraformTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) stack.shims = mocks.Shims return stack, mocks @@ -284,7 +284,7 @@ func TestStack_Up(t *testing.T) { }) t.Run("NilInjector", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) blueprint := createTestBlueprint() @@ -295,9 +295,9 @@ func TestStack_Up(t *testing.T) { } func TestStack_Down(t *testing.T) { - setup := func(t *testing.T) (*BaseStack, *Mocks) { + setup := func(t *testing.T) (*BaseStack, *TerraformTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) stack.shims = mocks.Shims return stack, mocks @@ -322,7 +322,7 @@ func TestStack_Down(t *testing.T) { }) t.Run("NilInjector", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupTerraformMocks(t) stack := NewBaseStack(mocks.Runtime, mocks.BlueprintHandler) blueprint := createTestBlueprint() @@ -343,7 +343,7 @@ func TestStack_Interface(t *testing.T) { // ============================================================================= func TestWindsorStack_NewWindsorStack(t *testing.T) { - setup := func(t *testing.T) (*WindsorStack, *Mocks) { + setup := func(t *testing.T) (*WindsorStack, *TerraformTestMocks) { t.Helper() mocks := setupWindsorStackMocks(t) stack := NewWindsorStack(mocks.Runtime, mocks.BlueprintHandler) @@ -360,7 +360,7 @@ func TestWindsorStack_NewWindsorStack(t *testing.T) { } func TestWindsorStack_Up(t *testing.T) { - setup := func(t *testing.T) (*WindsorStack, *Mocks) { + setup := func(t *testing.T) (*WindsorStack, *TerraformTestMocks) { t.Helper() mocks := setupWindsorStackMocks(t) stack := NewWindsorStack(mocks.Runtime, mocks.BlueprintHandler) @@ -471,10 +471,101 @@ func TestWindsorStack_Up(t *testing.T) { t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) } }) + + t.Run("NilBlueprint", func(t *testing.T) { + stack, _ := setup(t) + err := stack.Up(nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "blueprint not provided") { + t.Errorf("Expected blueprint not provided error, got: %v", err) + } + }) + + t.Run("EmptyProjectRoot", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Runtime.ProjectRoot = "" + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "project root is empty") { + t.Errorf("Expected project root error, got: %v", err) + } + }) + + t.Run("TerraformEnvNotAvailable", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Runtime.EnvPrinters.TerraformEnv = nil + stack.terraformEnv = nil + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "terraform environment printer not available") { + t.Errorf("Expected terraform env error, got: %v", err) + } + }) + + t.Run("ErrorUnsettingEnvVar", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Shims.Unsetenv = func(key string) error { + return fmt.Errorf("unsetenv error") + } + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error unsetting") { + t.Errorf("Expected unsetenv error, got: %v", err) + } + }) + + t.Run("ErrorSettingEnvVar", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Shims.Setenv = func(key, value string) error { + return fmt.Errorf("setenv error") + } + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error setting") { + t.Errorf("Expected setenv error, got: %v", err) + } + }) + + t.Run("ErrorRemovingBackendOverride", func(t *testing.T) { + stack, mocks := setup(t) + projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") + backendOverridePath := filepath.Join(projectRoot, ".windsor", ".tf_modules", "remote", "path", "backend_override.tf") + if err := os.MkdirAll(filepath.Dir(backendOverridePath), 0755); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if err := os.WriteFile(backendOverridePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create backend override file: %v", err) + } + mocks.Shims.Remove = func(path string) error { + return fmt.Errorf("remove error") + } + blueprint := createTestBlueprint() + err := stack.Up(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error removing backend override file") { + t.Errorf("Expected remove error, got: %v", err) + } + }) } func TestWindsorStack_Down(t *testing.T) { - setup := func(t *testing.T) (*WindsorStack, *Mocks) { + setup := func(t *testing.T) (*WindsorStack, *TerraformTestMocks) { t.Helper() mocks := setupWindsorStackMocks(t) stack := NewWindsorStack(mocks.Runtime, mocks.BlueprintHandler) @@ -633,4 +724,119 @@ func TestWindsorStack_Down(t *testing.T) { t.Fatalf("Expected error to contain %q, got %q", expectedError, err.Error()) } }) + + t.Run("NilBlueprint", func(t *testing.T) { + stack, _ := setup(t) + err := stack.Down(nil) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "blueprint not provided") { + t.Errorf("Expected blueprint not provided error, got: %v", err) + } + }) + + t.Run("EmptyProjectRoot", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Runtime.ProjectRoot = "" + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "project root is empty") { + t.Errorf("Expected project root error, got: %v", err) + } + }) + + t.Run("TerraformEnvNotAvailable", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Runtime.EnvPrinters.TerraformEnv = nil + stack.terraformEnv = nil + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "terraform environment printer not available") { + t.Errorf("Expected terraform env error, got: %v", err) + } + }) + + t.Run("ErrorUnsettingEnvVar", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Shims.Unsetenv = func(key string) error { + return fmt.Errorf("unsetenv error") + } + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error unsetting") { + t.Errorf("Expected unsetenv error, got: %v", err) + } + }) + + t.Run("ErrorSettingEnvVar", func(t *testing.T) { + stack, mocks := setup(t) + mocks.Shims.Setenv = func(key, value string) error { + return fmt.Errorf("setenv error") + } + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error setting") { + t.Errorf("Expected setenv error, got: %v", err) + } + }) + + t.Run("ErrorRemovingBackendOverride", func(t *testing.T) { + stack, mocks := setup(t) + projectRoot := os.Getenv("WINDSOR_PROJECT_ROOT") + backendOverridePath := filepath.Join(projectRoot, ".windsor", ".tf_modules", "remote", "path", "backend_override.tf") + if err := os.MkdirAll(filepath.Dir(backendOverridePath), 0755); err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if err := os.WriteFile(backendOverridePath, []byte("test"), 0644); err != nil { + t.Fatalf("Failed to create backend override file: %v", err) + } + mocks.Shims.Remove = func(path string) error { + return fmt.Errorf("remove error") + } + blueprint := createTestBlueprint() + err := stack.Down(blueprint) + if err == nil { + t.Error("Expected error, got nil") + } + if !strings.Contains(err.Error(), "error removing backend_override.tf") { + t.Errorf("Expected remove error, got: %v", err) + } + }) +} + +func TestNewShims(t *testing.T) { + t.Run("InitializesAllFields", func(t *testing.T) { + shims := NewShims() + if shims.Stat == nil { + t.Error("Expected Stat to be initialized") + } + if shims.Chdir == nil { + t.Error("Expected Chdir to be initialized") + } + if shims.Getwd == nil { + t.Error("Expected Getwd to be initialized") + } + if shims.Setenv == nil { + t.Error("Expected Setenv to be initialized") + } + if shims.Unsetenv == nil { + t.Error("Expected Unsetenv to be initialized") + } + if shims.Remove == nil { + t.Error("Expected Remove to be initialized") + } + }) }