From c451bdd54184b54b3ba41bf47395d69c9d33be51 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sat, 15 Nov 2025 15:19:26 -0500 Subject: [PATCH] chore(project): Enhance test coverage Increase test coverage and standardize style of project tests. Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/project/project_test.go | 340 +++++++++++++++++++++++++++++++++--- 1 file changed, 315 insertions(+), 25 deletions(-) diff --git a/pkg/project/project_test.go b/pkg/project/project_test.go index 5018f4832..8a19567ee 100644 --- a/pkg/project/project_test.go +++ b/pkg/project/project_test.go @@ -7,19 +7,23 @@ import ( "testing" "github.com/windsorcli/cli/pkg/composer" + "github.com/windsorcli/cli/pkg/composer/blueprint" "github.com/windsorcli/cli/pkg/provisioner" "github.com/windsorcli/cli/pkg/runtime" "github.com/windsorcli/cli/pkg/runtime/config" "github.com/windsorcli/cli/pkg/runtime/shell" "github.com/windsorcli/cli/pkg/runtime/tools" "github.com/windsorcli/cli/pkg/workstation" + "github.com/windsorcli/cli/pkg/workstation/network" + "github.com/windsorcli/cli/pkg/workstation/services" ) // ============================================================================= // Test Setup // ============================================================================= -type Mocks struct { +// ProjectTestMocks contains all the mock dependencies for testing the Project +type ProjectTestMocks struct { Runtime *runtime.Runtime ConfigHandler config.ConfigHandler Shell shell.Shell @@ -28,7 +32,8 @@ type Mocks struct { Provisioner *provisioner.Provisioner } -func setupMocks(t *testing.T) *Mocks { +// setupProjectMocks creates mock components for testing the Project with optional overrides +func setupProjectMocks(t *testing.T, opts ...func(*ProjectTestMocks)) *ProjectTestMocks { t.Helper() tmpDir := t.TempDir() @@ -113,13 +118,20 @@ func setupMocks(t *testing.T) *Mocks { comp := composer.NewComposer(rt) prov := provisioner.NewProvisioner(rt, comp.BlueprintHandler) - return &Mocks{ + mocks := &ProjectTestMocks{ Runtime: rt, ConfigHandler: configHandler, Shell: mockShell, Provisioner: prov, Composer: comp, } + + // Apply any overrides + for _, opt := range opts { + opt(mocks) + } + + return mocks } // ============================================================================= @@ -128,7 +140,7 @@ func setupMocks(t *testing.T) *Mocks { func TestNewProject(t *testing.T) { t.Run("CreatesProjectWithDependencies", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) @@ -158,7 +170,7 @@ func TestNewProject(t *testing.T) { }) t.Run("UsesProvidedContextName", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("custom-context", &Project{Runtime: mocks.Runtime}) @@ -172,7 +184,7 @@ func TestNewProject(t *testing.T) { }) t.Run("UsesConfigContextWhenContextNameEmpty", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("", &Project{Runtime: mocks.Runtime}) @@ -186,7 +198,7 @@ func TestNewProject(t *testing.T) { }) t.Run("UsesLocalWhenContextNameAndConfigContextEmpty", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.GetContextFunc = func() string { return "" @@ -204,7 +216,7 @@ func TestNewProject(t *testing.T) { }) t.Run("CreatesWorkstationWhenDevMode", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.IsDevModeFunc = func(contextName string) bool { return true @@ -222,7 +234,7 @@ func TestNewProject(t *testing.T) { }) t.Run("SkipsWorkstationWhenNotDevMode", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.IsDevModeFunc = func(contextName string) bool { return false @@ -240,7 +252,7 @@ func TestNewProject(t *testing.T) { }) t.Run("ErrorOnContextInitializationFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockShell := mocks.Shell.(*shell.MockShell) mockShell.GetProjectRootFunc = func() (string, error) { return "", fmt.Errorf("failed to get project root") @@ -265,6 +277,173 @@ func TestNewProject(t *testing.T) { } }) + t.Run("ErrorOnApplyConfigDefaultsFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsLoadedFunc = func() bool { + return false + } + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + mockConfig.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + mockConfig.SetFunc = func(key string, value any) error { + if key == "dev" { + return fmt.Errorf("set dev failed") + } + return nil + } + + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + + if err == nil { + t.Fatal("Expected error for ApplyConfigDefaults failure") + } + if proj != nil { + t.Error("Expected Project to be nil on error") + } + }) + + t.Run("ErrorOnWorkstationCreationFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + + mocks.Runtime.Shell = nil + + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + + if err == nil { + t.Fatal("Expected error for workstation creation failure") + } + if proj != nil { + t.Error("Expected Project to be nil on error") + } + if !strings.Contains(err.Error(), "failed to create workstation") { + t.Errorf("Expected error about workstation creation, got: %v", err) + } + }) + + t.Run("HandlesComposerOverride", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockComposer := composer.NewComposer(mocks.Runtime) + + proj, err := NewProject("test-context", &Project{ + Runtime: mocks.Runtime, + Composer: mockComposer, + }) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if proj.Composer != mockComposer { + t.Error("Expected Composer override to be used") + } + }) + + t.Run("HandlesProvisionerOverride", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockProvisioner := provisioner.NewProvisioner(mocks.Runtime, mocks.Composer.BlueprintHandler) + + proj, err := NewProject("test-context", &Project{ + Runtime: mocks.Runtime, + Provisioner: mockProvisioner, + }) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if proj.Provisioner != mockProvisioner { + t.Error("Expected Provisioner override to be used") + } + }) + + t.Run("HandlesWorkstationOverride", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + + mockWorkstation, err := workstation.NewWorkstation(mocks.Runtime) + if err != nil { + t.Fatalf("Failed to create workstation: %v", err) + } + + proj, err := NewProject("test-context", &Project{ + Runtime: mocks.Runtime, + Workstation: mockWorkstation, + }) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if proj.Workstation != mockWorkstation { + t.Error("Expected Workstation override to be used") + } + }) + + t.Run("CreatesRuntimeWhenNoOverrides", func(t *testing.T) { + proj, err := NewProject("test-context") + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if proj == nil { + t.Fatal("Expected Project to be created") + } + if proj.Runtime == nil { + t.Error("Expected Runtime to be created") + } + if proj.Composer == nil { + t.Error("Expected Composer to be created") + } + if proj.Provisioner == nil { + t.Error("Expected Provisioner to be created") + } + }) + + t.Run("CreatesRuntimeWhenOverridesHasNilRuntime", func(t *testing.T) { + proj, err := NewProject("test-context", &Project{Runtime: nil}) + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + if proj == nil { + t.Fatal("Expected Project to be created") + } + if proj.Runtime == nil { + t.Error("Expected Runtime to be created") + } + }) + + t.Run("ErrorOnRuntimeCreationFailure", func(t *testing.T) { + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("failed to get project root") + } + + proj, err := NewProject("test-context", &Project{ + Runtime: &runtime.Runtime{ + Shell: mockShell, + }, + }) + + if err == nil { + t.Fatal("Expected error for runtime creation failure") + } + if proj != nil { + t.Error("Expected Project to be nil on error") + } + if !strings.Contains(err.Error(), "failed to initialize context") && !strings.Contains(err.Error(), "failed to get project root") && !strings.Contains(err.Error(), "config handler not available") { + t.Errorf("Expected error about initializing context, getting project root, or config handler, got: %v", err) + } + }) + } // ============================================================================= @@ -273,7 +452,7 @@ func TestNewProject(t *testing.T) { func TestProject_Configure(t *testing.T) { t.Run("SuccessWithNilFlagOverrides", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) if err != nil { t.Fatalf("Failed to create project: %v", err) @@ -287,7 +466,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("SuccessWithEmptyFlagOverrides", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) if err != nil { t.Fatalf("Failed to create project: %v", err) @@ -301,7 +480,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("SuccessWithFlagOverrides", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) if err != nil { t.Fatalf("Failed to create project: %v", err) @@ -320,7 +499,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("SetsGenericProviderInDevModeWhenProviderNotSet", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.IsDevModeFunc = func(contextName string) bool { return true @@ -357,7 +536,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("SkipsGenericProviderWhenProviderAlreadySet", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.IsDevModeFunc = func(contextName string) bool { return true @@ -394,7 +573,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("ErrorOnApplyProviderDefaultsFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.SetFunc = func(key string, value any) error { if key == "cluster.driver" { @@ -417,7 +596,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("ErrorOnLoadConfigFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.LoadConfigFunc = func() error { return fmt.Errorf("load config failed") @@ -441,7 +620,7 @@ func TestProject_Configure(t *testing.T) { }) t.Run("ErrorOnSetFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.SetFunc = func(key string, value any) error { return fmt.Errorf("set failed") @@ -463,11 +642,12 @@ func TestProject_Configure(t *testing.T) { t.Errorf("Expected specific error message, got: %v", err) } }) + } func TestProject_Initialize(t *testing.T) { t.Run("SuccessWithoutWorkstation", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) if err != nil { t.Fatalf("Failed to create project: %v", err) @@ -481,7 +661,7 @@ func TestProject_Initialize(t *testing.T) { }) t.Run("SuccessWithWorkstation", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.IsDevModeFunc = func(contextName string) bool { return true @@ -506,7 +686,7 @@ func TestProject_Initialize(t *testing.T) { }) t.Run("SuccessWithOverwriteTrue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) if err != nil { t.Fatalf("Failed to create project: %v", err) @@ -520,7 +700,7 @@ func TestProject_Initialize(t *testing.T) { }) t.Run("ErrorOnGenerateContextIDFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.GenerateContextIDFunc = func() error { return fmt.Errorf("generate context ID failed") @@ -544,7 +724,7 @@ func TestProject_Initialize(t *testing.T) { }) t.Run("ErrorOnSaveConfigFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.SaveConfigFunc = func(hasSetFlags ...bool) error { return fmt.Errorf("save config failed") @@ -567,11 +747,120 @@ func TestProject_Initialize(t *testing.T) { } }) + t.Run("ErrorOnLoadBlueprintFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockBlueprintHandler := blueprint.NewMockBlueprintHandler() + mockBlueprintHandler.LoadBlueprintFunc = func() error { + return fmt.Errorf("load blueprint failed") + } + proj.Composer.BlueprintHandler = mockBlueprintHandler + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for LoadBlueprint failure") + return + } + + if !strings.Contains(err.Error(), "failed to load blueprint data") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("ErrorOnGenerateFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockBlueprintHandler := blueprint.NewMockBlueprintHandler() + mockBlueprintHandler.WriteFunc = func(overwrite ...bool) error { + return fmt.Errorf("generate failed") + } + proj.Composer.BlueprintHandler = mockBlueprintHandler + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for Generate failure") + return + } + + if !strings.Contains(err.Error(), "failed to generate infrastructure") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("ErrorOnPrepareToolsFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + mockToolsManager := tools.NewMockToolsManager() + mockToolsManager.CheckFunc = func() error { + return fmt.Errorf("prepare tools failed") + } + proj.Runtime.ToolsManager = mockToolsManager + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for PrepareTools failure") + return + } + + if !strings.Contains(err.Error(), "error checking tools") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + + t.Run("ErrorOnAssignIPsFailure", func(t *testing.T) { + mocks := setupProjectMocks(t) + mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfig.IsDevModeFunc = func(contextName string) bool { + return true + } + + proj, err := NewProject("test-context", &Project{Runtime: mocks.Runtime}) + if err != nil { + t.Fatalf("Failed to create project: %v", err) + } + + if proj.Workstation == nil { + t.Fatal("Expected workstation to be created") + } + + mockNetworkManager := network.NewMockNetworkManager() + mockNetworkManager.AssignIPsFunc = func(services []services.Service) error { + return fmt.Errorf("assign IPs failed") + } + proj.Workstation.NetworkManager = mockNetworkManager + + err = proj.Initialize(false) + + if err == nil { + t.Error("Expected error for AssignIPs failure") + return + } + + if !strings.Contains(err.Error(), "failed to assign IPs to network manager") { + t.Errorf("Expected specific error message, got: %v", err) + } + }) + } func TestProject_PerformCleanup(t *testing.T) { t.Run("Success", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) cleanCalled := false mockConfig.CleanFunc = func() error { @@ -596,7 +885,7 @@ func TestProject_PerformCleanup(t *testing.T) { }) t.Run("ErrorOnCleanFailure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupProjectMocks(t) mockConfig := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfig.CleanFunc = func() error { return fmt.Errorf("clean failed") @@ -618,4 +907,5 @@ func TestProject_PerformCleanup(t *testing.T) { t.Errorf("Expected specific error message, got: %v", err) } }) + }