From 1c2e049a7e91bec770fd4ae5e05f03b95d800c43 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 16 Nov 2025 17:06:03 -0500 Subject: [PATCH 01/14] Make runtime tests conformal Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime_test.go | 1862 ++++++++++++++++++++++++++--------- 1 file changed, 1388 insertions(+), 474 deletions(-) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 11262b2a0..3985bb1a6 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -10,6 +10,7 @@ import ( v1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/runtime/config" + "github.com/windsorcli/cli/pkg/runtime/env" "github.com/windsorcli/cli/pkg/runtime/secrets" "github.com/windsorcli/cli/pkg/runtime/shell" ) @@ -18,14 +19,20 @@ import ( // Test Setup // ============================================================================= -// setupEnvironmentMocks creates mock components for testing the Runtime -func setupEnvironmentMocks(t *testing.T) *Mocks { +// RuntimeTestMocks contains all the mock dependencies for testing the Runtime +type RuntimeTestMocks struct { + ConfigHandler config.ConfigHandler + Shell shell.Shell + Runtime *Runtime +} + +// setupRuntimeMocks creates mock components for testing the Runtime with optional overrides +func setupRuntimeMocks(t *testing.T, opts ...func(*RuntimeTestMocks)) *RuntimeTestMocks { t.Helper() configHandler := config.NewMockConfigHandler() mockShell := shell.NewMockShell() - // Set up basic configuration configHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { switch key { case "docker.enabled", "cluster.enabled", "terraform.enabled": @@ -46,7 +53,6 @@ func setupEnvironmentMocks(t *testing.T) *Mocks { } } - // Set up shell mock to return output mockShell.RenderEnvVarsFunc = func(envVars map[string]string, export bool) string { var result string for key, value := range envVars { @@ -67,22 +73,18 @@ func setupEnvironmentMocks(t *testing.T) *Mocks { return result } - // Set up session token mock mockShell.GetSessionTokenFunc = func() (string, error) { return "mock-session-token", nil } - // Set up GetProjectRoot mock mockShell.GetProjectRootFunc = func() (string, error) { return "/test/project", nil } - // Set up GetContext mock configHandler.GetContextFunc = func() string { return "test-context" } - // Create execution context - paths will be set automatically by NewRuntime rtOpts := []*Runtime{ { Shell: mockShell, @@ -90,91 +92,130 @@ func setupEnvironmentMocks(t *testing.T) *Mocks { }, } - ctx, err := NewRuntime(rtOpts...) + rt, err := NewRuntime(rtOpts...) if err != nil { t.Fatalf("Failed to create context: %v", err) } - return &Mocks{ + mocks := &RuntimeTestMocks{ ConfigHandler: configHandler, Shell: mockShell, - Runtime: ctx, + Runtime: rt, + } + + for _, opt := range opts { + opt(mocks) } + + return mocks } -// Mocks contains all the mock dependencies for testing -type Mocks struct { - ConfigHandler config.ConfigHandler - Shell shell.Shell - Runtime *Runtime +type MockToolsManager struct { + WriteManifestFunc func() error + InstallFunc func() error + CheckFunc func() error +} + +func (m *MockToolsManager) WriteManifest() error { + if m.WriteManifestFunc != nil { + return m.WriteManifestFunc() + } + return nil +} + +func (m *MockToolsManager) Install() error { + if m.InstallFunc != nil { + return m.InstallFunc() + } + return nil +} + +func (m *MockToolsManager) Check() error { + if m.CheckFunc != nil { + return m.CheckFunc() + } + return nil } // ============================================================================= // Test Constructor // ============================================================================= -func TestNewRuntime(t *testing.T) { +func TestRuntime_NewRuntime(t *testing.T) { t.Run("CreatesContextWithDependencies", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) + // Given a runtime with dependencies + mocks := setupRuntimeMocks(t) - ctx := mocks.Runtime + // When the runtime is created + rt := mocks.Runtime - if ctx == nil { + // Then all dependencies should be set correctly + + if rt == nil { t.Fatal("Expected context to be created") } - if ctx.Shell != mocks.Shell { + if rt.Shell != mocks.Shell { t.Error("Expected shell to be set") } - if ctx.ConfigHandler != mocks.ConfigHandler { + if rt.ConfigHandler != mocks.ConfigHandler { t.Error("Expected config handler to be set") } - if ctx.envVars == nil { + if rt.envVars == nil { t.Error("Expected envVars map to be initialized") } - if ctx.aliases == nil { + if rt.aliases == nil { t.Error("Expected aliases map to be initialized") } - if ctx.ContextName != "test-context" { - t.Errorf("Expected ContextName to be 'test-context', got: %s", ctx.ContextName) + if rt.ContextName != "test-context" { + t.Errorf("Expected ContextName to be 'test-context', got: %s", rt.ContextName) } - if ctx.ProjectRoot != "/test/project" { - t.Errorf("Expected ProjectRoot to be '/test/project', got: %s", ctx.ProjectRoot) + if rt.ProjectRoot != "/test/project" { + t.Errorf("Expected ProjectRoot to be '/test/project', got: %s", rt.ProjectRoot) } expectedConfigRoot := filepath.Join("/test/project", "contexts", "test-context") - if ctx.ConfigRoot != expectedConfigRoot { - t.Errorf("Expected ConfigRoot to be %q, got: %s", expectedConfigRoot, ctx.ConfigRoot) + if rt.ConfigRoot != expectedConfigRoot { + t.Errorf("Expected ConfigRoot to be %q, got: %s", expectedConfigRoot, rt.ConfigRoot) } expectedTemplateRoot := filepath.Join("/test/project", "contexts", "_template") - if ctx.TemplateRoot != expectedTemplateRoot { - t.Errorf("Expected TemplateRoot to be %q, got: %s", expectedTemplateRoot, ctx.TemplateRoot) + if rt.TemplateRoot != expectedTemplateRoot { + t.Errorf("Expected TemplateRoot to be %q, got: %s", expectedTemplateRoot, rt.TemplateRoot) } }) t.Run("ErrorWhenContextIsNil", func(t *testing.T) { + // Given nil options + // When NewRuntime is called _, err := NewRuntime(nil) + // Then no error should be returned + if err != nil { t.Errorf("Expected no error when opts is nil, got: %v", err) } }) t.Run("ErrorWhenRuntimeIsNil", func(t *testing.T) { + // Given nil options + // When NewRuntime is called _, err := NewRuntime(nil) + // Then no error should be returned + if err != nil { t.Errorf("Expected no error when opts is nil, got: %v", err) } }) t.Run("CreatesShellWhenNotProvided", func(t *testing.T) { + // Given a runtime with config handler but no shell mockConfigHandler := config.NewMockConfigHandler() mockConfigHandler.GetContextFunc = func() string { return "test" @@ -186,8 +227,11 @@ func TestNewRuntime(t *testing.T) { }, } + // When NewRuntime is called result, err := NewRuntime(rtOpts...) + // Then shell should be created + if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -198,6 +242,7 @@ func TestNewRuntime(t *testing.T) { }) t.Run("CreatesConfigHandlerWhenNotProvided", func(t *testing.T) { + // Given a runtime with shell but no config handler mockShell := shell.NewMockShell() mockShell.GetProjectRootFunc = func() (string, error) { return "/test", nil @@ -209,8 +254,11 @@ func TestNewRuntime(t *testing.T) { }, } + // When NewRuntime is called result, err := NewRuntime(rtOpts...) + // Then config handler should be created + if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -219,320 +267,399 @@ func TestNewRuntime(t *testing.T) { t.Error("Expected config handler to be created") } }) -} -// ============================================================================= -// Test LoadEnvironment -// ============================================================================= + t.Run("ErrorWhenGetProjectRootFails", func(t *testing.T) { + // Given a shell that fails to get project root + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("failed to get project root") + } -func TestRuntime_LoadEnvironment(t *testing.T) { - t.Run("LoadsEnvironmentSuccessfully", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + rtOpts := []*Runtime{ + { + Shell: mockShell, + }, + } - err := ctx.LoadEnvironment(false) + // When NewRuntime is called + _, err := NewRuntime(rtOpts...) - // The context should load successfully with the default WindsorEnv printer - if err != nil { - t.Fatalf("Expected no error, got: %v", err) - } + // Then an error should be returned - // Check that the WindsorEnv printer was initialized - if ctx.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv printer to be initialized") + if err == nil { + t.Error("Expected error when GetProjectRoot fails") } - // Check that environment variables were loaded - if len(ctx.envVars) == 0 { - t.Error("Expected environment variables to be loaded") + if !strings.Contains(err.Error(), "failed to get project root") { + t.Errorf("Expected error about getting project root, got: %v", err) } }) - t.Run("HandlesConfigHandlerNotLoaded", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + t.Run("ErrorWhenGetProjectRootFailsOnSecondCall", func(t *testing.T) { + // Given a shell that fails to get project root on second call + mockShell := shell.NewMockShell() + callCount := 0 + mockShell.GetProjectRootFunc = func() (string, error) { + callCount++ + if callCount == 1 { + return "", nil + } + return "", fmt.Errorf("failed to get project root") + } + + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "" + } + + rtOpts := []*Runtime{ + { + Shell: mockShell, + ConfigHandler: mockConfigHandler, + ProjectRoot: "", + }, + } - // Set config handler to nil to test error handling - ctx.ConfigHandler = nil + // When NewRuntime is called + _, err := NewRuntime(rtOpts...) - err := ctx.LoadEnvironment(false) + // Then an error should be returned if err == nil { - t.Error("Expected error when config handler is not loaded") + t.Error("Expected error when GetProjectRoot fails on second call") + } + + if !strings.Contains(err.Error(), "failed to get project root") { + t.Errorf("Expected error about getting project root, got: %v", err) } }) - t.Run("HandlesEnvPrinterInitializationError", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + t.Run("DefaultsContextNameToLocalWhenEmpty", func(t *testing.T) { + // Given a config handler with empty context name + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } + + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "" + } - err := ctx.LoadEnvironment(false) + rtOpts := []*Runtime{ + { + Shell: mockShell, + ConfigHandler: mockConfigHandler, + }, + } + + // When NewRuntime is called + rt, err := NewRuntime(rtOpts...) + + // Then context name should default to "local" - // This should not error since the default WindsorEnv printer has working initialization if err != nil { t.Fatalf("Expected no error, got: %v", err) } - // The WindsorEnv printer should be initialized after LoadEnvironment - if ctx.EnvPrinters.WindsorEnv == nil { - t.Error("Expected WindsorEnv printer to be initialized") + if rt.ContextName != "local" { + t.Errorf("Expected ContextName to be 'local', got: %s", rt.ContextName) } }) -} -// ============================================================================= + t.Run("HandlesAllOverridePaths", func(t *testing.T) { + // Given runtime options with all paths and dependencies overridden + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "/test/project", nil + } -// ============================================================================= -// Test Getter Methods -// ============================================================================= + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetContextFunc = func() string { + return "test-context" + } -func TestRuntime_GetEnvVars(t *testing.T) { - t.Run("ReturnsCopyOfEnvVars", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + mockToolsManager := &MockToolsManager{} + mockSopsProvider := secrets.NewMockSecretsProvider(mockShell) + mockOnepasswordProvider := secrets.NewMockSecretsProvider(mockShell) + mockAwsEnv := env.NewMockEnvPrinter() + mockAzureEnv := env.NewMockEnvPrinter() + mockDockerEnv := env.NewMockEnvPrinter() + mockKubeEnv := env.NewMockEnvPrinter() + mockTalosEnv := env.NewMockEnvPrinter() + mockTerraformEnv := env.NewMockEnvPrinter() + mockWindsorEnv := env.NewMockEnvPrinter() - original := map[string]string{ - "TEST_VAR1": "value1", - "TEST_VAR2": "value2", + rtOpts := []*Runtime{ + { + Shell: mockShell, + ConfigHandler: mockConfigHandler, + ContextName: "custom-context", + ProjectRoot: "/custom/project", + ConfigRoot: "/custom/config", + TemplateRoot: "/custom/template", + ToolsManager: mockToolsManager, + }, } - ctx.envVars = original + rtOpts[0].SecretsProviders.Sops = mockSopsProvider + rtOpts[0].SecretsProviders.Onepassword = mockOnepasswordProvider + rtOpts[0].EnvPrinters.AwsEnv = mockAwsEnv + rtOpts[0].EnvPrinters.AzureEnv = mockAzureEnv + rtOpts[0].EnvPrinters.DockerEnv = mockDockerEnv + rtOpts[0].EnvPrinters.KubeEnv = mockKubeEnv + rtOpts[0].EnvPrinters.TalosEnv = mockTalosEnv + rtOpts[0].EnvPrinters.TerraformEnv = mockTerraformEnv + rtOpts[0].EnvPrinters.WindsorEnv = mockWindsorEnv - copy := ctx.GetEnvVars() + // When NewRuntime is called + rt, err := NewRuntime(rtOpts...) - if len(copy) != len(original) { - t.Error("Expected copy to have same length as original") + // Then all overrides should be applied correctly + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) } - // Modify the copy - copy["NEW_VAR"] = "new_value" + if rt.ContextName != "custom-context" { + t.Errorf("Expected ContextName to be 'custom-context', got: %s", rt.ContextName) + } - // Original should be unchanged - if len(ctx.envVars) != len(original) { - t.Error("Expected original to be unchanged") + if rt.ProjectRoot != "/test/project" { + t.Errorf("Expected ProjectRoot to be '/test/project' (from GetProjectRoot), got: %s", rt.ProjectRoot) } - }) -} -func TestRuntime_GetAliases(t *testing.T) { - t.Run("ReturnsCopyOfAliases", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + if rt.ConfigRoot != "/custom/config" { + t.Errorf("Expected ConfigRoot to be '/custom/config', got: %s", rt.ConfigRoot) + } - original := map[string]string{ - "test1": "echo test1", - "test2": "echo test2", + if rt.TemplateRoot != "/custom/template" { + t.Errorf("Expected TemplateRoot to be '/custom/template', got: %s", rt.TemplateRoot) } - ctx.aliases = original - copy := ctx.GetAliases() + if rt.ToolsManager != mockToolsManager { + t.Error("Expected ToolsManager to be set") + } - if len(copy) != len(original) { - t.Error("Expected copy to have same length as original") + if rt.SecretsProviders.Sops != mockSopsProvider { + t.Error("Expected Sops provider to be set") } - // Modify the copy - copy["new"] = "echo new" + if rt.SecretsProviders.Onepassword != mockOnepasswordProvider { + t.Error("Expected Onepassword provider to be set") + } - // Original should be unchanged - if len(ctx.aliases) != len(original) { - t.Error("Expected original to be unchanged") + if rt.EnvPrinters.AwsEnv != mockAwsEnv { + t.Error("Expected AwsEnv to be set") + } + + if rt.EnvPrinters.AzureEnv != mockAzureEnv { + t.Error("Expected AzureEnv to be set") + } + + if rt.EnvPrinters.DockerEnv != mockDockerEnv { + t.Error("Expected DockerEnv to be set") + } + + if rt.EnvPrinters.KubeEnv != mockKubeEnv { + t.Error("Expected KubeEnv to be set") + } + + if rt.EnvPrinters.TalosEnv != mockTalosEnv { + t.Error("Expected TalosEnv to be set") + } + + if rt.EnvPrinters.TerraformEnv != mockTerraformEnv { + t.Error("Expected TerraformEnv to be set") + } + + if rt.EnvPrinters.WindsorEnv != mockWindsorEnv { + t.Error("Expected WindsorEnv to be set") } }) } // ============================================================================= -// Test Private Methods +// Test Public Methods // ============================================================================= -func TestRuntime_loadSecrets(t *testing.T) { - t.Run("LoadsSecretsSuccessfully", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - - // Set up mock secrets providers - mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell) - mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) +func TestRuntime_LoadEnvironment(t *testing.T) { + t.Run("LoadsEnvironmentSuccessfully", func(t *testing.T) { + // Given a runtime with mocks + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - ctx.SecretsProviders.Sops = mockSopsProvider - ctx.SecretsProviders.Onepassword = mockOnepasswordProvider + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) - err := ctx.loadSecrets() + // Then environment should load successfully if err != nil { t.Fatalf("Expected no error, got: %v", err) } - }) - - t.Run("HandlesSecretsProviderError", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - - // Set up mock secrets provider that returns an error - mockProvider := secrets.NewMockSecretsProvider(mocks.Shell) - mockProvider.LoadSecretsFunc = func() error { - return errors.New("secrets load failed") - } - ctx.SecretsProviders.Sops = mockProvider - - err := ctx.loadSecrets() - if err == nil { - t.Fatal("Expected error, got nil") + if rt.EnvPrinters.WindsorEnv == nil { + t.Error("Expected WindsorEnv printer to be initialized") } - if !strings.Contains(err.Error(), "secrets load failed") { - t.Errorf("Expected secrets load error, got: %v", err) + if len(rt.envVars) == 0 { + t.Error("Expected environment variables to be loaded") } }) - t.Run("HandlesNilProviders", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + t.Run("HandlesConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime with nil config handler + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Leave providers as nil - ctx.SecretsProviders.Sops = nil - ctx.SecretsProviders.Onepassword = nil + rt.ConfigHandler = nil - err := ctx.loadSecrets() - if err != nil { - t.Fatalf("Expected no error with nil providers, got: %v", err) + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) + + // Then an error should be returned + + if err == nil { + t.Error("Expected error when config handler is not loaded") } }) - t.Run("HandlesMixedProviders", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + t.Run("HandlesEnvPrinterInitializationError", func(t *testing.T) { + // Given a runtime with mocks + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Set up one provider that works and one that's nil - mockProvider := secrets.NewMockSecretsProvider(mocks.Shell) - ctx.SecretsProviders.Sops = mockProvider - ctx.SecretsProviders.Onepassword = nil + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) - err := ctx.loadSecrets() + // Then environment should load successfully if err != nil { - t.Fatalf("Expected no error with mixed providers, got: %v", err) + t.Fatalf("Expected no error, got: %v", err) } - }) -} -func TestRuntime_initializeSecretsProviders(t *testing.T) { - t.Run("InitializesSopsProviderWhenEnabled", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - - // Enable SOPS in config - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "secrets.sops.enabled" { - return true - } - return false + if rt.EnvPrinters.WindsorEnv == nil { + t.Error("Expected WindsorEnv printer to be initialized") } + }) - ctx.initializeSecretsProviders() + t.Run("ErrorWhenGetEnvVarsFails", func(t *testing.T) { + // Given a runtime with an env printer that fails to get env vars + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - if ctx.SecretsProviders.Sops == nil { - t.Error("Expected SOPS provider to be initialized") + mockEnvPrinter := env.NewMockEnvPrinter() + mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { + return nil, fmt.Errorf("failed to get env vars") } - }) + rt.EnvPrinters.WindsorEnv = mockEnvPrinter - t.Run("InitializesOnepasswordProviderWhenEnabled", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) - // Enable 1Password in config - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "secrets.onepassword.enabled" { - return true - } - return false - } + // Then an error should be returned - ctx.initializeSecretsProviders() + if err == nil { + t.Error("Expected error when GetEnvVars fails") + } - if ctx.SecretsProviders.Onepassword == nil { - t.Error("Expected 1Password provider to be initialized") + if !strings.Contains(err.Error(), "error getting environment variables") { + t.Errorf("Expected error about getting environment variables, got: %v", err) } }) - t.Run("SkipsProvidersWhenDisabled", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + t.Run("ErrorWhenGetAliasFails", func(t *testing.T) { + // Given a runtime with an env printer that fails to get aliases + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Disable both providers - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - return false + mockEnvPrinter := env.NewMockEnvPrinter() + mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { + return map[string]string{}, nil } + mockEnvPrinter.GetAliasFunc = func() (map[string]string, error) { + return nil, fmt.Errorf("failed to get aliases") + } + rt.EnvPrinters.WindsorEnv = mockEnvPrinter - ctx.initializeSecretsProviders() + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) - if ctx.SecretsProviders.Sops != nil { - t.Error("Expected SOPS provider to be nil when disabled") + // Then an error should be returned + + if err == nil { + t.Error("Expected error when GetAlias fails") } - if ctx.SecretsProviders.Onepassword != nil { - t.Error("Expected 1Password provider to be nil when disabled") + if !strings.Contains(err.Error(), "error getting aliases") { + t.Errorf("Expected error about getting aliases, got: %v", err) } }) - t.Run("DoesNotOverrideExistingProviders", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - - // Pre-set a provider - existingProvider := secrets.NewMockSecretsProvider(mocks.Shell) - ctx.SecretsProviders.Sops = existingProvider + t.Run("ErrorWhenPostEnvHookFails", func(t *testing.T) { + // Given a runtime with an env printer that fails to execute post env hook + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Enable SOPS in config - mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { - if key == "secrets.sops.enabled" { - return true - } - return false + mockEnvPrinter := env.NewMockEnvPrinter() + mockEnvPrinter.GetEnvVarsFunc = func() (map[string]string, error) { + return map[string]string{}, nil + } + mockEnvPrinter.GetAliasFunc = func() (map[string]string, error) { + return map[string]string{}, nil + } + mockEnvPrinter.PostEnvHookFunc = func(directory ...string) error { + return fmt.Errorf("failed to execute post env hook") } + rt.EnvPrinters.WindsorEnv = mockEnvPrinter - ctx.initializeSecretsProviders() + // When LoadEnvironment is called + err := rt.LoadEnvironment(false) - // Should still be the same provider - if ctx.SecretsProviders.Sops != existingProvider { - t.Error("Expected existing provider to be preserved") + // Then an error should be returned + + if err == nil { + t.Error("Expected error when PostEnvHook fails") + } + + if !strings.Contains(err.Error(), "failed to execute post env hooks") { + t.Errorf("Expected error about executing post env hooks, got: %v", err) } }) -} -func TestRuntime_LoadEnvironment_WithSecrets(t *testing.T) { t.Run("LoadsEnvironmentWithSecretsSuccessfully", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with secrets providers + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Set up mock secrets providers mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) - ctx.SecretsProviders.Sops = mockSopsProvider - ctx.SecretsProviders.Onepassword = mockOnepasswordProvider + rt.SecretsProviders.Sops = mockSopsProvider + rt.SecretsProviders.Onepassword = mockOnepasswordProvider - err := ctx.LoadEnvironment(true) // Enable secrets loading + // When LoadEnvironment is called with secrets enabled + err := rt.LoadEnvironment(true) + + // Then environment should load successfully if err != nil { t.Fatalf("Expected no error, got: %v", err) } }) t.Run("HandlesSecretsLoadError", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a secrets provider that fails to load + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - // Set up mock secrets provider that returns an error mockProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockProvider.LoadSecretsFunc = func() error { return errors.New("secrets load failed") } - ctx.SecretsProviders.Sops = mockProvider + rt.SecretsProviders.Sops = mockProvider + + // When LoadEnvironment is called with secrets enabled + err := rt.LoadEnvironment(true) - err := ctx.LoadEnvironment(true) // Enable secrets loading + // Then an error should be returned if err == nil { t.Fatal("Expected error, got nil") } @@ -543,129 +670,78 @@ func TestRuntime_LoadEnvironment_WithSecrets(t *testing.T) { }) } -func TestRuntime_initializeComponents_EdgeCases(t *testing.T) { - t.Run("ReturnsNil", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime +func TestRuntime_GetEnvVars(t *testing.T) { + t.Run("ReturnsCopyOfEnvVars", func(t *testing.T) { + // Given a runtime with environment variables + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime - err := ctx.initializeComponents() - if err != nil { - t.Errorf("Expected nil, got: %v", err) + original := map[string]string{ + "TEST_VAR1": "value1", + "TEST_VAR2": "value2", } - }) -} + rt.envVars = original -// ============================================================================= -// Test Helpers -// ============================================================================= + // When GetEnvVars is called + copy := rt.GetEnvVars() -type MockToolsManager struct { - WriteManifestFunc func() error - InstallFunc func() error - CheckFunc func() error -} - -func (m *MockToolsManager) WriteManifest() error { - if m.WriteManifestFunc != nil { - return m.WriteManifestFunc() - } - return nil -} - -func (m *MockToolsManager) Install() error { - if m.InstallFunc != nil { - return m.InstallFunc() - } - return nil -} - -func (m *MockToolsManager) Check() error { - if m.CheckFunc != nil { - return m.CheckFunc() - } - return nil -} - -type MockEnvPrinter struct { - GetEnvVarsFunc func() (map[string]string, error) - GetAliasFunc func() (map[string]string, error) - PostEnvHookFunc func(directory ...string) error - GetManagedEnvFunc func() []string - GetManagedAliasFunc func() []string - SetManagedEnvFunc func(env string) - SetManagedAliasFunc func(alias string) - ResetFunc func() -} + // Then a copy should be returned that doesn't affect the original + if len(copy) != len(original) { + t.Error("Expected copy to have same length as original") + } -func (m *MockEnvPrinter) GetEnvVars() (map[string]string, error) { - if m.GetEnvVarsFunc != nil { - return m.GetEnvVarsFunc() - } - return make(map[string]string), nil -} + copy["NEW_VAR"] = "new_value" -func (m *MockEnvPrinter) GetAlias() (map[string]string, error) { - if m.GetAliasFunc != nil { - return m.GetAliasFunc() - } - return make(map[string]string), nil + if len(rt.envVars) != len(original) { + t.Error("Expected original to be unchanged") + } + }) } -func (m *MockEnvPrinter) PostEnvHook(directory ...string) error { - if m.PostEnvHookFunc != nil { - return m.PostEnvHookFunc(directory...) - } - return nil -} +func TestRuntime_GetAliases(t *testing.T) { + t.Run("ReturnsCopyOfAliases", func(t *testing.T) { + // Given a runtime with aliases + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime -func (m *MockEnvPrinter) GetManagedEnv() []string { - if m.GetManagedEnvFunc != nil { - return m.GetManagedEnvFunc() - } - return []string{} -} + original := map[string]string{ + "test1": "echo test1", + "test2": "echo test2", + } + rt.aliases = original -func (m *MockEnvPrinter) GetManagedAlias() []string { - if m.GetManagedAliasFunc != nil { - return m.GetManagedAliasFunc() - } - return []string{} -} + // When GetAliases is called + copy := rt.GetAliases() -func (m *MockEnvPrinter) SetManagedEnv(env string) { - if m.SetManagedEnvFunc != nil { - m.SetManagedEnvFunc(env) - } -} + // Then a copy should be returned that doesn't affect the original + if len(copy) != len(original) { + t.Error("Expected copy to have same length as original") + } -func (m *MockEnvPrinter) SetManagedAlias(alias string) { - if m.SetManagedAliasFunc != nil { - m.SetManagedAliasFunc(alias) - } -} + copy["new"] = "echo new" -func (m *MockEnvPrinter) Reset() { - if m.ResetFunc != nil { - m.ResetFunc() - } + if len(rt.aliases) != len(original) { + t.Error("Expected original to be unchanged") + } + }) } -// ============================================================================= -// Test CheckTools -// ============================================================================= - func TestRuntime_CheckTools(t *testing.T) { t.Run("Success", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a tools manager + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockToolsManager := &MockToolsManager{} mockToolsManager.CheckFunc = func() error { return nil } - ctx.ToolsManager = mockToolsManager + rt.ToolsManager = mockToolsManager + + // When CheckTools is called + err := rt.CheckTools() - err := ctx.CheckTools() + // Then no error should be returned if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -673,8 +749,9 @@ func TestRuntime_CheckTools(t *testing.T) { }) t.Run("InitializesToolsManagerWhenNil", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with nil tools manager + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { @@ -684,27 +761,34 @@ func TestRuntime_CheckTools(t *testing.T) { return nil } - ctx.ToolsManager = nil + rt.ToolsManager = nil - err := ctx.CheckTools() + // When CheckTools is called + err := rt.CheckTools() + + // Then tools manager should be initialized if err != nil { t.Errorf("Expected no error, got: %v", err) } - if ctx.ToolsManager == nil { + if rt.ToolsManager == nil { t.Error("Expected ToolsManager to be initialized") } }) t.Run("HandlesToolsManagerUnavailable", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with nil tools manager and config handler + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + rt.ToolsManager = nil + rt.ConfigHandler = nil - ctx.ToolsManager = nil - ctx.ConfigHandler = nil + // When CheckTools is called + err := rt.CheckTools() - err := ctx.CheckTools() + // Then an error should be returned if err == nil { t.Error("Expected error when ToolsManager cannot be initialized") @@ -716,16 +800,20 @@ func TestRuntime_CheckTools(t *testing.T) { }) t.Run("HandlesToolsManagerCheckError", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a tools manager that fails to check + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockToolsManager := &MockToolsManager{} mockToolsManager.CheckFunc = func() error { return errors.New("tools check failed") } - ctx.ToolsManager = mockToolsManager + rt.ToolsManager = mockToolsManager - err := ctx.CheckTools() + // When CheckTools is called + err := rt.CheckTools() + + // Then an error should be returned if err == nil { t.Error("Expected error when ToolsManager.Check fails") @@ -739,19 +827,19 @@ func TestRuntime_CheckTools(t *testing.T) { t.Errorf("Expected error to contain original error, got: %v", err) } }) - } func TestRuntime_HandleSessionReset(t *testing.T) { t.Run("ResetsWhenNoSessionToken", func(t *testing.T) { + // Given a runtime with no session token t.Cleanup(func() { os.Unsetenv("NO_CACHE") os.Unsetenv("WINDSOR_SESSION_TOKEN") }) os.Unsetenv("WINDSOR_SESSION_TOKEN") - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockShell := mocks.Shell.(*shell.MockShell) resetCalled := false @@ -762,7 +850,10 @@ func TestRuntime_HandleSessionReset(t *testing.T) { return false, nil } - err := ctx.HandleSessionReset() + // When HandleSessionReset is called + err := rt.HandleSessionReset() + + // Then reset should be called if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -774,12 +865,13 @@ func TestRuntime_HandleSessionReset(t *testing.T) { }) t.Run("ResetsWhenResetFlagSet", func(t *testing.T) { + // Given a runtime with reset flag set t.Cleanup(func() { os.Unsetenv("NO_CACHE") }) - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockShell := mocks.Shell.(*shell.MockShell) resetCalled := false @@ -790,7 +882,10 @@ func TestRuntime_HandleSessionReset(t *testing.T) { return true, nil } - err := ctx.HandleSessionReset() + // When HandleSessionReset is called + err := rt.HandleSessionReset() + + // Then reset should be called if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -802,8 +897,9 @@ func TestRuntime_HandleSessionReset(t *testing.T) { }) t.Run("SkipsResetWhenSessionTokenAndNoResetFlag", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with session token and no reset flag + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime t.Setenv("WINDSOR_SESSION_TOKEN", "test-token") @@ -816,7 +912,10 @@ func TestRuntime_HandleSessionReset(t *testing.T) { return false, nil } - err := ctx.HandleSessionReset() + // When HandleSessionReset is called + err := rt.HandleSessionReset() + + // Then reset should not be called if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -828,9 +927,13 @@ func TestRuntime_HandleSessionReset(t *testing.T) { }) t.Run("ErrorWhenShellNotInitialized", func(t *testing.T) { - ctx := &Runtime{} + // Given a runtime with nil shell + rt := &Runtime{} + + // When HandleSessionReset is called + err := rt.HandleSessionReset() - err := ctx.HandleSessionReset() + // Then an error should be returned if err == nil { t.Error("Expected error when Shell is nil") @@ -842,15 +945,19 @@ func TestRuntime_HandleSessionReset(t *testing.T) { }) t.Run("ErrorWhenCheckResetFlagsFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a shell that fails to check reset flags + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockShell := mocks.Shell.(*shell.MockShell) mockShell.CheckResetFlagsFunc = func() (bool, error) { return false, fmt.Errorf("check reset flags failed") } - err := ctx.HandleSessionReset() + // When HandleSessionReset is called + err := rt.HandleSessionReset() + + // Then an error should be returned if err == nil { t.Error("Expected error when CheckResetFlags fails") @@ -860,19 +967,24 @@ func TestRuntime_HandleSessionReset(t *testing.T) { t.Errorf("Expected error about check reset flags, got: %v", err) } }) + } func TestRuntime_ApplyConfigDefaults(t *testing.T) { t.Run("SkipsWhenConfigAlreadyLoaded", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with config already loaded + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { return true } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then no error should be returned if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -880,9 +992,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("SetsDefaultsForDevMode", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime in dev mode + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -907,7 +1020,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then dev mode defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -927,9 +1043,13 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("ErrorWhenConfigHandlerNotAvailable", func(t *testing.T) { - ctx := &Runtime{} + // Given a runtime with nil config handler + rt := &Runtime{} - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then an error should be returned if err == nil { t.Error("Expected error when ConfigHandler is nil") @@ -941,9 +1061,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("ErrorWhenSetDevFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime with a config handler that fails to set dev + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -962,7 +1083,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then an error should be returned if err == nil { t.Error("Expected error when Set dev fails") @@ -974,9 +1098,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("SetsDefaultsForNonDevMode", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime not in dev mode + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -999,8 +1124,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + // Then defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -1011,9 +1138,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("SetsVMDriverForDockerDesktop", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime in dev mode with Docker Desktop + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -1037,8 +1165,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + // Then VM driver should be set if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -1056,10 +1186,96 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { } }) + t.Run("UsesFullConfigForDevModeWithNonDockerDesktop", func(t *testing.T) { + // Given a runtime in dev mode with non-Docker Desktop VM driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return true + } + + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "vm.driver" { + return "colima" + } + return "" + } + + setDefaultCalled := false + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + setDefaultCalled = true + return nil + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + return nil + } + + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then full config should be set + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !setDefaultCalled { + t.Error("Expected SetDefault to be called") + } + }) + + t.Run("UsesStandardConfigForNonDevMode", func(t *testing.T) { + // Given a runtime not in dev mode + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.IsLoadedFunc = func() bool { + return false + } + mockConfigHandler.IsDevModeFunc = func(contextName string) bool { + return false + } + + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + return "" + } + + setDefaultCalled := false + mockConfigHandler.SetDefaultFunc = func(cfg v1alpha1.Context) error { + setDefaultCalled = true + return nil + } + + mockConfigHandler.SetFunc = func(key string, value interface{}) error { + return nil + } + + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + + // Then standard config should be set + if err != nil { + t.Errorf("Expected no error, got: %v", err) + } + + if !setDefaultCalled { + t.Error("Expected SetDefault to be called") + } + }) + t.Run("ErrorWhenSetDefaultFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime with a config handler that fails to set default + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -1080,8 +1296,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + // Then an error should be returned if err == nil { t.Error("Expected error when SetDefault fails") } @@ -1092,9 +1310,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("ErrorWhenSetVMDriverFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime with a config handler that fails to set VM driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -1118,8 +1337,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + // Then an error should be returned if err == nil { t.Error("Expected error when Set vm.driver fails") } @@ -1130,9 +1351,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { }) t.Run("ErrorWhenSetProviderFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime with a config handler that fails to set provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.IsLoadedFunc = func() bool { @@ -1156,8 +1378,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { return nil } - err := ctx.ApplyConfigDefaults() + // When ApplyConfigDefaults is called + err := rt.ApplyConfigDefaults() + // Then an error should be returned if err == nil { t.Error("Expected error when Set provider fails") } @@ -1170,9 +1394,10 @@ func TestRuntime_ApplyConfigDefaults(t *testing.T) { func TestRuntime_ApplyProviderDefaults(t *testing.T) { t.Run("SetsAWSDefaults", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with AWS provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) setCalls := make(map[string]interface{}) @@ -1181,8 +1406,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("aws") + // When ApplyProviderDefaults is called with "aws" + err := rt.ApplyProviderDefaults("aws") + // Then AWS defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -1197,9 +1424,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("SetsAzureDefaults", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with Azure provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) setCalls := make(map[string]interface{}) @@ -1208,8 +1436,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("azure") + // When ApplyProviderDefaults is called with "azure" + err := rt.ApplyProviderDefaults("azure") + // Then Azure defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -1224,9 +1454,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("SetsGenericDefaults", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime with generic provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) setCalls := make(map[string]interface{}) @@ -1235,8 +1466,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("generic") + // When ApplyProviderDefaults is called with "generic" + err := rt.ApplyProviderDefaults("generic") + // Then generic defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) } @@ -1247,9 +1480,13 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("ErrorWhenConfigHandlerNotAvailable", func(t *testing.T) { - ctx := &Runtime{} + // Given a runtime with nil config handler + rt := &Runtime{} - err := ctx.ApplyProviderDefaults("aws") + // When ApplyProviderDefaults is called + err := rt.ApplyProviderDefaults("aws") + + // Then an error should be returned if err == nil { t.Error("Expected error when ConfigHandler is nil") @@ -1261,9 +1498,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("ErrorWhenSetFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with a config handler that fails to set + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.SetFunc = func(key string, value interface{}) error { @@ -1273,7 +1511,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("aws") + // When ApplyProviderDefaults is called + err := rt.ApplyProviderDefaults("aws") + + // Then an error should be returned if err == nil { t.Error("Expected error when Set fails") @@ -1285,9 +1526,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("SetsDefaultsForDevModeWithNoProvider", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime in dev mode with no provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { @@ -1309,7 +1551,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("") + // When ApplyProviderDefaults is called with empty provider + err := rt.ApplyProviderDefaults("") + + // Then dev mode defaults should be set if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -1321,9 +1566,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("GetsProviderFromConfig", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with provider in config + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { @@ -1339,7 +1585,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("") + // When ApplyProviderDefaults is called with empty provider + err := rt.ApplyProviderDefaults("") + + // Then provider defaults should be set from config if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -1351,9 +1600,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("ErrorWhenSetClusterDriverFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with a config handler that fails to set cluster driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { @@ -1369,7 +1619,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("generic") + // When ApplyProviderDefaults is called + err := rt.ApplyProviderDefaults("generic") + + // Then an error should be returned if err == nil { t.Error("Expected error when Set cluster.driver fails") @@ -1381,9 +1634,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("ErrorWhenSetAzureDriverFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "prod" + // Given a runtime with a config handler that fails to set Azure driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "prod" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.SetFunc = func(key string, value interface{}) error { @@ -1393,7 +1647,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("azure") + // When ApplyProviderDefaults is called with "azure" + err := rt.ApplyProviderDefaults("azure") + + // Then an error should be returned if err == nil { t.Error("Expected error when Set cluster.driver fails for azure") @@ -1405,9 +1662,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { }) t.Run("ErrorWhenSetDevModeClusterDriverFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime - ctx.ContextName = "local" + // Given a runtime in dev mode with a config handler that fails to set cluster driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + rt.ContextName = "local" mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { @@ -1429,7 +1687,10 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { return nil } - err := ctx.ApplyProviderDefaults("") + // When ApplyProviderDefaults is called + err := rt.ApplyProviderDefaults("") + + // Then an error should be returned if err == nil { t.Error("Expected error when Set cluster.driver fails for dev mode") @@ -1443,8 +1704,9 @@ func TestRuntime_ApplyProviderDefaults(t *testing.T) { func TestRuntime_PrepareTools(t *testing.T) { t.Run("Success", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a tools manager + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockToolsManager := &MockToolsManager{} mockToolsManager.CheckFunc = func() error { @@ -1453,9 +1715,12 @@ func TestRuntime_PrepareTools(t *testing.T) { mockToolsManager.InstallFunc = func() error { return nil } - ctx.ToolsManager = mockToolsManager + rt.ToolsManager = mockToolsManager + + // When PrepareTools is called + err := rt.PrepareTools() - err := ctx.PrepareTools() + // Then no error should be returned if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -1463,16 +1728,20 @@ func TestRuntime_PrepareTools(t *testing.T) { }) t.Run("ErrorWhenCheckFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a tools manager that fails to check + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockToolsManager := &MockToolsManager{} mockToolsManager.CheckFunc = func() error { return fmt.Errorf("tools check failed") } - ctx.ToolsManager = mockToolsManager + rt.ToolsManager = mockToolsManager - err := ctx.PrepareTools() + // When PrepareTools is called + err := rt.PrepareTools() + + // Then an error should be returned if err == nil { t.Error("Expected error when Check fails") @@ -1484,8 +1753,9 @@ func TestRuntime_PrepareTools(t *testing.T) { }) t.Run("ErrorWhenInstallFails", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a tools manager that fails to install + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime mockToolsManager := &MockToolsManager{} mockToolsManager.CheckFunc = func() error { @@ -1494,9 +1764,12 @@ func TestRuntime_PrepareTools(t *testing.T) { mockToolsManager.InstallFunc = func() error { return fmt.Errorf("tools install failed") } - ctx.ToolsManager = mockToolsManager + rt.ToolsManager = mockToolsManager + + // When PrepareTools is called + err := rt.PrepareTools() - err := ctx.PrepareTools() + // Then an error should be returned if err == nil { t.Error("Expected error when Install fails") @@ -1506,17 +1779,43 @@ func TestRuntime_PrepareTools(t *testing.T) { t.Errorf("Expected error about installing tools, got: %v", err) } }) + + t.Run("ErrorWhenToolsManagerCannotBeInitialized", func(t *testing.T) { + // Given a runtime with nil tools manager and config handler + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + rt.ToolsManager = nil + rt.ConfigHandler = nil + + // When PrepareTools is called + err := rt.PrepareTools() + + // Then an error should be returned + + if err == nil { + t.Error("Expected error when ToolsManager cannot be initialized") + } + + if !strings.Contains(err.Error(), "tools manager not available") { + t.Errorf("Expected error about tools manager not available, got: %v", err) + } + }) } func TestRuntime_GetBuildID(t *testing.T) { t.Run("CreatesNewBuildIDWhenNoneExists", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with no existing build ID + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir + + // When GetBuildID is called + buildID, err := rt.GetBuildID() - buildID, err := ctx.GetBuildID() + // Then a new build ID should be created if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -1528,18 +1827,22 @@ func TestRuntime_GetBuildID(t *testing.T) { }) t.Run("ReturnsExistingBuildID", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with an existing build ID + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir - buildID1, err := ctx.GetBuildID() + buildID1, err := rt.GetBuildID() if err != nil { t.Fatalf("Failed to get initial build ID: %v", err) } - buildID2, err := ctx.GetBuildID() + // When GetBuildID is called again + buildID2, err := rt.GetBuildID() + + // Then the same build ID should be returned if err != nil { t.Fatalf("Failed to get second build ID: %v", err) } @@ -1548,17 +1851,90 @@ func TestRuntime_GetBuildID(t *testing.T) { t.Errorf("Expected build IDs to match, got %s and %s", buildID1, buildID2) } }) + + t.Run("ErrorWhenReadFileFails", func(t *testing.T) { + // Given a runtime with a build ID file that cannot be read + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + tmpDir := t.TempDir() + rt.ProjectRoot = tmpDir + + buildIDDir := filepath.Join(tmpDir, ".windsor") + if err := os.MkdirAll(buildIDDir, 0750); err != nil { + t.Fatalf("Failed to create build ID directory: %v", err) + } + + buildIDFile := filepath.Join(buildIDDir, ".build-id") + if err := os.WriteFile(buildIDFile, []byte("test-build-id"), 0600); err != nil { + t.Fatalf("Failed to write build ID file: %v", err) + } + + if err := os.Chmod(buildIDDir, 0000); err != nil { + t.Fatalf("Failed to set directory permissions: %v", err) + } + defer os.Chmod(buildIDDir, 0750) + + // When GetBuildID is called + _, err := rt.GetBuildID() + + // Then an error should be returned + + if err == nil { + t.Error("Expected error when ReadFile fails") + } + + if !strings.Contains(err.Error(), "failed to read build ID file") { + t.Errorf("Expected error about reading build ID file, got: %v", err) + } + }) + + t.Run("ErrorWhenWriteBuildIDToFileFails", func(t *testing.T) { + // Given a runtime with a build ID directory that cannot be written to + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + tmpDir := t.TempDir() + rt.ProjectRoot = tmpDir + + buildIDDir := filepath.Join(tmpDir, ".windsor") + if err := os.MkdirAll(buildIDDir, 0750); err != nil { + t.Fatalf("Failed to create build ID directory: %v", err) + } + + if err := os.Chmod(buildIDDir, 0000); err != nil { + t.Fatalf("Failed to set directory permissions: %v", err) + } + defer os.Chmod(buildIDDir, 0750) + + // When GetBuildID is called + _, err := rt.GetBuildID() + + // Then an error should be returned + + if err == nil { + t.Error("Expected error when writeBuildIDToFile fails") + } + + if !strings.Contains(err.Error(), "failed to set build ID") && !strings.Contains(err.Error(), "failed to read build ID file") { + t.Errorf("Expected error about setting or reading build ID, got: %v", err) + } + }) } func TestRuntime_GenerateBuildID(t *testing.T) { t.Run("GeneratesAndSavesBuildID", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with no existing build ID + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir + + // When GenerateBuildID is called + buildID, err := rt.GenerateBuildID() - buildID, err := ctx.GenerateBuildID() + // Then a new build ID should be generated and saved if err != nil { t.Errorf("Expected no error, got: %v", err) @@ -1570,18 +1946,22 @@ func TestRuntime_GenerateBuildID(t *testing.T) { }) t.Run("IncrementsBuildIDOnSubsequentCalls", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with an existing build ID + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir - buildID1, err := ctx.GenerateBuildID() + buildID1, err := rt.GenerateBuildID() if err != nil { t.Fatalf("Failed to generate first build ID: %v", err) } - buildID2, err := ctx.GenerateBuildID() + // When GenerateBuildID is called again + buildID2, err := rt.GenerateBuildID() + + // Then the build ID should be incremented if err != nil { t.Fatalf("Failed to generate second build ID: %v", err) } @@ -1592,13 +1972,17 @@ func TestRuntime_GenerateBuildID(t *testing.T) { }) t.Run("ErrorOnInvalidFormat", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with an invalid build ID format + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir + + // When incrementBuildID is called with invalid format + buildID, err := rt.incrementBuildID("invalid", "251112") - buildID, err := ctx.incrementBuildID("invalid", "251112") + // Then an error should be returned if err == nil { t.Error("Expected error for invalid format") @@ -1610,13 +1994,17 @@ func TestRuntime_GenerateBuildID(t *testing.T) { }) t.Run("ErrorOnInvalidCounter", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with an invalid build ID counter + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir - buildID, err := ctx.incrementBuildID("251112.123.abc", "251112") + // When incrementBuildID is called with invalid counter + buildID, err := rt.incrementBuildID("251112.123.abc", "251112") + + // Then an error should be returned if err == nil { t.Error("Expected error for invalid counter") @@ -1628,24 +2016,550 @@ func TestRuntime_GenerateBuildID(t *testing.T) { }) t.Run("ResetsCounterOnDateChange", func(t *testing.T) { - mocks := setupEnvironmentMocks(t) - ctx := mocks.Runtime + // Given a runtime with a build ID from a previous date + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime tmpDir := t.TempDir() - ctx.ProjectRoot = tmpDir + rt.ProjectRoot = tmpDir + + oldDate := "251111" + newDate := "251112" + existingBuildID := fmt.Sprintf("%s.123.5", oldDate) - buildID, err := ctx.incrementBuildID("251111.123.5", "251112") + // When incrementBuildID is called with a new date + buildID, err := rt.incrementBuildID(existingBuildID, newDate) + + // Then the counter should be reset if err != nil { - t.Errorf("Expected no error, got: %v", err) + t.Fatalf("Expected no error, got: %v", err) } - if !strings.HasPrefix(buildID, "251112.") { - t.Error("Expected new date in build ID") + if !strings.HasPrefix(buildID, newDate) { + t.Errorf("Expected build ID to start with new date %s, got: %s", newDate, buildID) } if !strings.HasSuffix(buildID, ".1") { - t.Error("Expected counter to reset to 1") + t.Errorf("Expected build ID to end with .1, got: %s", buildID) + } + }) + + t.Run("IncrementsCounterOnSameDate", func(t *testing.T) { + // Given a runtime with a build ID from the same date + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + tmpDir := t.TempDir() + rt.ProjectRoot = tmpDir + + date := "251112" + existingBuildID := fmt.Sprintf("%s.123.5", date) + + // When incrementBuildID is called with the same date + buildID, err := rt.incrementBuildID(existingBuildID, date) + + // Then the counter should be incremented + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if !strings.HasPrefix(buildID, date) { + t.Errorf("Expected build ID to start with date %s, got: %s", date, buildID) + } + + if !strings.Contains(buildID, ".123.6") { + t.Errorf("Expected build ID to contain incremented counter .123.6, got: %s", buildID) + } + }) + + t.Run("ErrorWhenWriteBuildIDToFileFails", func(t *testing.T) { + // Given a runtime with a build ID directory that cannot be written to + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + tmpDir := t.TempDir() + rt.ProjectRoot = tmpDir + + buildIDDir := filepath.Join(tmpDir, ".windsor") + if err := os.MkdirAll(buildIDDir, 0750); err != nil { + t.Fatalf("Failed to create build ID directory: %v", err) + } + + if err := os.Chmod(buildIDDir, 0000); err != nil { + t.Fatalf("Failed to set directory permissions: %v", err) + } + defer os.Chmod(buildIDDir, 0750) + + // When GenerateBuildID is called + _, err := rt.GenerateBuildID() + + // Then an error should be returned + + if err == nil { + t.Error("Expected error when writeBuildIDToFile fails") + } + + if !strings.Contains(err.Error(), "failed to set build ID") && !strings.Contains(err.Error(), "failed to read build ID file") { + t.Errorf("Expected error about setting or reading build ID, got: %v", err) + } + }) +} + +// ============================================================================= +// Test Private Methods +// ============================================================================= + +func TestRuntime_loadSecrets(t *testing.T) { + t.Run("LoadsSecretsSuccessfully", func(t *testing.T) { + // Given a runtime with secrets providers + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell) + mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) + + rt.SecretsProviders.Sops = mockSopsProvider + rt.SecretsProviders.Onepassword = mockOnepasswordProvider + + // When loadSecrets is called + err := rt.loadSecrets() + + // Then secrets should load successfully + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + }) + + t.Run("HandlesSecretsProviderError", func(t *testing.T) { + // Given a runtime with a secrets provider that fails to load + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockProvider := secrets.NewMockSecretsProvider(mocks.Shell) + mockProvider.LoadSecretsFunc = func() error { + return errors.New("secrets load failed") + } + + rt.SecretsProviders.Sops = mockProvider + + // When loadSecrets is called + err := rt.loadSecrets() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + + if !strings.Contains(err.Error(), "secrets load failed") { + t.Errorf("Expected secrets load error, got: %v", err) + } + }) + + t.Run("HandlesNilProviders", func(t *testing.T) { + // Given a runtime with nil secrets providers + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + rt.SecretsProviders.Sops = nil + rt.SecretsProviders.Onepassword = nil + + // When loadSecrets is called + err := rt.loadSecrets() + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error with nil providers, got: %v", err) + } + }) + + t.Run("HandlesMixedProviders", func(t *testing.T) { + // Given a runtime with mixed secrets providers + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockProvider := secrets.NewMockSecretsProvider(mocks.Shell) + rt.SecretsProviders.Sops = mockProvider + rt.SecretsProviders.Onepassword = nil + + // When loadSecrets is called + err := rt.loadSecrets() + + // Then no error should be returned + if err != nil { + t.Fatalf("Expected no error with mixed providers, got: %v", err) + } + }) +} + +func TestRuntime_initializeSecretsProviders(t *testing.T) { + t.Run("InitializesSopsProviderWhenEnabled", func(t *testing.T) { + // Given a runtime with SOPS enabled in config + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "secrets.sops.enabled" { + return true + } + return false + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then SOPS provider should be initialized + + if rt.SecretsProviders.Sops == nil { + t.Error("Expected SOPS provider to be initialized") + } + }) + + t.Run("InitializesOnepasswordProviderWhenEnabled", func(t *testing.T) { + // Given a runtime with 1Password enabled in config + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "secrets.onepassword.enabled" { + return true + } + return false + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then 1Password provider should be initialized + + if rt.SecretsProviders.Onepassword == nil { + t.Error("Expected 1Password provider to be initialized") + } + }) + + t.Run("SkipsProvidersWhenDisabled", func(t *testing.T) { + // Given a runtime with secrets providers disabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + return false + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then providers should not be initialized + + if rt.SecretsProviders.Sops != nil { + t.Error("Expected SOPS provider to be nil when disabled") + } + + if rt.SecretsProviders.Onepassword != nil { + t.Error("Expected 1Password provider to be nil when disabled") + } + }) + + t.Run("DoesNotOverrideExistingProviders", func(t *testing.T) { + // Given a runtime with an existing secrets provider + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + existingProvider := secrets.NewMockSecretsProvider(mocks.Shell) + rt.SecretsProviders.Sops = existingProvider + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "secrets.sops.enabled" { + return true + } + return false + } + + // When initializeSecretsProviders is called + rt.initializeSecretsProviders() + + // Then existing provider should be preserved + if rt.SecretsProviders.Sops != existingProvider { + t.Error("Expected existing provider to be preserved") + } + }) +} + +func TestRuntime_initializeComponents_EdgeCases(t *testing.T) { + t.Run("ReturnsNil", func(t *testing.T) { + // Given a runtime with mocks + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + // When initializeComponents is called + err := rt.initializeComponents() + + // Then no error should be returned + if err != nil { + t.Errorf("Expected nil, got: %v", err) + } + }) +} + +func TestRuntime_initializeEnvPrinters(t *testing.T) { + t.Run("InitializesAwsEnvWhenEnabled", func(t *testing.T) { + // Given a runtime with AWS enabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "aws.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then AWS env printer should be initialized + + if rt.EnvPrinters.AwsEnv == nil { + t.Error("Expected AwsEnv to be initialized") + } + }) + + t.Run("InitializesAzureEnvWhenEnabled", func(t *testing.T) { + // Given a runtime with Azure enabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "azure.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Azure env printer should be initialized + + if rt.EnvPrinters.AzureEnv == nil { + t.Error("Expected AzureEnv to be initialized") + } + }) + + t.Run("InitializesDockerEnvWhenEnabled", func(t *testing.T) { + // Given a runtime with Docker enabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "docker.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Docker env printer should be initialized + + if rt.EnvPrinters.DockerEnv == nil { + t.Error("Expected DockerEnv to be initialized") + } + }) + + t.Run("InitializesKubeEnvWhenEnabled", func(t *testing.T) { + // Given a runtime with cluster enabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "cluster.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Kube env printer should be initialized + + if rt.EnvPrinters.KubeEnv == nil { + t.Error("Expected KubeEnv to be initialized") + } + }) + + t.Run("InitializesTalosEnvWhenDriverIsTalos", func(t *testing.T) { + // Given a runtime with Talos cluster driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "talos" + } + return "" + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Talos env printer should be initialized + + if rt.EnvPrinters.TalosEnv == nil { + t.Error("Expected TalosEnv to be initialized") + } + }) + + t.Run("InitializesTalosEnvWhenDriverIsOmni", func(t *testing.T) { + // Given a runtime with Omni cluster driver + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetStringFunc = func(key string, defaultValue ...string) string { + if key == "cluster.driver" { + return "omni" + } + return "" + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Talos env printer should be initialized + + if rt.EnvPrinters.TalosEnv == nil { + t.Error("Expected TalosEnv to be initialized") + } + }) + + t.Run("InitializesTerraformEnvWhenEnabled", func(t *testing.T) { + // Given a runtime with Terraform enabled + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "terraform.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Terraform env printer should be initialized + + if rt.EnvPrinters.TerraformEnv == nil { + t.Error("Expected TerraformEnv to be initialized") + } + }) + + t.Run("InitializesWindsorEnvAlways", func(t *testing.T) { + // Given a runtime with mocks + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Windsor env printer should always be initialized + + if rt.EnvPrinters.WindsorEnv == nil { + t.Error("Expected WindsorEnv to be initialized") + } + }) + + t.Run("SkipsInitializationWhenShellIsNil", func(t *testing.T) { + // Given a runtime with nil shell + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + rt.Shell = nil + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then env printers should not be initialized + + if rt.EnvPrinters.AwsEnv != nil { + t.Error("Expected AwsEnv not to be initialized when Shell is nil") + } + }) + + t.Run("SkipsInitializationWhenConfigHandlerIsNil", func(t *testing.T) { + // Given a runtime with nil config handler + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + rt.ConfigHandler = nil + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then env printers should not be initialized + + if rt.EnvPrinters.AwsEnv != nil { + t.Error("Expected AwsEnv not to be initialized when ConfigHandler is nil") + } + }) + + t.Run("DoesNotOverrideExistingPrinters", func(t *testing.T) { + // Given a runtime with an existing env printer + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + existingAwsEnv := env.NewMockEnvPrinter() + rt.EnvPrinters.AwsEnv = existingAwsEnv + + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) + mockConfigHandler.GetBoolFunc = func(key string, defaultValue ...bool) bool { + if key == "aws.enabled" { + return true + } + return false + } + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then existing printer should be preserved + + if rt.EnvPrinters.AwsEnv != existingAwsEnv { + t.Error("Expected existing AwsEnv to be preserved") + } + }) + + t.Run("InitializesWindsorEnvWithSecretsProviders", func(t *testing.T) { + // Given a runtime with secrets providers + mocks := setupRuntimeMocks(t) + rt := mocks.Runtime + + mockSopsProvider := secrets.NewMockSecretsProvider(mocks.Shell) + mockOnepasswordProvider := secrets.NewMockSecretsProvider(mocks.Shell) + rt.SecretsProviders.Sops = mockSopsProvider + rt.SecretsProviders.Onepassword = mockOnepasswordProvider + + // When initializeEnvPrinters is called + rt.initializeEnvPrinters() + + // Then Windsor env printer should be initialized with secrets providers + + if rt.EnvPrinters.WindsorEnv == nil { + t.Error("Expected WindsorEnv to be initialized with secrets providers") } }) } From 7da2da7313fba964acee5fd51bb9bea5f2ec8f21 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:44:45 -0500 Subject: [PATCH 02/14] Make env tests conformal Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/env/aws_env_test.go | 137 ++++++--- pkg/runtime/env/azure_env_test.go | 87 ++++-- pkg/runtime/env/docker_env_test.go | 235 ++++++++------- pkg/runtime/env/env_test.go | 122 ++++---- pkg/runtime/env/kube_env_test.go | 14 +- pkg/runtime/env/shims_test.go | 4 +- pkg/runtime/env/talos_env_test.go | 6 +- pkg/runtime/env/terraform_env_test.go | 328 +++++++++++++++++++- pkg/runtime/env/windsor_env_test.go | 417 +++++++++++++++++++++----- 9 files changed, 1006 insertions(+), 344 deletions(-) diff --git a/pkg/runtime/env/aws_env_test.go b/pkg/runtime/env/aws_env_test.go index fc0c7bc8a..1e8cac668 100644 --- a/pkg/runtime/env/aws_env_test.go +++ b/pkg/runtime/env/aws_env_test.go @@ -17,57 +17,81 @@ import ( // Test Setup // ============================================================================= -func setupAwsEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupAwsEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() - if len(opts) == 0 || opts[0].ConfigStr == "" { - opts = []*SetupOptions{{ - ConfigStr: ` -version: v1alpha1 -contexts: - test-context: - aws: - aws_profile: default - aws_endpoint_url: https://aws.endpoint - s3_hostname: s3.amazonaws.com - mwaa_endpoint: https://mwaa.endpoint -`, - }} + + // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) + mocks := setupEnvMocks(t, opts...) + + // If ConfigHandler wasn't overridden, use MockConfigHandler + if _, ok := mocks.ConfigHandler.(*config.MockConfigHandler); !ok { + mocks.ConfigHandler = config.NewMockConfigHandler() } - // Create a mock config handler - mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) - // Set up the GetConfigRoot function to return a mock path mockConfigHandler.GetConfigRootFunc = func() (string, error) { return "/mock/config/root", nil } - // Set up the GetConfig function to return a mock config - mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { - // Parse the config string - var config v1alpha1.Config - if err := yaml.Unmarshal([]byte(opts[0].ConfigStr), &config); err != nil { - t.Fatalf("Failed to unmarshal config: %v", err) + loadedConfigs := make(map[string]*v1alpha1.Context) + currentContext := "test-context" + + mockConfigHandler.GetContextFunc = func() string { + return currentContext + } + + mockConfigHandler.SetContextFunc = func(context string) error { + currentContext = context + return nil + } + + mockConfigHandler.LoadConfigStringFunc = func(content string) error { + var cfg v1alpha1.Config + if err := yaml.Unmarshal([]byte(content), &cfg); err != nil { + return err + } + for name, ctx := range cfg.Contexts { + if ctx != nil { + ctxCopy := *ctx + loadedConfigs[name] = &ctxCopy + } else { + loadedConfigs[name] = &v1alpha1.Context{} + } } + return nil + } - // Return the context for the test-context - if ctx, ok := config.Contexts["test-context"]; ok { + mockConfigHandler.GetConfigFunc = func() *v1alpha1.Context { + if ctx, ok := loadedConfigs[currentContext]; ok { return ctx } return &v1alpha1.Context{} } - // Create mocks with the mock config handler - mocks := setupMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, - }) + // Only load default config if ConfigHandler wasn't overridden via opts + // If ConfigHandler was injected via opts, assume test wants to control it + if len(opts) == 0 { + defaultConfigStr := ` +version: v1alpha1 +contexts: + test-context: + aws: + aws_profile: default + aws_endpoint_url: https://aws.endpoint + s3_hostname: s3.amazonaws.com + mwaa_endpoint: https://mwaa.endpoint +` + + if err := mocks.ConfigHandler.LoadConfigString(defaultConfigStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + } - // ConfigHandler is now fully initialized in constructor if err := mocks.ConfigHandler.SetContext("test-context"); err != nil { t.Fatalf("Failed to set context: %v", err) } - // Set up shims for AWS config file check mocks.Shims.Stat = func(name string) (os.FileInfo, error) { if name == filepath.FromSlash("/mock/config/root/.aws/config") { return nil, nil @@ -84,7 +108,7 @@ contexts: // TestAwsEnv_GetEnvVars tests the GetEnvVars method of the AwsEnvPrinter func TestAwsEnv_GetEnvVars(t *testing.T) { - setup := func() (*AwsEnvPrinter, *Mocks) { + setup := func() (*AwsEnvPrinter, *EnvTestMocks) { mocks := setupAwsEnvMocks(t) env := NewAwsEnvPrinter(mocks.Shell, mocks.ConfigHandler) env.shims = mocks.Shims @@ -92,9 +116,13 @@ func TestAwsEnv_GetEnvVars(t *testing.T) { } t.Run("Success", func(t *testing.T) { + // Given an AWS env printer with configuration env, _ := setup() + // When GetEnvVars is called envVars, err := env.GetEnvVars() + + // Then environment variables should match expected values if err != nil { t.Errorf("GetEnvVars returned an error: %v", err) } @@ -113,14 +141,17 @@ func TestAwsEnv_GetEnvVars(t *testing.T) { }) t.Run("NonExistentConfigFile", func(t *testing.T) { + // Given an AWS env printer with non-existent config file env, _ := setup() - // Override shims to make AWS config file not exist env.shims.Stat = func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist } + // When GetEnvVars is called envVars, err := env.GetEnvVars() + + // Then environment variables should be returned without AWS_CONFIG_FILE if err != nil { t.Errorf("GetEnvVars returned an error: %v", err) } @@ -138,44 +169,60 @@ func TestAwsEnv_GetEnvVars(t *testing.T) { }) t.Run("MissingConfiguration", func(t *testing.T) { - mocks := setupAwsEnvMocks(t, &SetupOptions{ - ConfigStr: ` + // Given an AWS env printer with missing AWS configuration + mocks := setupAwsEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: {} -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } env := NewAwsEnvPrinter(mocks.Shell, mocks.ConfigHandler) + env.shims = mocks.Shims + // When GetEnvVars is called _, err := env.GetEnvVars() + + // Then an error should be returned if err == nil { t.Error("GetEnvVars did not return an error") } - if !strings.Contains(err.Error(), "context configuration or AWS configuration is missing") { + if err != nil && !strings.Contains(err.Error(), "context configuration or AWS configuration is missing") { t.Errorf("GetEnvVars returned error %v, want error containing 'context configuration or AWS configuration is missing'", err) } }) t.Run("GetConfigRootError", func(t *testing.T) { - mocks := setupAwsEnvMocks(t, &SetupOptions{ - ConfigStr: ` + // Given a printer with a config handler that fails to get config root + mocks := setupAwsEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: aws: aws_profile: default -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + if err := mocks.ConfigHandler.SetContext("test-context"); err != nil { + t.Fatalf("Failed to set context: %v", err) + } + + env := NewAwsEnvPrinter(mocks.Shell, mocks.ConfigHandler) + env.shims = mocks.Shims - // Mock the GetConfigRoot function to return an error mockConfigHandler := mocks.ConfigHandler.(*config.MockConfigHandler) mockConfigHandler.GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("error retrieving configuration root directory") } - env := NewAwsEnvPrinter(mocks.Shell, mocks.ConfigHandler) - + // When GetEnvVars is called _, err := env.GetEnvVars() + + // Then an error should be returned if err == nil { t.Error("GetEnvVars did not return an error") } diff --git a/pkg/runtime/env/azure_env_test.go b/pkg/runtime/env/azure_env_test.go index 02110416e..9f64d61cd 100644 --- a/pkg/runtime/env/azure_env_test.go +++ b/pkg/runtime/env/azure_env_test.go @@ -4,7 +4,6 @@ import ( "fmt" "os" "path/filepath" - "reflect" "strings" "testing" @@ -15,30 +14,34 @@ import ( // Test Setup // ============================================================================= -func setupAzureEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupAzureEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() - if opts == nil { - opts = []*SetupOptions{{}} - } - if opts[0].ConfigStr == "" { - opts[0].ConfigStr = ` + // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) + mocks := setupEnvMocks(t, opts...) + + // Only load default config if ConfigHandler wasn't overridden via opts + // If ConfigHandler was injected via opts, assume test wants to control it + // Check by seeing if it's a MockConfigHandler (which would indicate injection) or if opts were provided + if len(opts) == 0 { + configStr := ` version: v1alpha1 contexts: - mock-context: + test-context: azure: subscription_id: "test-subscription" tenant_id: "test-tenant" environment: "test-environment" ` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mocks.ConfigHandler.SetContext("test-context") } - if opts[0].Context == "" { - opts[0].Context = "mock-context" - } - mocks := setupMocks(t, opts[0]) - // Mock stat function to make Azure config directory exist + configRoot, _ := mocks.ConfigHandler.GetConfigRoot() + azureConfigDir := filepath.Join(configRoot, ".azure") mocks.Shims.Stat = func(name string) (os.FileInfo, error) { - if name == filepath.FromSlash("/mock/config/root/.azure") { + if name == azureConfigDir { return nil, nil } return nil, os.ErrNotExist @@ -52,7 +55,7 @@ contexts: // ============================================================================= func TestAzureEnv_GetEnvVars(t *testing.T) { - setup := func(t *testing.T, opts ...*SetupOptions) (*AzureEnvPrinter, *Mocks) { + setup := func(t *testing.T, opts ...func(*EnvTestMocks)) (*AzureEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupAzureEnvMocks(t, opts...) printer := NewAzureEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -61,15 +64,20 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { } t.Run("Success", func(t *testing.T) { + // Given a printer with Azure configuration printer, mocks := setup(t) configRoot, err := mocks.ConfigHandler.GetConfigRoot() if err != nil { t.Fatalf("Failed to get config root: %v", err) } + + // When GetEnvVars is called envVars, err := printer.GetEnvVars() if err != nil { t.Errorf("Expected no error, got %v", err) } + + // Then the environment variables should match expected values expectedEnvVars := map[string]string{ "AZURE_CONFIG_DIR": filepath.ToSlash(filepath.Join(configRoot, ".azure")), "AZURE_CORE_LOGIN_EXPERIENCE_V2": "false", @@ -77,21 +85,38 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { "ARM_TENANT_ID": "test-tenant", "ARM_ENVIRONMENT": "test-environment", } - if !reflect.DeepEqual(envVars, expectedEnvVars) { - t.Errorf("GetEnvVars returned %v, want %v", envVars, expectedEnvVars) + if envVars["AZURE_CORE_LOGIN_EXPERIENCE_V2"] != expectedEnvVars["AZURE_CORE_LOGIN_EXPERIENCE_V2"] { + t.Errorf("GetEnvVars returned AZURE_CORE_LOGIN_EXPERIENCE_V2=%v, want %v", envVars["AZURE_CORE_LOGIN_EXPERIENCE_V2"], expectedEnvVars["AZURE_CORE_LOGIN_EXPERIENCE_V2"]) + } + if envVars["ARM_SUBSCRIPTION_ID"] != expectedEnvVars["ARM_SUBSCRIPTION_ID"] { + t.Errorf("GetEnvVars returned ARM_SUBSCRIPTION_ID=%v, want %v", envVars["ARM_SUBSCRIPTION_ID"], expectedEnvVars["ARM_SUBSCRIPTION_ID"]) + } + if envVars["ARM_TENANT_ID"] != expectedEnvVars["ARM_TENANT_ID"] { + t.Errorf("GetEnvVars returned ARM_TENANT_ID=%v, want %v", envVars["ARM_TENANT_ID"], expectedEnvVars["ARM_TENANT_ID"]) + } + if envVars["ARM_ENVIRONMENT"] != expectedEnvVars["ARM_ENVIRONMENT"] { + t.Errorf("GetEnvVars returned ARM_ENVIRONMENT=%v, want %v", envVars["ARM_ENVIRONMENT"], expectedEnvVars["ARM_ENVIRONMENT"]) + } + if !strings.HasSuffix(envVars["AZURE_CONFIG_DIR"], filepath.ToSlash("/.azure")) { + t.Errorf("GetEnvVars returned AZURE_CONFIG_DIR=%v, want path ending with /.azure", envVars["AZURE_CONFIG_DIR"]) } }) t.Run("GetConfigRootError", func(t *testing.T) { + // Given a printer with a config handler that fails to get config root mockConfigHandler := &config.MockConfigHandler{} mockConfigHandler.GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("error retrieving configuration root directory") } - mocks := setupAzureEnvMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, + mocks := setupAzureEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = mockConfigHandler }) printer := NewAzureEnvPrinter(mocks.Shell, mocks.ConfigHandler) + + // When GetEnvVars is called _, err := printer.GetEnvVars() + + // Then an error should be returned if err == nil { t.Error("Expected error, got nil") } @@ -101,16 +126,26 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { }) t.Run("MissingConfiguration", func(t *testing.T) { - // Setup without default Azure config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + // Given a printer with no Azure configuration + mocks := setupAzureEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = config.NewConfigHandler(m.Shell) + }) + configStr := ` version: v1alpha1 contexts: - mock-context: {} -`, - Context: "mock-context", - }) + test-context: {} +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mocks.ConfigHandler.SetContext("test-context") + printer := NewAzureEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + + // When GetEnvVars is called envVars, err := printer.GetEnvVars() + + // Then no error should be returned and environment variables should be empty if err != nil { t.Errorf("Expected no error, got %v", err) } diff --git a/pkg/runtime/env/docker_env_test.go b/pkg/runtime/env/docker_env_test.go index 7e2da8d0b..04320402f 100644 --- a/pkg/runtime/env/docker_env_test.go +++ b/pkg/runtime/env/docker_env_test.go @@ -23,11 +23,15 @@ type DockerEnvPrinterMocks struct { } // setupDockerEnvMocks creates a new set of mocks for Docker environment tests -func setupDockerEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupDockerEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() - if len(opts) == 0 || opts[0].ConfigStr == "" { - opts = []*SetupOptions{{ - ConfigStr: ` + // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) + mocks := setupEnvMocks(t, opts...) + + // Only load default config if ConfigHandler wasn't overridden via opts + // If ConfigHandler was injected via opts, assume test wants to control it + if len(opts) == 0 { + configStr := ` version: v1alpha1 contexts: test-context: @@ -37,15 +41,14 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }} - } - - // Create mocks with the config - mocks := setupMocks(t, opts...) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } - // Set the context - mocks.ConfigHandler.SetContext("test-context") + // Set the context + mocks.ConfigHandler.SetContext("test-context") + } // Set up shims for Docker operations mocks.Shims.UserHomeDir = func() (string, error) { @@ -67,19 +70,7 @@ func TestDockerEnvPrinter_GetEnvVars(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a new DockerEnvPrinter with default configuration - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` -version: v1alpha1 -contexts: - test-context: - vm: - driver: colima - docker: - registries: - mock-registry-url: - hostport: 5000 -`, - }) + mocks := setupDockerEnvMocks(t) printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims @@ -119,19 +110,7 @@ contexts: t.Run("ColimaDriver", func(t *testing.T) { // Given a new DockerEnvPrinter with Colima driver os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` -version: v1alpha1 -contexts: - test-context: - vm: - driver: colima - docker: - registries: - mock-registry-url: - hostport: 5000 -`, - }) + mocks := setupDockerEnvMocks(t) printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims @@ -171,8 +150,8 @@ contexts: t.Run("DockerDesktopDriver", func(t *testing.T) { // Given a new DockerEnvPrinter with Docker Desktop driver on Linux os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -182,8 +161,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // And Linux OS environment mocks.Shims.Goos = func() string { @@ -260,8 +241,8 @@ contexts: t.Run("DockerDriver", func(t *testing.T) { // Given a new DockerEnvPrinter with Docker driver os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -271,8 +252,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims @@ -402,8 +385,8 @@ contexts: t.Run(tc.name, func(t *testing.T) { // Given a new DockerEnvPrinter with specific OS os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -413,8 +396,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } mocks.Shims.Goos = func() string { return tc.os @@ -444,8 +429,8 @@ contexts: os.Setenv("DOCKER_HOST", "tcp://custom-docker-host:2375") defer os.Unsetenv("DOCKER_HOST") - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -455,8 +440,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } mocks.Shims.LookupEnv = func(key string) (string, bool) { if key == "DOCKER_HOST" { @@ -485,19 +472,7 @@ contexts: t.Run("DockerHostNotSet", func(t *testing.T) { // Given a new DockerEnvPrinter without DOCKER_HOST environment variable - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` -version: v1alpha1 -contexts: - test-context: - vm: - driver: colima - docker: - registries: - mock-registry-url: - hostport: 5000 -`, - }) + mocks := setupDockerEnvMocks(t) mocks.Shims.LookupEnv = func(key string) (string, bool) { return "", false @@ -528,8 +503,8 @@ contexts: t.Run("DockerHostFromEnvironmentOverridesDriver", func(t *testing.T) { // Given a new DockerEnvPrinter with both DOCKER_HOST and vm driver - mocks := setupDockerEnvMocks(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -539,8 +514,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } mocks.Shims.LookupEnv = func(key string) (string, bool) { if key == "DOCKER_HOST" { @@ -627,7 +604,7 @@ func TestDockerEnvPrinter_GetAlias(t *testing.T) { // TestDockerEnvPrinter_getRegistryURL tests the getRegistryURL method of the DockerEnvPrinter func TestDockerEnvPrinter_getRegistryURL(t *testing.T) { // setup creates a new DockerEnvPrinter with the given configuration - setup := func(t *testing.T, opts ...*SetupOptions) (*DockerEnvPrinter, *Mocks) { + setup := func(t *testing.T, opts ...func(*EnvTestMocks)) (*DockerEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupDockerEnvMocks(t, opts...) printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -637,8 +614,8 @@ func TestDockerEnvPrinter_getRegistryURL(t *testing.T) { t.Run("ValidRegistryURL", func(t *testing.T) { // Given a DockerEnvPrinter with a valid registry URL in config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -646,7 +623,10 @@ contexts: driver: colima docker: registry_url: registry.example.com:5000 -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // And the registry URL is set in the context @@ -667,8 +647,8 @@ contexts: t.Run("RegistryURLWithConfig", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL and matching config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -679,7 +659,10 @@ contexts: registries: registry.example.com: hostport: 5000 -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // And the registry URL is set in the context @@ -700,8 +683,8 @@ contexts: t.Run("EmptyRegistryURL", func(t *testing.T) { // Given a DockerEnvPrinter with no registry URL but with registries config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -711,7 +694,10 @@ contexts: registries: mock-registry-url: hostport: 5000 -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // When getting the registry URL @@ -729,8 +715,10 @@ contexts: t.Run("EmptyConfig", func(t *testing.T) { // Given a DockerEnvPrinter with empty registries config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = config.NewConfigHandler(m.Shell) + }) + configStr := ` version: v1alpha1 contexts: test-context: @@ -738,8 +726,12 @@ contexts: driver: colima docker: registries: {} -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims // When getting the registry URL url, err := printer.getRegistryURL() @@ -756,8 +748,8 @@ contexts: t.Run("RegistryURLWithoutPortNoConfig", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL without port and no matching config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -768,7 +760,10 @@ contexts: registries: other-registry: hostport: 5000 -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // And the registry URL is set in the context @@ -789,8 +784,8 @@ contexts: t.Run("RegistryURLInvalidPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL with invalid port - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -798,7 +793,10 @@ contexts: driver: colima docker: registry_url: registry.example.com:invalid -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // When getting the registry URL @@ -816,8 +814,8 @@ contexts: t.Run("RegistryURLNoPortNoHostPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL without port and no hostport in config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -827,7 +825,10 @@ contexts: registry_url: registry.example.com registries: registry.example.com: {} -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // When getting the registry URL @@ -845,8 +846,8 @@ contexts: t.Run("RegistryURLEmptyRegistries", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL and empty registries config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -855,7 +856,10 @@ contexts: docker: registry_url: registry.example.com registries: {} -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // When getting the registry URL @@ -873,15 +877,21 @@ contexts: t.Run("NilDockerConfig", func(t *testing.T) { // Given a DockerEnvPrinter with no Docker config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = config.NewConfigHandler(m.Shell) + }) + configStr := ` version: v1alpha1 contexts: test-context: vm: driver: colima -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims // When getting the registry URL url, err := printer.getRegistryURL() @@ -898,8 +908,8 @@ contexts: t.Run("NilRegistriesWithURL", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL but no registries config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + printer, _ := setup(t, func(m *EnvTestMocks) { + configStr := ` version: v1alpha1 contexts: test-context: @@ -907,7 +917,10 @@ contexts: driver: colima docker: registry_url: registry.example.com -`, +` + if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } }) // When getting the registry URL @@ -925,8 +938,10 @@ contexts: t.Run("RegistryWithoutHostPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry without hostport in config - printer, _ := setup(t, &SetupOptions{ - ConfigStr: ` + mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = config.NewConfigHandler(m.Shell) + }) + configStr := ` version: v1alpha1 contexts: test-context: @@ -936,8 +951,12 @@ contexts: registries: registry.example.com: remote: "" -`, - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims // When getting the registry URL url, err := printer.getRegistryURL() diff --git a/pkg/runtime/env/env_test.go b/pkg/runtime/env/env_test.go index e66eed958..d85b1d4b8 100644 --- a/pkg/runtime/env/env_test.go +++ b/pkg/runtime/env/env_test.go @@ -13,36 +13,29 @@ import ( // Test Setup // ============================================================================= -// Mocks holds all the mock objects used in the tests. -type Mocks struct { +// EnvTestMocks holds all the mock objects used in the tests. +type EnvTestMocks struct { ConfigHandler config.ConfigHandler Shell *shell.MockShell Shims *Shims } -type SetupOptions struct { - ConfigHandler config.ConfigHandler - ConfigStr string - Context string -} - -// setupShims creates a new Shims instance with default implementations -func setupShims(t *testing.T) *Shims { - t.Helper() +// setupDefaultShims creates a new Shims instance with default implementations +func setupDefaultShims(tmpDir string) *Shims { shims := NewShims() shims.LookupEnv = func(key string) (string, bool) { return "", false } shims.WriteFile = func(name string, data []byte, perm os.FileMode) error { return nil } shims.ReadFile = func(name string) ([]byte, error) { return []byte{}, nil } shims.MkdirAll = func(path string, perm os.FileMode) error { return nil } - shims.UserHomeDir = func() (string, error) { return t.TempDir(), nil } shims.Stat = func(name string) (os.FileInfo, error) { return nil, nil } - shims.Getwd = func() (string, error) { return t.TempDir(), nil } + shims.UserHomeDir = func() (string, error) { return tmpDir, nil } + shims.Getwd = func() (string, error) { return tmpDir, nil } return shims } -func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() // Store original directory and create temp dir @@ -56,18 +49,8 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // Set project root environment variable os.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) - // Process options with defaults - options := &SetupOptions{} - if len(opts) > 0 && opts[0] != nil { - options = opts[0] - } - - // Set context from options or default to test-context - if options.Context != "" { - os.Setenv("WINDSOR_CONTEXT", options.Context) - } else { - os.Setenv("WINDSOR_CONTEXT", "test-context") - } + // Set context default to test-context + os.Setenv("WINDSOR_CONTEXT", "test-context") // Create shell with project root matching temp dir mockShell := shell.NewMockShell() @@ -75,19 +58,20 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { return tmpDir, nil } - // Create config handler - var configHandler config.ConfigHandler - if options.ConfigHandler == nil { - configHandler = config.NewConfigHandler(mockShell) - } else { - configHandler = options.ConfigHandler - } - if options.ConfigStr != "" { - configHandler.LoadConfigString(options.ConfigStr) + // Setup shims + shims := setupDefaultShims(tmpDir) + + // Create initial mocks with defaults + mocks := &EnvTestMocks{ + Shell: mockShell, + ConfigHandler: config.NewConfigHandler(mockShell), + Shims: shims, } - // Setup shims - shims := setupShims(t) + // Apply any dependency injection overrides BEFORE using mocks + for _, opt := range opts { + opt(mocks) + } // Register cleanup to restore original state t.Cleanup(func() { @@ -98,12 +82,7 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { } }) - // Return mocks - return &Mocks{ - Shell: mockShell, - ConfigHandler: configHandler, - Shims: shims, - } + return mocks } // ============================================================================= @@ -112,9 +91,9 @@ func setupMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // TestEnv_NewBaseEnvPrinter tests the NewBaseEnvPrinter constructor func TestEnv_NewBaseEnvPrinter(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -131,7 +110,7 @@ func TestEnv_NewBaseEnvPrinter(t *testing.T) { t.Run("WithValidDependencies", func(t *testing.T) { // Given a new BaseEnvPrinter with valid shell and configHandler - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) // Then it should be created successfully @@ -143,9 +122,9 @@ func TestEnv_NewBaseEnvPrinter(t *testing.T) { // TestBaseEnvPrinter_GetEnvVars tests the GetEnvVars method of the BaseEnvPrinter struct func TestBaseEnvPrinter_GetEnvVars(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -167,9 +146,9 @@ func TestBaseEnvPrinter_GetEnvVars(t *testing.T) { // TestBaseEnvPrinter_GetManagedEnv tests the GetManagedEnv method of the BaseEnvPrinter struct func TestBaseEnvPrinter_GetManagedEnv(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -202,11 +181,38 @@ func TestBaseEnvPrinter_GetManagedEnv(t *testing.T) { }) } +// TestBaseEnvPrinter_GetAlias tests the GetAlias method of the BaseEnvPrinter struct +func TestBaseEnvPrinter_GetAlias(t *testing.T) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupEnvMocks(t) + printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) + return printer, mocks + } + + t.Run("Success", func(t *testing.T) { + // Given a new BaseEnvPrinter + printer, _ := setup(t) + + // When calling GetAlias + aliasMap, err := printer.GetAlias() + + // Then no error should be returned and aliasMap should be empty + if err != nil { + t.Errorf("GetAlias() error = %v, want nil", err) + } + expectedAliasMap := map[string]string{} + if !reflect.DeepEqual(aliasMap, expectedAliasMap) { + t.Errorf("aliasMap = %v, want %v", aliasMap, expectedAliasMap) + } + }) +} + // TestBaseEnvPrinter_GetManagedAlias tests the GetManagedAlias method of the BaseEnvPrinter struct func TestBaseEnvPrinter_GetManagedAlias(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -241,9 +247,9 @@ func TestBaseEnvPrinter_GetManagedAlias(t *testing.T) { // TestBaseEnvPrinter_SetManagedEnv tests the SetManagedEnv method of the BaseEnvPrinter struct func TestBaseEnvPrinter_SetManagedEnv(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -308,9 +314,9 @@ func TestBaseEnvPrinter_SetManagedEnv(t *testing.T) { // TestBaseEnvPrinter_SetManagedAlias tests the SetManagedAlias method of the BaseEnvPrinter struct func TestBaseEnvPrinter_SetManagedAlias(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } @@ -375,9 +381,9 @@ func TestBaseEnvPrinter_SetManagedAlias(t *testing.T) { // TestBaseEnvPrinter_Reset tests the Reset method of the BaseEnvPrinter struct func TestBaseEnvPrinter_Reset(t *testing.T) { - setup := func(t *testing.T) (*BaseEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*BaseEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupEnvMocks(t) printer := NewBaseEnvPrinter(mocks.Shell, mocks.ConfigHandler) return printer, mocks } diff --git a/pkg/runtime/env/kube_env_test.go b/pkg/runtime/env/kube_env_test.go index db3d6d617..2efd0048e 100644 --- a/pkg/runtime/env/kube_env_test.go +++ b/pkg/runtime/env/kube_env_test.go @@ -41,13 +41,9 @@ func (m mockFileInfo) IsDir() bool { return true } func (m mockFileInfo) Sys() any { return nil } // setupKubeEnvMocks creates a base mock setup for Kubernetes environment tests -func setupKubeEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupKubeEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() - if len(opts) == 0 { - opts = []*SetupOptions{{}} - } - - mocks := setupMocks(t, opts[0]) + mocks := setupEnvMocks(t, opts...) projectRoot, err := mocks.Shell.GetProjectRoot() if err != nil { t.Fatalf("Failed to get project root: %v", err) @@ -105,7 +101,7 @@ func setupKubeEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // TestKubeEnvPrinter_GetEnvVars tests the GetEnvVars method of the KubeEnvPrinter func TestKubeEnvPrinter_GetEnvVars(t *testing.T) { - setup := func(t *testing.T) (*KubeEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*KubeEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupKubeEnvMocks(t) printer := NewKubeEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -205,7 +201,9 @@ func TestKubeEnvPrinter_GetEnvVars(t *testing.T) { } // And a KubeEnvPrinter with the mock ConfigHandler - mocks := setupKubeEnvMocks(t, &SetupOptions{ConfigHandler: mockConfigHandler}) + mocks := setupKubeEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = mockConfigHandler + }) printer := NewKubeEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims diff --git a/pkg/runtime/env/shims_test.go b/pkg/runtime/env/shims_test.go index 76768cf73..d32be0379 100644 --- a/pkg/runtime/env/shims_test.go +++ b/pkg/runtime/env/shims_test.go @@ -6,9 +6,11 @@ import ( func TestNewShims(t *testing.T) { t.Run("CreatesShimsWithAllFunctions", func(t *testing.T) { + // Given a new Shims instance + // When NewShims is called shims := NewShims() - // Verify all shim functions are initialized + // Then all shim functions should be initialized if shims.Stat == nil { t.Error("Stat shim not initialized") } diff --git a/pkg/runtime/env/talos_env_test.go b/pkg/runtime/env/talos_env_test.go index 3bba4b3c9..c743b1c06 100644 --- a/pkg/runtime/env/talos_env_test.go +++ b/pkg/runtime/env/talos_env_test.go @@ -15,7 +15,7 @@ import ( // TestTalosEnv_GetEnvVars tests the GetEnvVars method of the TalosEnvPrinter func TestTalosEnv_GetEnvVars(t *testing.T) { - setup := func(t *testing.T, provider string) (*TalosEnvPrinter, *Mocks) { + setup := func(t *testing.T, provider string) (*TalosEnvPrinter, *EnvTestMocks) { t.Helper() // Create a mock config handler @@ -27,8 +27,8 @@ func TestTalosEnv_GetEnvVars(t *testing.T) { return "" } - mocks := setupMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, + mocks := setupEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = mockConfigHandler }) // Set up GetConfigRoot to return the correct path diff --git a/pkg/runtime/env/terraform_env_test.go b/pkg/runtime/env/terraform_env_test.go index 478d681df..0cb611727 100644 --- a/pkg/runtime/env/terraform_env_test.go +++ b/pkg/runtime/env/terraform_env_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + blueprintv1alpha1 "github.com/windsorcli/cli/api/v1alpha1" "github.com/windsorcli/cli/pkg/runtime/config" ) @@ -17,9 +18,9 @@ import ( // ============================================================================= // setupTerraformEnvMocks creates and configures mock objects for Terraform environment tests. -func setupTerraformEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { - // Pass the mock config handler to setupMocks - mocks := setupMocks(t, opts...) +func setupTerraformEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { + // Pass the mock config handler to setupEnvMocks + mocks := setupEnvMocks(t, opts...) mocks.Shims.Getwd = func() (string, error) { // Use platform-agnostic path @@ -66,7 +67,7 @@ func setupTerraformEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { // TestTerraformEnv_GetEnvVars tests the GetEnvVars method of the TerraformEnvPrinter func TestTerraformEnv_GetEnvVars(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -125,9 +126,9 @@ func TestTerraformEnv_GetEnvVars(t *testing.T) { }) t.Run("ErrorGettingProjectPath", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing Getwd printer, mocks := setup(t) - // Mock Getwd to return an error mocks.Shims.Getwd = func() (string, error) { return "", fmt.Errorf("mock error getting current directory") } @@ -206,12 +207,13 @@ func TestTerraformEnv_GetEnvVars(t *testing.T) { }) t.Run("ErrorGettingConfigRoot", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing config root lookup configHandler := config.NewMockConfigHandler() configHandler.GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("mock error getting config root") } - mocks := setupTerraformEnvMocks(t, &SetupOptions{ - ConfigHandler: configHandler, + mocks := setupTerraformEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = configHandler }) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims @@ -335,7 +337,7 @@ func TestTerraformEnv_GetEnvVars(t *testing.T) { } func TestTerraformEnv_PostEnvHook(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -427,7 +429,7 @@ func TestTerraformEnv_PostEnvHook(t *testing.T) { } func TestTerraformEnv_findRelativeTerraformProjectPath(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -578,7 +580,7 @@ func TestTerraformEnv_sanitizeForK8s(t *testing.T) { } func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -894,7 +896,7 @@ func TestTerraformEnv_generateBackendOverrideTf(t *testing.T) { } func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -1152,7 +1154,7 @@ func TestTerraformEnv_generateBackendConfigArgs(t *testing.T) { } func TestTerraformEnv_processBackendConfig(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -1240,7 +1242,7 @@ func TestTerraformEnv_processBackendConfig(t *testing.T) { } func TestTerraformEnv_DependencyResolution(t *testing.T) { - setup := func(t *testing.T) (*TerraformEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupTerraformEnvMocks(t) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -1249,6 +1251,7 @@ func TestTerraformEnv_DependencyResolution(t *testing.T) { } t.Run("ValidDependencyChain", func(t *testing.T) { + // Given a TerraformEnvPrinter with valid dependency chain printer, mocks := setup(t) // Mock blueprint.yaml content @@ -1332,6 +1335,7 @@ terraform: }) t.Run("CircularDependencyDetection", func(t *testing.T) { + // Given a TerraformEnvPrinter with circular dependencies printer, mocks := setup(t) // Mock blueprint.yaml content with circular dependencies @@ -1376,6 +1380,7 @@ terraform: }) t.Run("NonExistentDependency", func(t *testing.T) { + // Given a TerraformEnvPrinter with non-existent dependency printer, mocks := setup(t) // Mock blueprint.yaml content with non-existent dependency @@ -1414,6 +1419,7 @@ terraform: }) t.Run("ComponentsWithoutNames", func(t *testing.T) { + // Given a TerraformEnvPrinter with components without names printer, mocks := setup(t) // Mock blueprint.yaml content with components without names @@ -1468,6 +1474,7 @@ terraform: }) t.Run("EmptyTerraformOutput", func(t *testing.T) { + // Given a TerraformEnvPrinter with empty terraform output printer, mocks := setup(t) // Mock blueprint.yaml content @@ -1520,6 +1527,7 @@ terraform: }) t.Run("NoCurrentComponent", func(t *testing.T) { + // Given a TerraformEnvPrinter with no matching current component printer, mocks := setup(t) // Set up the current working directory to not match any component @@ -1546,6 +1554,7 @@ terraform: func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { t.Run("GeneratesCorrectArgsWithoutParallelism", func(t *testing.T) { + // Given a TerraformEnvPrinter without parallelism configuration mocks := setupTerraformEnvMocks(t) printer := &TerraformEnvPrinter{ @@ -1581,6 +1590,7 @@ func TestTerraformEnv_GenerateTerraformArgs(t *testing.T) { }) t.Run("GeneratesCorrectArgsWithParallelism", func(t *testing.T) { + // Given a TerraformEnvPrinter with parallelism configuration mocks := setupTerraformEnvMocks(t) // Mock blueprint.yaml content with parallelism @@ -1642,6 +1652,7 @@ terraform: }) t.Run("ParallelismOnlyAppliedToMatchingComponent", func(t *testing.T) { + // Given a TerraformEnvPrinter with parallelism for a different component mocks := setupTerraformEnvMocks(t) // Mock blueprint.yaml with parallelism for different component @@ -1691,6 +1702,7 @@ terraform: }) t.Run("HandlesMissingBlueprintFile", func(t *testing.T) { + // Given a TerraformEnvPrinter without blueprint.yaml file mocks := setupTerraformEnvMocks(t) printer := &TerraformEnvPrinter{ @@ -1714,6 +1726,7 @@ terraform: }) t.Run("CorrectArgumentOrdering", func(t *testing.T) { + // Given a TerraformEnvPrinter with parallelism configuration mocks := setupTerraformEnvMocks(t) // Mock blueprint.yaml content with parallelism @@ -1763,3 +1776,292 @@ terraform: } }) } + +// TestTerraformEnv_getTerraformComponents tests the getTerraformComponents method of the TerraformEnvPrinter +func TestTerraformEnv_getTerraformComponents(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorWhenGetConfigRootFails", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing GetConfigRoot + mocks := setupTerraformEnvMocks(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting config root") + } + printer := NewTerraformEnvPrinter(mocks.Shell, mockConfigHandler) + printer.shims = mocks.Shims + + // When getTerraformComponents is called without projectPath + result := printer.getTerraformComponents() + + // Then an empty slice should be returned + components, ok := result.([]blueprintv1alpha1.TerraformComponent) + if !ok { + t.Fatalf("Expected []blueprintv1alpha1.TerraformComponent, got %T", result) + } + if len(components) != 0 { + t.Errorf("Expected empty slice, got %v", components) + } + }) + + t.Run("ErrorWhenGetConfigRootFailsWithProjectPath", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing GetConfigRoot + mocks := setupTerraformEnvMocks(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting config root") + } + printer := NewTerraformEnvPrinter(mocks.Shell, mockConfigHandler) + printer.shims = mocks.Shims + + // When getTerraformComponents is called with projectPath + result := printer.getTerraformComponents("test/path") + + // Then nil should be returned + if result != nil { + t.Errorf("Expected nil, got %v", result) + } + }) + + t.Run("ErrorWhenReadFileFails", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing ReadFile + printer, mocks := setup(t) + + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return nil, fmt.Errorf("mock error reading blueprint file") + } + return os.ReadFile(filename) + } + + // When getTerraformComponents is called without projectPath + result := printer.getTerraformComponents() + + // Then an empty slice should be returned + components, ok := result.([]blueprintv1alpha1.TerraformComponent) + if !ok { + t.Fatalf("Expected []blueprintv1alpha1.TerraformComponent, got %T", result) + } + if len(components) != 0 { + t.Errorf("Expected empty slice, got %v", components) + } + }) + + t.Run("ErrorWhenYamlUnmarshalFails", func(t *testing.T) { + // Given a TerraformEnvPrinter with invalid YAML + printer, mocks := setup(t) + + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, "blueprint.yaml") { + return []byte("invalid: yaml: content"), nil + } + return os.ReadFile(filename) + } + + // When getTerraformComponents is called without projectPath + result := printer.getTerraformComponents() + + // Then an empty slice should be returned + components, ok := result.([]blueprintv1alpha1.TerraformComponent) + if !ok { + t.Fatalf("Expected []blueprintv1alpha1.TerraformComponent, got %T", result) + } + if len(components) != 0 { + t.Errorf("Expected empty slice, got %v", components) + } + }) +} + +// TestTerraformEnv_restoreEnvVar tests the restoreEnvVar method of the TerraformEnvPrinter +func TestTerraformEnv_restoreEnvVar(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("RestoresValueWhenOriginalValueNotEmpty", func(t *testing.T) { + // Given a TerraformEnvPrinter and an environment variable with original value + printer, _ := setup(t) + + testKey := "TEST_RESTORE_VAR" + originalValue := "original-value" + os.Setenv(testKey, "changed-value") + defer os.Unsetenv(testKey) + + // When restoreEnvVar is called with original value + printer.restoreEnvVar(testKey, originalValue) + + // Then the environment variable should be restored + if os.Getenv(testKey) != originalValue { + t.Errorf("Expected %s=%s, got %s", testKey, originalValue, os.Getenv(testKey)) + } + }) + + t.Run("UnsetsValueWhenOriginalValueEmpty", func(t *testing.T) { + // Given a TerraformEnvPrinter and an environment variable with empty original value + printer, _ := setup(t) + + testKey := "TEST_UNSET_VAR" + os.Setenv(testKey, "some-value") + defer os.Unsetenv(testKey) + + // When restoreEnvVar is called with empty original value + printer.restoreEnvVar(testKey, "") + + // Then the environment variable should be unset + if os.Getenv(testKey) != "" { + t.Errorf("Expected %s to be unset, got %s", testKey, os.Getenv(testKey)) + } + }) +} + +// TestTerraformEnv_captureTerraformOutputs tests the captureTerraformOutputs method of the TerraformEnvPrinter +func TestTerraformEnv_captureTerraformOutputs(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorWhenGetTerraformComponentsFails", func(t *testing.T) { + // Given a TerraformEnvPrinter with failing getTerraformComponents + mocks := setupTerraformEnvMocks(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting config root") + } + printer := NewTerraformEnvPrinter(mocks.Shell, mockConfigHandler) + printer.shims = mocks.Shims + + // When captureTerraformOutputs is called + result, err := printer.captureTerraformOutputs("/some/path") + + // Then no error should be returned and empty map should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(result) != 0 { + t.Errorf("Expected empty map, got %v", result) + } + }) + + t.Run("ErrorWhenComponentPathNotFound", func(t *testing.T) { + // Given a TerraformEnvPrinter with component path not found + printer, _ := setup(t) + + // When captureTerraformOutputs is called with non-existent module path + result, err := printer.captureTerraformOutputs("/non/existent/path") + + // Then no error should be returned and empty map should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if len(result) != 0 { + t.Errorf("Expected empty map, got %v", result) + } + }) + +} + +// TestTerraformEnv_addDependencyVariables tests the addDependencyVariables method of the TerraformEnvPrinter +func TestTerraformEnv_addDependencyVariables(t *testing.T) { + setup := func(t *testing.T) (*TerraformEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupTerraformEnvMocks(t) + printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("NoOpWhenCurrentComponentIsNil", func(t *testing.T) { + // Given a TerraformEnvPrinter with nil current component + mocks := setupTerraformEnvMocks(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting config root") + } + printer := NewTerraformEnvPrinter(mocks.Shell, mockConfigHandler) + printer.shims = mocks.Shims + + terraformArgs := &TerraformArgs{ + TerraformVars: make(map[string]string), + } + + // When addDependencyVariables is called + err := printer.addDependencyVariables("non-existent/path", terraformArgs) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("NoOpWhenNoDependencies", func(t *testing.T) { + // Given a TerraformEnvPrinter with component without dependencies + printer, mocks := setup(t) + + configRoot, _ := mocks.ConfigHandler.GetConfigRoot() + blueprintPath := filepath.Join(configRoot, "blueprint.yaml") + blueprintYAML := `apiVersion: v1alpha1 +kind: Blueprint +metadata: + name: test-blueprint +terraform: + - path: test/path + fullPath: /some/path + dependsOn: []` + + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if filename == blueprintPath { + return []byte(blueprintYAML), nil + } + return os.ReadFile(filename) + } + + terraformArgs := &TerraformArgs{ + TerraformVars: make(map[string]string), + } + + // When addDependencyVariables is called + err := printer.addDependencyVariables("test/path", terraformArgs) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) + + t.Run("NoOpWhenGetTerraformComponentsReturnsEmpty", func(t *testing.T) { + // Given a TerraformEnvPrinter with empty components + mocks := setupTerraformEnvMocks(t) + mockConfigHandler := config.NewMockConfigHandler() + mockConfigHandler.GetConfigRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting config root") + } + printer := NewTerraformEnvPrinter(mocks.Shell, mockConfigHandler) + printer.shims = mocks.Shims + + terraformArgs := &TerraformArgs{ + TerraformVars: make(map[string]string), + } + + // When addDependencyVariables is called + err := printer.addDependencyVariables("test/path", terraformArgs) + + // Then no error should be returned + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + }) +} diff --git a/pkg/runtime/env/windsor_env_test.go b/pkg/runtime/env/windsor_env_test.go index 2ad81ab4e..96cf183f3 100644 --- a/pkg/runtime/env/windsor_env_test.go +++ b/pkg/runtime/env/windsor_env_test.go @@ -15,13 +15,15 @@ import ( // Test Setup // ============================================================================= -func setupWindsorEnvMocks(t *testing.T, opts ...*SetupOptions) *Mocks { +func setupWindsorEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { t.Helper() - if opts == nil { - opts = []*SetupOptions{{}} - } - if opts[0].ConfigStr == "" { - opts[0].ConfigStr = ` + // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) + mocks := setupEnvMocks(t, opts...) + + // Only load default config if ConfigHandler wasn't overridden via opts + // If ConfigHandler was injected via opts, assume test wants to control it + if len(opts) == 0 { + configStr := ` version: v1alpha1 contexts: mock-context: @@ -29,11 +31,11 @@ contexts: TEST_VAR: test_value SECRET_VAR: "{{secret_name}}" ` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mocks.ConfigHandler.SetContext("mock-context") } - if opts[0].Context == "" { - opts[0].Context = "mock-context" - } - mocks := setupMocks(t, opts[0]) // Get the temp dir that was set up in setupMocks projectRoot, err := mocks.Shell.GetProjectRoot() @@ -82,7 +84,7 @@ contexts: // TestWindsorEnv_GetEnvVars tests the GetEnvVars method of the WindsorEnvPrinter func TestWindsorEnv_GetEnvVars(t *testing.T) { - setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupWindsorEnvMocks(t) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) @@ -91,9 +93,9 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { } t.Run("Success", func(t *testing.T) { + // Given a properly initialized WindsorEnvPrinter printer, _ := setup(t) - // Given a properly initialized WindsorEnvPrinter // When GetEnvVars is called envVars, err := printer.GetEnvVars() @@ -136,8 +138,8 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { return "mock-string" } - mocks := setupWindsorEnvMocks(t, &SetupOptions{ - ConfigHandler: mockConfigHandler, + mocks := setupWindsorEnvMocks(t, func(m *EnvTestMocks) { + m.ConfigHandler = mockConfigHandler }) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) printer.shims = mocks.Shims @@ -159,9 +161,9 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { }) t.Run("ProjectRootError", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing project root retrieval printer, mocks := setup(t) - // Given a WindsorEnvPrinter with failing project root retrieval mocks.Shell.GetProjectRootFunc = func() (string, error) { return "", fmt.Errorf("mock project root error") } @@ -179,14 +181,13 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { }) t.Run("SecretVarWithCacheEnabled", func(t *testing.T) { + // Given a WindsorEnvPrinter with cache enabled printer, mocks := setup(t) - // Given a WindsorEnvPrinter with cache enabled t.Setenv("NO_CACHE", "0") t.Setenv("SECRET_VAR", "cached_value") t.Setenv("WINDSOR_MANAGED_ENV", "") - // And mock secrets provider mockSecretsProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockSecretsProvider.ParseSecretsFunc = func(input string) (string, error) { if strings.Contains(input, "{{secret_name}}") { @@ -196,9 +197,6 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { } _ = mockSecretsProvider - // Re-create printer with updated secrets provider - - // And config with secret variable if err := mocks.ConfigHandler.LoadConfigString(` version: v1alpha1 contexts: @@ -228,13 +226,12 @@ contexts: }) t.Run("SecretVarWithCacheDisabled", func(t *testing.T) { + // Given a WindsorEnvPrinter with cache disabled printer, mocks := setup(t) - // Given a WindsorEnvPrinter with cache disabled t.Setenv("NO_CACHE", "1") t.Setenv("SECRET_VAR", "cached_value") - // And config with secret variable if err := mocks.ConfigHandler.LoadConfigString(` version: v1alpha1 contexts: @@ -258,13 +255,12 @@ contexts: }) t.Run("SecretVarWithErrorInExistingValue", func(t *testing.T) { + // Given a WindsorEnvPrinter with error in existing value printer, mocks := setup(t) - // Given a WindsorEnvPrinter with error in existing value t.Setenv("NO_CACHE", "0") t.Setenv("SECRET_VAR", "secret error") - // And config with secret variable if err := mocks.ConfigHandler.LoadConfigString(` version: v1alpha1 contexts: @@ -288,14 +284,13 @@ contexts: }) t.Run("SecretVarWithManagedEnvExists", func(t *testing.T) { + // Given a WindsorEnvPrinter with managed env exists printer, mocks := setup(t) - // Given a WindsorEnvPrinter with managed env exists t.Setenv("NO_CACHE", "0") t.Setenv("SECRET_VAR", "cached_value") t.Setenv("WINDSOR_MANAGED_ENV", "SECRET_VAR") - // And config with secret variable if err := mocks.ConfigHandler.LoadConfigString(` version: v1alpha1 contexts: @@ -320,9 +315,9 @@ contexts: }) t.Run("ExistingSessionToken", func(t *testing.T) { + // Given a WindsorEnvPrinter with token regeneration printer, mocks := setup(t) - // Given a WindsorEnvPrinter with token regeneration var callCount int mocks.Shell.GetSessionTokenFunc = func() (string, error) { callCount++ @@ -358,9 +353,9 @@ contexts: }) t.Run("SessionTokenError", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing session token generation printer, mocks := setup(t) - // Given a WindsorEnvPrinter with failing session token generation mocks.Shell.GetSessionTokenFunc = func() (string, error) { return "", fmt.Errorf("mock session token error") } @@ -378,26 +373,26 @@ contexts: }) t.Run("NoEnvironmentVarsInConfig", func(t *testing.T) { - // Setup with empty environment - mocks := setupWindsorEnvMocks(t, &SetupOptions{ - ConfigStr: ` + // Given a WindsorEnvPrinter with empty environment configuration + mocks := setupWindsorEnvMocks(t) + configStr := ` version: v1alpha1 contexts: mock-context: environment: {} -`, - Context: "mock-context", - }) +` + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } + mocks.ConfigHandler.SetContext("mock-context") printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) printer.shims = mocks.Shims - // Given a WindsorEnvPrinter with empty environment configuration projectRoot, err := mocks.Shell.GetProjectRoot() if err != nil { t.Fatalf("Failed to get project root: %v", err) } - // And no managed environment variables or aliases printer.managedEnv = []string{} printer.managedAlias = []string{} @@ -429,17 +424,15 @@ contexts: }) t.Run("EnvironmentTokenWithoutSignalFile", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and no signal file printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And no signal file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { return nil, os.ErrNotExist } - // And GetSessionToken configured to handle environment token mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { return envToken, nil @@ -460,17 +453,15 @@ contexts: }) t.Run("EnvironmentTokenWithStatError", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and stat error printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And stat returns a non-ErrNotExist error mocks.Shims.Stat = func(name string) (os.FileInfo, error) { return nil, fmt.Errorf("mock stat error") } - // And GetSessionToken configured to handle environment token mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { return envToken, nil @@ -491,38 +482,33 @@ contexts: }) t.Run("EnvironmentTokenWithSignalFile", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and signal file printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And signal file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { if strings.Contains(name, ".session.envtoken") { - return nil, nil // File exists + return nil, nil } return nil, os.ErrNotExist } - // And RemoveAll succeeds mocks.Shims.RemoveAll = func(path string) error { return nil } - // And CryptoRandRead returns predictable output mocks.Shims.CryptoRandRead = func(b []byte) (n int, err error) { for i := range b { - b[i] = byte(i % 62) // Will map to characters in charset + b[i] = byte(i % 62) } return len(b), nil } - // And GetSessionToken configured to handle environment token and signal file mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { tokenFilePath := filepath.Join("/mock/project/root", ".windsor", ".session."+envToken) if _, err := mocks.Shims.Stat(tokenFilePath); err == nil { - // Signal file exists, generate new token return "abcdefg", nil } return envToken, nil @@ -546,38 +532,33 @@ contexts: }) t.Run("SignalFileRemovalError", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and signal file removal error printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And signal file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { if strings.Contains(name, ".session.envtoken") { - return nil, nil // File exists + return nil, nil } return nil, os.ErrNotExist } - // And RemoveAll fails mocks.Shims.RemoveAll = func(path string) error { return fmt.Errorf("mock error removing signal file") } - // And CryptoRandRead returns predictable output mocks.Shims.CryptoRandRead = func(b []byte) (n int, err error) { for i := range b { - b[i] = byte(i % 62) // Will map to characters in charset + b[i] = byte(i % 62) } return len(b), nil } - // And GetSessionToken configured to handle environment token and signal file mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { tokenFilePath := filepath.Join("/mock/project/root", ".windsor", ".session."+envToken) if _, err := mocks.Shims.Stat(tokenFilePath); err == nil { - // Signal file exists, generate new token return "abcdefg", nil } return envToken, nil @@ -601,12 +582,11 @@ contexts: }) t.Run("ProjectRootErrorDuringEnvTokenSignalFileCheck", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and project root error printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And GetSessionToken returns an error during signal file check mocks.Shell.GetSessionTokenFunc = func() (string, error) { if _, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { return "", fmt.Errorf("error getting project root: mock error getting project root during token check") @@ -629,25 +609,22 @@ contexts: }) t.Run("RandomGenerationError", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and random generation error printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And signal file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { if strings.Contains(name, ".session.envtoken") { - return nil, nil // File exists + return nil, nil } return nil, os.ErrNotExist } - // And GetSessionToken returns an error during token regeneration mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { tokenFilePath := filepath.Join("/mock/project/root", ".windsor", ".session."+envToken) if _, err := mocks.Shims.Stat(tokenFilePath); err == nil { - // Signal file exists, mock error during regeneration return "", fmt.Errorf("mock random generation error during token regeneration") } return envToken, nil @@ -668,9 +645,9 @@ contexts: }) t.Run("GetProjectRootError", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing project root lookup printer, mocks := setup(t) - // Given a WindsorEnvPrinter with failing project root lookup mocks.Shell.GetProjectRootFunc = func() (string, error) { return "", fmt.Errorf("mock shell error") } @@ -686,12 +663,11 @@ contexts: }) t.Run("ProjectRootErrorDuringTokenCheck", func(t *testing.T) { + // Given a WindsorEnvPrinter with environment token and project root error during token check printer, mocks := setup(t) - // Given a WindsorEnvPrinter with environment token t.Setenv("WINDSOR_SESSION_TOKEN", "envtoken") - // And GetSessionToken returns an error during project root check mocks.Shell.GetSessionTokenFunc = func() (string, error) { if _, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { return "", fmt.Errorf("error getting project root: mock shell error during token check") @@ -714,18 +690,17 @@ contexts: }) t.Run("ComprehensiveEnvironmentTokenTest", func(t *testing.T) { + // Given a WindsorEnvPrinter with mock file system functions printer, mocks := setup(t) - // Given a WindsorEnvPrinter with mock file system functions mocks.Shims.Stat = func(name string) (os.FileInfo, error) { if strings.Contains(name, ".session.testtoken") { - return nil, nil // Session file exists + return nil, nil } return nil, os.ErrNotExist } // Phase 1: No environment token present - // Given no environment token mocks.Shims.LookupEnv = func(key string) (string, bool) { return "", false } @@ -738,7 +713,6 @@ contexts: firstToken := envVars["WINDSOR_SESSION_TOKEN"] // Phase 2: Set environment token - // Given environment token is set mocks.Shims.LookupEnv = func(key string) (string, bool) { if key == "WINDSOR_SESSION_TOKEN" { return "testtoken", true @@ -746,13 +720,11 @@ contexts: return "", false } - // And GetSessionToken configured to handle testtoken mocks.Shell.GetSessionTokenFunc = func() (string, error) { if envToken, exists := mocks.Shims.LookupEnv("WINDSOR_SESSION_TOKEN"); exists { - // Our testtoken has a signal file tokenFilePath := filepath.Join("/mock/project/root", ".windsor", ".session."+envToken) if _, err := mocks.Shims.Stat(tokenFilePath); err == nil { - return "newtoken", nil // Return a different token to show regeneration + return "newtoken", nil } return envToken, nil } @@ -796,7 +768,7 @@ func TestWindsorEnv_PostEnvHook(t *testing.T) { // TestWindsorEnv_Initialize tests the Initialize method of the WindsorEnvPrinter func TestWindsorEnv_Initialize(t *testing.T) { - setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupWindsorEnvMocks(t) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) @@ -805,6 +777,7 @@ func TestWindsorEnv_Initialize(t *testing.T) { } t.Run("Success", func(t *testing.T) { + // Given a WindsorEnvPrinter printer, _ := setup(t) // Then printer should be created @@ -812,7 +785,6 @@ func TestWindsorEnv_Initialize(t *testing.T) { t.Fatal("Expected printer to be created") } - // And secretsProviders should be empty (passed as empty slice) if len(printer.secretsProviders) != 0 { t.Errorf("Expected 0 secrets providers, got %d", len(printer.secretsProviders)) } @@ -832,7 +804,7 @@ func TestWindsorEnv_Initialize(t *testing.T) { // TestWindsorEnv_ParseAndCheckSecrets tests the parseAndCheckSecrets method func TestWindsorEnv_ParseAndCheckSecrets(t *testing.T) { - setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupWindsorEnvMocks(t) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) @@ -841,9 +813,9 @@ func TestWindsorEnv_ParseAndCheckSecrets(t *testing.T) { } t.Run("Success", func(t *testing.T) { + // Given a WindsorEnvPrinter with a secrets provider that successfully parses secrets printer, mocks := setup(t) - // Given a mock secrets provider that successfully parses secrets mockSecretsProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockSecretsProvider.ParseSecretsFunc = func(input string) (string, error) { if input == "value with ${{ secrets.mySecret }}" { @@ -863,9 +835,9 @@ func TestWindsorEnv_ParseAndCheckSecrets(t *testing.T) { }) t.Run("SecretsProviderError", func(t *testing.T) { + // Given a WindsorEnvPrinter with a secrets provider that fails to parse secrets printer, mocks := setup(t) - // Given a mock secrets provider that fails to parse secrets mockSecretsProvider := secrets.NewMockSecretsProvider(mocks.Shell) mockSecretsProvider.ParseSecretsFunc = func(input string) (string, error) { return "", fmt.Errorf("error parsing secrets") @@ -885,9 +857,9 @@ func TestWindsorEnv_ParseAndCheckSecrets(t *testing.T) { }) t.Run("NoSecretsProviders", func(t *testing.T) { + // Given a WindsorEnvPrinter with no secrets providers printer, _ := setup(t) - // Given a WindsorEnvPrinter with no secrets providers printer.secretsProviders = []secrets.SecretsProvider{} // When parseAndCheckSecrets is called @@ -945,7 +917,7 @@ func TestWindsorEnv_ParseAndCheckSecrets(t *testing.T) { } func TestWindsorEnv_shouldUseCache(t *testing.T) { - setup := func(t *testing.T) (*WindsorEnvPrinter, *Mocks) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { t.Helper() mocks := setupWindsorEnvMocks(t) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) @@ -1029,3 +1001,284 @@ func TestWindsorEnv_shouldUseCache(t *testing.T) { } }) } + +// TestWindsorEnv_getBuildID tests the getBuildID method of the WindsorEnvPrinter +func TestWindsorEnv_getBuildID(t *testing.T) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupWindsorEnvMocks(t) + printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorWhenGetProjectRootFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing GetProjectRoot + printer, mocks := setup(t) + + mocks.Shell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting project root") + } + + // When getBuildID is called + _, err := printer.getBuildID() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get project root") { + t.Errorf("Expected error about getting project root, got %v", err) + } + }) + + t.Run("ErrorWhenReadFileFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with existing build ID file that fails to read + printer, mocks := setup(t) + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, ".build-id") { + return nil, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, ".build-id") { + return nil, fmt.Errorf("mock error reading build ID file") + } + return os.ReadFile(filename) + } + + // When getBuildID is called + _, err := printer.getBuildID() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to read build ID file") { + t.Errorf("Expected error about reading build ID file, got %v", err) + } + }) + + t.Run("ErrorWhenWriteBuildIDToFileFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with no existing build ID file + printer, mocks := setup(t) + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + return nil, os.ErrNotExist + } + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return fmt.Errorf("mock error creating directory") + } + + // When getBuildID is called + _, err := printer.getBuildID() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to set build ID") { + t.Errorf("Expected error about setting build ID, got %v", err) + } + }) +} + +// TestWindsorEnv_writeBuildIDToFile tests the writeBuildIDToFile method of the WindsorEnvPrinter +func TestWindsorEnv_writeBuildIDToFile(t *testing.T) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupWindsorEnvMocks(t) + printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorWhenGetProjectRootFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing GetProjectRoot + printer, mocks := setup(t) + + mocks.Shell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting project root") + } + + // When writeBuildIDToFile is called + err := printer.writeBuildIDToFile("240101.123.1") + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get project root") { + t.Errorf("Expected error about getting project root, got %v", err) + } + }) + + t.Run("ErrorWhenMkdirAllFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing MkdirAll + printer, mocks := setup(t) + + mocks.Shims.MkdirAll = func(path string, perm os.FileMode) error { + return fmt.Errorf("mock error creating directory") + } + + // When writeBuildIDToFile is called + err := printer.writeBuildIDToFile("240101.123.1") + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to create build ID directory") { + t.Errorf("Expected error about creating directory, got %v", err) + } + }) +} + +// TestWindsorEnv_generateBuildID tests the generateBuildID method of the WindsorEnvPrinter +func TestWindsorEnv_generateBuildID(t *testing.T) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupWindsorEnvMocks(t) + printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorWhenGetProjectRootFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with failing GetProjectRoot + printer, mocks := setup(t) + + mocks.Shell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("mock error getting project root") + } + + // When generateBuildID is called + _, err := printer.generateBuildID() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to get project root") { + t.Errorf("Expected error about getting project root, got %v", err) + } + }) + + t.Run("ErrorWhenReadFileFails", func(t *testing.T) { + // Given a WindsorEnvPrinter with existing build ID file that fails to read + printer, mocks := setup(t) + + mocks.Shims.Stat = func(name string) (os.FileInfo, error) { + if strings.Contains(name, ".build-id") { + return nil, nil + } + return nil, os.ErrNotExist + } + + mocks.Shims.ReadFile = func(filename string) ([]byte, error) { + if strings.Contains(filename, ".build-id") { + return nil, fmt.Errorf("mock error reading build ID file") + } + return os.ReadFile(filename) + } + + // When generateBuildID is called + _, err := printer.generateBuildID() + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "failed to read build ID file") { + t.Errorf("Expected error about reading build ID file, got %v", err) + } + }) + +} + +// TestWindsorEnv_incrementBuildID tests the incrementBuildID method of the WindsorEnvPrinter +func TestWindsorEnv_incrementBuildID(t *testing.T) { + setup := func(t *testing.T) (*WindsorEnvPrinter, *EnvTestMocks) { + t.Helper() + mocks := setupWindsorEnvMocks(t) + printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) + printer.shims = mocks.Shims + return printer, mocks + } + + t.Run("ErrorOnInvalidFormat", func(t *testing.T) { + // Given a WindsorEnvPrinter with invalid build ID format + printer, _ := setup(t) + + // When incrementBuildID is called with invalid format + _, err := printer.incrementBuildID("invalid", "240101") + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid build ID format") { + t.Errorf("Expected error about invalid format, got %v", err) + } + }) + + t.Run("ErrorOnInvalidCounter", func(t *testing.T) { + // Given a WindsorEnvPrinter with invalid counter component + printer, _ := setup(t) + + // When incrementBuildID is called with invalid counter + _, err := printer.incrementBuildID("240101.123.invalid", "240101") + + // Then an error should be returned + if err == nil { + t.Fatal("Expected error, got nil") + } + if !strings.Contains(err.Error(), "invalid counter component") { + t.Errorf("Expected error about invalid counter, got %v", err) + } + }) + + t.Run("IncrementsCounterForSameDate", func(t *testing.T) { + // Given a WindsorEnvPrinter with same date + printer, _ := setup(t) + + // When incrementBuildID is called with same date + result, err := printer.incrementBuildID("240101.123.5", "240101") + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And counter should be incremented + expected := "240101.123.6" + if result != expected { + t.Errorf("Expected %s, got %s", expected, result) + } + }) + + t.Run("ResetsCounterForDifferentDate", func(t *testing.T) { + // Given a WindsorEnvPrinter with different date + printer, _ := setup(t) + + // When incrementBuildID is called with different date + result, err := printer.incrementBuildID("240101.123.5", "240102") + + // Then no error should be returned + if err != nil { + t.Fatalf("Unexpected error: %v", err) + } + + // And counter should be reset to 1 + if !strings.HasSuffix(result, ".1") { + t.Errorf("Expected counter to be reset to 1, got %s", result) + } + if !strings.HasPrefix(result, "240102.") { + t.Errorf("Expected date to be 240102, got %s", result) + } + }) +} From d96572837e9b74cb2fea6981b0b6c6712a6a815d Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Sun, 16 Nov 2025 21:54:25 -0500 Subject: [PATCH 03/14] Make shell tests conformal Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/shell/secure_shell_test.go | 14 +-- pkg/runtime/shell/shell_test.go | 126 ++++++++++++------------ pkg/runtime/shell/unix_shell_test.go | 12 +-- pkg/runtime/shell/windows_shell_test.go | 12 +-- 4 files changed, 80 insertions(+), 84 deletions(-) diff --git a/pkg/runtime/shell/secure_shell_test.go b/pkg/runtime/shell/secure_shell_test.go index f9f22fce0..24f773e88 100644 --- a/pkg/runtime/shell/secure_shell_test.go +++ b/pkg/runtime/shell/secure_shell_test.go @@ -20,18 +20,18 @@ import ( // ============================================================================= type SecureMocks struct { - *Mocks + *ShellTestMocks Client *ssh.MockClient ClientConn *ssh.MockClientConn Session *ssh.MockSession } // setupSecureShellMocks creates a new set of mocks for testing SecureShell -func setupSecureShellMocks(t *testing.T) *SecureMocks { +func setupSecureShellMocks(t *testing.T, opts ...func(*ShellTestMocks)) *SecureMocks { t.Helper() // Set up base mocks first - baseMocks := setupMocks(t) + baseMocks := setupShellMocks(t, opts...) // Create default mock components mockSession := &ssh.MockSession{ @@ -59,10 +59,10 @@ func setupSecureShellMocks(t *testing.T) *SecureMocks { } return &SecureMocks{ - Mocks: baseMocks, - Client: mockClient, - ClientConn: mockClientConn, - Session: mockSession, + ShellTestMocks: baseMocks, + Client: mockClient, + ClientConn: mockClientConn, + Session: mockSession, } } diff --git a/pkg/runtime/shell/shell_test.go b/pkg/runtime/shell/shell_test.go index 2fd5fc4f4..cc3c4951c 100644 --- a/pkg/runtime/shell/shell_test.go +++ b/pkg/runtime/shell/shell_test.go @@ -11,7 +11,6 @@ import ( "strings" "testing" "text/template" - ) // The ShellTest is a test suite for the Shell interface and its implementations. @@ -24,34 +23,13 @@ import ( // Test Setup // ============================================================================= -// Mock functions for testing -var ( - Command func(name string, args ...string) *exec.Cmd - CmdStart func(cmd *exec.Cmd) error - CmdWait func(cmd *exec.Cmd) error - CmdRun func(cmd *exec.Cmd) error - StdoutPipe func(cmd *exec.Cmd) (io.ReadCloser, error) - StderrPipe func(cmd *exec.Cmd) (io.ReadCloser, error) - Getwd func() (string, error) - Stat func(name string) (os.FileInfo, error) -) - -type Mocks struct { +type ShellTestMocks struct { Shims *Shims TmpDir string } -type SetupOptions struct { -} - -// setupMocks creates a new set of mocks for testing -func setupMocks(t *testing.T) *Mocks { - t.Helper() - - // Create temp dir - tmpDir := t.TempDir() - - // Create shims with mock implementations +// setupDefaultShims creates a new Shims instance with default implementations +func setupDefaultShims(tmpDir string) *Shims { shims := NewShims() // Mock command execution with proper cleanup @@ -90,7 +68,7 @@ func setupMocks(t *testing.T) *Mocks { r, w := io.Pipe() go func() { if _, err := w.Write([]byte("test output\n")); err != nil { - t.Errorf("Failed to write to stdout pipe: %v", err) + // Ignore error in goroutine } w.Close() }() @@ -101,7 +79,7 @@ func setupMocks(t *testing.T) *Mocks { r, w := io.Pipe() go func() { if _, err := w.Write([]byte("error\n")); err != nil { - t.Errorf("Failed to write to stderr pipe: %v", err) + // Ignore error in goroutine } w.Close() }() @@ -110,7 +88,7 @@ func setupMocks(t *testing.T) *Mocks { // Mock file operations shims.Getwd = func() (string, error) { - return "/test/dir", nil + return tmpDir, nil } shims.Stat = func(name string) (os.FileInfo, error) { @@ -158,7 +136,7 @@ func setupMocks(t *testing.T) *Mocks { } shims.UserHomeDir = func() (string, error) { - return "/home/test", nil + return tmpDir, nil } // Mock random operations with proper cleanup @@ -197,7 +175,7 @@ func setupMocks(t *testing.T) *Mocks { // Mock filepath operations shims.Glob = func(pattern string) ([]string, error) { - return []string{"/test/dir/test"}, nil + return []string{filepath.Join(tmpDir, "test")}, nil } shims.Join = func(elem ...string) string { @@ -208,10 +186,28 @@ func setupMocks(t *testing.T) *Mocks { return scanner.Text() } - return &Mocks{ - Shims: shims, + return shims +} + +// setupShellMocks creates a new set of mocks for testing +func setupShellMocks(t *testing.T, opts ...func(*ShellTestMocks)) *ShellTestMocks { + t.Helper() + + // Create temp dir + tmpDir := t.TempDir() + + // Create initial mocks with defaults + mocks := &ShellTestMocks{ + Shims: setupDefaultShims(tmpDir), TmpDir: tmpDir, } + + // Apply any dependency injection overrides BEFORE using mocks + for _, opt := range opts { + opt(mocks) + } + + return mocks } // ============================================================================= @@ -263,9 +259,9 @@ func TestShell_SetVerbosity(t *testing.T) { // ============================================================================= func TestShell_GetProjectRoot(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -382,9 +378,9 @@ func TestShell_GetProjectRoot(t *testing.T) { } func TestShell_Exec(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -488,9 +484,9 @@ func TestShell_Exec(t *testing.T) { } func TestShell_ExecSudo(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -719,9 +715,9 @@ func TestShell_ExecSudo(t *testing.T) { } func TestShell_ExecSilent(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -898,9 +894,9 @@ func TestShell_ExecSilent(t *testing.T) { } func TestShell_GetSessionToken(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -970,9 +966,9 @@ func TestShell_GetSessionToken(t *testing.T) { } func TestShell_CheckResetFlags(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -1103,9 +1099,9 @@ func TestShell_CheckResetFlags(t *testing.T) { } func TestShell_GenerateRandomString(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -1130,9 +1126,9 @@ func TestShell_GenerateRandomString(t *testing.T) { } func TestShell_InstallHook(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -1367,9 +1363,9 @@ func captureStdout(t *testing.T, f func()) string { } func TestShell_AddCurrentDirToTrustedFile(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -1619,9 +1615,9 @@ func TestShell_AddCurrentDirToTrustedFile(t *testing.T) { } func TestShell_CheckTrustedDirectory(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -1810,9 +1806,9 @@ func TestShell_CheckTrustedDirectory(t *testing.T) { } func TestShell_WriteResetToken(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -2007,9 +2003,9 @@ func TestShell_WriteResetToken(t *testing.T) { } func TestShell_Reset(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -2235,9 +2231,9 @@ func TestShell_Reset(t *testing.T) { } func TestShell_ResetSessionToken(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -2302,9 +2298,9 @@ func TestShell_ResetSessionToken(t *testing.T) { } func TestShell_ExecProgress(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -2853,9 +2849,9 @@ func TestShell_ExecProgress(t *testing.T) { func TestShell_RegisterSecret(t *testing.T) { // setup creates a new shell with mocked dependencies for testing - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() return shell, mocks } @@ -2931,9 +2927,9 @@ func TestShell_RegisterSecret(t *testing.T) { func TestShell_scrubString(t *testing.T) { // setup creates a new shell with mocked dependencies for testing - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() return shell, mocks } @@ -3185,7 +3181,7 @@ func TestShell_scrubString(t *testing.T) { func TestScrubbingWriter(t *testing.T) { setup := func(t *testing.T) (*DefaultShell, *bytes.Buffer) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims diff --git a/pkg/runtime/shell/unix_shell_test.go b/pkg/runtime/shell/unix_shell_test.go index 1c578b1af..f46315e95 100644 --- a/pkg/runtime/shell/unix_shell_test.go +++ b/pkg/runtime/shell/unix_shell_test.go @@ -20,9 +20,9 @@ import ( // TestDefaultShell_GetProjectRoot tests the GetProjectRoot method on Unix systems func TestDefaultShell_GetProjectRoot(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -76,9 +76,9 @@ func TestDefaultShell_GetProjectRoot(t *testing.T) { // TestDefaultShell_UnsetEnvs tests the UnsetEnvs method on Unix systems func TestDefaultShell_UnsetEnvs(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -119,9 +119,9 @@ func TestDefaultShell_UnsetEnvs(t *testing.T) { // TestDefaultShell_UnsetAlias tests the UnsetAlias method on Unix systems func TestDefaultShell_UnsetAlias(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks diff --git a/pkg/runtime/shell/windows_shell_test.go b/pkg/runtime/shell/windows_shell_test.go index c94af9298..62c914601 100644 --- a/pkg/runtime/shell/windows_shell_test.go +++ b/pkg/runtime/shell/windows_shell_test.go @@ -20,9 +20,9 @@ import ( // TestDefaultShell_GetProjectRoot tests the GetProjectRoot method on Windows systems func TestDefaultShell_GetProjectRoot(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -74,9 +74,9 @@ func TestDefaultShell_GetProjectRoot(t *testing.T) { // TestDefaultShell_UnsetEnvs tests the UnsetEnvs method on Windows systems func TestDefaultShell_UnsetEnvs(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks @@ -117,9 +117,9 @@ func TestDefaultShell_UnsetEnvs(t *testing.T) { // TestDefaultShell_UnsetAlias tests the UnsetAlias method on Windows systems func TestDefaultShell_UnsetAlias(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { t.Helper() - mocks := setupMocks(t) + mocks := setupShellMocks(t) shell := NewDefaultShell() shell.shims = mocks.Shims return shell, mocks From eb68105529e23c870a5d2fb7d893fc12abebcd60 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:45:48 -0500 Subject: [PATCH 04/14] Make config handler tests conformal Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../config/config_handler_public_test.go | 187 +++++++++--------- 1 file changed, 96 insertions(+), 91 deletions(-) diff --git a/pkg/runtime/config/config_handler_public_test.go b/pkg/runtime/config/config_handler_public_test.go index a193a6cd5..365853839 100644 --- a/pkg/runtime/config/config_handler_public_test.go +++ b/pkg/runtime/config/config_handler_public_test.go @@ -13,16 +13,13 @@ import ( // Test Setup // ============================================================================= -type Mocks struct { +type ConfigTestMocks struct { Shell *shell.MockShell Shims *Shims } -type SetupOptions struct { - ConfigStr string -} - -func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { +// setupConfigMocks creates a new set of mocks for testing +func setupConfigMocks(t *testing.T, opts ...func(*ConfigTestMocks)) *ConfigTestMocks { t.Helper() tmpDir := t.TempDir() @@ -34,15 +31,23 @@ func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { return tmpDir, nil } + // Create initial mocks with defaults + mocks := &ConfigTestMocks{ + Shell: mockShell, + Shims: NewShims(), + } + + // Apply any dependency injection overrides BEFORE using mocks + for _, opt := range opts { + opt(mocks) + } + t.Cleanup(func() { os.Unsetenv("WINDSOR_PROJECT_ROOT") os.Unsetenv("WINDSOR_CONTEXT") }) - return &Mocks{ - Shell: mockShell, - Shims: NewShims(), - } + return mocks } // ============================================================================= @@ -51,7 +56,7 @@ func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { func TestConfigHandler_Initialize(t *testing.T) { t.Run("Success", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -66,7 +71,7 @@ func TestConfigHandler_Initialize(t *testing.T) { }) t.Run("InitializesDataMap", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -85,7 +90,7 @@ func TestConfigHandler_Initialize(t *testing.T) { t.Run("CreatesHandlerWithShell", func(t *testing.T) { // Given a shell - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -98,7 +103,7 @@ func TestConfigHandler_Initialize(t *testing.T) { func TestConfigHandler_LoadConfig(t *testing.T) { t.Run("LoadsRootConfigContextSection", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -130,7 +135,7 @@ contexts: }) t.Run("LoadsContextSpecificWindsorYaml", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -162,7 +167,7 @@ cluster: }) t.Run("LoadsValuesYaml", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -199,7 +204,7 @@ nested: }) t.Run("MergesAllSourcesWithCorrectPrecedence", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -246,7 +251,7 @@ contexts: }) t.Run("LoadsSchemaWithoutErrors", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) tmpDir, _ := mocks.Shell.GetProjectRoot() @@ -268,7 +273,7 @@ properties: }) t.Run("SetsLoadedFlag", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -292,7 +297,7 @@ properties: }) t.Run("ValidatesValuesYamlAgainstSchema", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -323,7 +328,7 @@ additionalProperties: false }) t.Run("HandlesYmlExtension", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.SetContext("test-context") @@ -348,7 +353,7 @@ additionalProperties: false t.Run("CreatesHandlerWithShell", func(t *testing.T) { // Given a shell - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell).(*configHandler) // When loading config with shell @@ -554,7 +559,7 @@ func TestConfigHandler_LoadConfigString(t *testing.T) { os.Setenv("WINDSOR_CONTEXT", "test-context") defer os.Unsetenv("WINDSOR_CONTEXT") - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) yaml := `version: v1alpha1 @@ -585,7 +590,7 @@ contexts: }) t.Run("MergesFlatYamlStructure", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) yaml := `provider: generic @@ -610,7 +615,7 @@ custom_key: custom_value }) t.Run("SetsLoadedFlag", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.LoadConfigString("provider: test\n") @@ -653,7 +658,7 @@ custom_key: custom_value func TestConfigHandler_Get(t *testing.T) { t.Run("ReturnsValueFromData", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("simple.key", "test_value") @@ -666,7 +671,7 @@ func TestConfigHandler_Get(t *testing.T) { }) t.Run("ReturnsNilForEmptyPath", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) value := handler.Get("") @@ -677,7 +682,7 @@ func TestConfigHandler_Get(t *testing.T) { }) t.Run("ReturnsNilForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) value := handler.Get("nonexistent.key") @@ -688,7 +693,7 @@ func TestConfigHandler_Get(t *testing.T) { }) t.Run("NavigatesNestedMaps", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("parent.child.grandchild", "nested_value") @@ -701,7 +706,7 @@ func TestConfigHandler_Get(t *testing.T) { }) t.Run("FallsBackToSchemaDefaultsForTopLevelKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -726,7 +731,7 @@ properties: }) t.Run("FallsBackToSchemaDefaultsForNestedKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -757,7 +762,7 @@ properties: func TestConfigHandler_GetString(t *testing.T) { t.Run("ReturnsStringValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("key", "string_value") @@ -770,7 +775,7 @@ func TestConfigHandler_GetString(t *testing.T) { }) t.Run("ReturnsEmptyStringForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetString("missing.key") @@ -781,7 +786,7 @@ func TestConfigHandler_GetString(t *testing.T) { }) t.Run("ReturnsProvidedDefault", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetString("missing.key", "default_value") @@ -792,7 +797,7 @@ func TestConfigHandler_GetString(t *testing.T) { }) t.Run("ConvertsNonStringToString", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("number", 42) @@ -807,7 +812,7 @@ func TestConfigHandler_GetString(t *testing.T) { func TestConfigHandler_GetInt(t *testing.T) { t.Run("ReturnsIntValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", 42) @@ -820,7 +825,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("IgnoresFloat64Values", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", float64(42.7)) @@ -833,7 +838,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ConvertsUint64ToInt", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", uint64(42)) @@ -846,7 +851,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ConvertsStringToInt", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", "42") @@ -859,7 +864,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ReturnsZeroForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetInt("missing.key") @@ -870,7 +875,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ReturnsProvidedDefault", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetInt("missing.key", 99) @@ -881,7 +886,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ConvertsInt64ToInt", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", int64(42)) @@ -894,7 +899,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ConvertsUintToInt", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", uint(42)) @@ -907,7 +912,7 @@ func TestConfigHandler_GetInt(t *testing.T) { }) t.Run("ReturnsZeroForNonNumericValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("count", "not_a_number") @@ -922,7 +927,7 @@ func TestConfigHandler_GetInt(t *testing.T) { func TestConfigHandler_GetBool(t *testing.T) { t.Run("ReturnsBoolValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("enabled", true) @@ -935,7 +940,7 @@ func TestConfigHandler_GetBool(t *testing.T) { }) t.Run("ReturnsFalseForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetBool("missing.key") @@ -946,7 +951,7 @@ func TestConfigHandler_GetBool(t *testing.T) { }) t.Run("ReturnsProvidedDefault", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetBool("missing.key", true) @@ -959,7 +964,7 @@ func TestConfigHandler_GetBool(t *testing.T) { func TestConfigHandler_GetStringSlice(t *testing.T) { t.Run("ReturnsStringSlice", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("items", []string{"a", "b", "c"}) @@ -975,7 +980,7 @@ func TestConfigHandler_GetStringSlice(t *testing.T) { }) t.Run("ConvertsInterfaceSlice", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("items", []interface{}{"x", "y", "z"}) @@ -991,7 +996,7 @@ func TestConfigHandler_GetStringSlice(t *testing.T) { }) t.Run("ReturnsEmptySliceForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetStringSlice("missing.key") @@ -1002,7 +1007,7 @@ func TestConfigHandler_GetStringSlice(t *testing.T) { }) t.Run("ReturnsProvidedDefault", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) defaultSlice := []string{"default1", "default2"} @@ -1017,7 +1022,7 @@ func TestConfigHandler_GetStringSlice(t *testing.T) { func TestConfigHandler_GetStringMap(t *testing.T) { t.Run("ReturnsStringMap", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("environment", map[string]string{"KEY1": "value1", "KEY2": "value2"}) @@ -1033,7 +1038,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { }) t.Run("ConvertsInterfaceMap", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("environment", map[string]interface{}{"KEY": "value"}) @@ -1046,7 +1051,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { }) t.Run("ReturnsEmptyMapForMissingKey", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.GetStringMap("missing.key") @@ -1057,7 +1062,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { }) t.Run("ReturnsProvidedDefault", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) defaultMap := map[string]string{"default": "value"} @@ -1070,7 +1075,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { }) t.Run("ConvertsInterfaceKeyMap", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("env", map[interface{}]interface{}{"KEY": "value"}) @@ -1083,7 +1088,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { }) t.Run("ConvertsNonStringValuesToString", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("env", map[string]interface{}{"NUM": 42, "BOOL": true}) @@ -1101,7 +1106,7 @@ func TestConfigHandler_GetStringMap(t *testing.T) { func TestConfigHandler_Set(t *testing.T) { t.Run("SetsSimpleValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.Set("key", "value") @@ -1117,7 +1122,7 @@ func TestConfigHandler_Set(t *testing.T) { }) t.Run("SetsNestedValue", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.Set("parent.child.key", "nested_value") @@ -1133,7 +1138,7 @@ func TestConfigHandler_Set(t *testing.T) { }) t.Run("CreatesIntermediateMaps", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.Set("a.b.c.d", "deep_value") @@ -1149,7 +1154,7 @@ func TestConfigHandler_Set(t *testing.T) { }) t.Run("ValidatesDynamicFieldsAgainstSchema", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1174,7 +1179,7 @@ additionalProperties: false }) t.Run("DoesNotValidateStaticFields", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1196,7 +1201,7 @@ additionalProperties: false }) t.Run("ReturnsErrorForEmptyPath", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.Set("", "value") @@ -1207,7 +1212,7 @@ additionalProperties: false }) t.Run("ReturnsErrorForInvalidPath", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.Set("invalid..path", "value") @@ -1218,7 +1223,7 @@ additionalProperties: false }) t.Run("ConvertsStringBasedOnSchemaType", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1261,7 +1266,7 @@ properties: func TestConfigHandler_SaveConfig(t *testing.T) { t.Run("CreatesRootWindsorYaml", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1287,7 +1292,7 @@ func TestConfigHandler_SaveConfig(t *testing.T) { }) t.Run("SeparatesStaticAndDynamicFields", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1336,7 +1341,7 @@ func TestConfigHandler_SaveConfig(t *testing.T) { }) t.Run("ExcludesFieldsWithYamlDashTag", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1368,7 +1373,7 @@ func TestConfigHandler_SaveConfig(t *testing.T) { }) t.Run("DoesNotSaveSchemaDefaults", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1406,7 +1411,7 @@ properties: }) t.Run("SavesOnlyUserSetDynamicValues", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1489,7 +1494,7 @@ properties: t.Run("CreatesHandlerWithShell", func(t *testing.T) { // Given a shell - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell).(*configHandler) // When attempting to save config without context @@ -1610,7 +1615,7 @@ properties: func TestConfigHandler_SetDefault(t *testing.T) { t.Run("MergesDefaultContextIntoData", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) defaultContext := v1alpha1.Context{ @@ -1630,7 +1635,7 @@ func TestConfigHandler_SetDefault(t *testing.T) { }) t.Run("AllowsOverridingDefaults", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) defaultContext := v1alpha1.Context{ @@ -1684,7 +1689,7 @@ func TestConfigHandler_SetDefault(t *testing.T) { func TestConfigHandler_GetConfig(t *testing.T) { t.Run("ConvertsDataMapToContextStruct", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("provider", "test_provider") @@ -1704,7 +1709,7 @@ func TestConfigHandler_GetConfig(t *testing.T) { }) t.Run("ExcludesNodesFieldDueToYamlTag", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("cluster.workers.count", 2) @@ -1767,7 +1772,7 @@ func TestConfigHandler_GetConfig(t *testing.T) { func TestConfigHandler_GetContextValues(t *testing.T) { t.Run("ReturnsDataMergedWithSchemaDefaults", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1801,7 +1806,7 @@ properties: }) t.Run("IncludesServiceCalculatedValues", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("cluster.workers.nodes.worker-1.endpoint", "127.0.0.1:50001") @@ -1832,7 +1837,7 @@ properties: func TestConfigHandler_GetConfigRoot(t *testing.T) { t.Run("ReturnsConfigRoot", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1872,7 +1877,7 @@ func TestConfigHandler_GetConfigRoot(t *testing.T) { func TestConfigHandler_Clean(t *testing.T) { t.Run("RemovesConfigDirectories", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -1918,7 +1923,7 @@ func TestConfigHandler_Clean(t *testing.T) { func TestConfigHandler_GenerateContextID(t *testing.T) { t.Run("GeneratesIDWhenNotSet", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) err := handler.GenerateContextID() @@ -1937,7 +1942,7 @@ func TestConfigHandler_GenerateContextID(t *testing.T) { }) t.Run("DoesNotOverrideExistingID", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.Set("id", "existing_id") @@ -1974,7 +1979,7 @@ func TestConfigHandler_GenerateContextID(t *testing.T) { func TestConfigHandler_LoadSchema(t *testing.T) { t.Run("LoadsSchemaSuccessfully", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -2004,7 +2009,7 @@ properties: }) t.Run("ReturnsErrorForInvalidSchema", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -2054,7 +2059,7 @@ properties: func TestConfigHandler_LoadSchemaFromBytes(t *testing.T) { t.Run("LoadsSchemaFromBytes", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) schemaContent := []byte(`$schema: https://json-schema.org/draft/2020-12/schema @@ -2113,7 +2118,7 @@ properties: func TestConfigHandler_GetContext(t *testing.T) { t.Run("ReturnsContextFromEnvironment", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -2125,7 +2130,7 @@ func TestConfigHandler_GetContext(t *testing.T) { }) t.Run("ReadsContextFromFile", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() os.Unsetenv("WINDSOR_CONTEXT") @@ -2145,7 +2150,7 @@ func TestConfigHandler_GetContext(t *testing.T) { }) t.Run("DefaultsToLocalWhenNoContextSet", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) os.Unsetenv("WINDSOR_CONTEXT") defer os.Setenv("WINDSOR_CONTEXT", "test-context") @@ -2162,7 +2167,7 @@ func TestConfigHandler_GetContext(t *testing.T) { func TestConfigHandler_SetContext(t *testing.T) { t.Run("WritesContextToFile", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -2221,7 +2226,7 @@ func TestConfigHandler_SetContext(t *testing.T) { func TestConfigHandler_IsLoaded(t *testing.T) { t.Run("ReturnsFalseBeforeLoading", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) result := handler.IsLoaded() @@ -2232,7 +2237,7 @@ func TestConfigHandler_IsLoaded(t *testing.T) { }) t.Run("ReturnsTrueAfterLoadingFiles", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) tmpDir, _ := mocks.Shell.GetProjectRoot() handler := NewConfigHandler(mocks.Shell) @@ -2252,7 +2257,7 @@ func TestConfigHandler_IsLoaded(t *testing.T) { }) t.Run("ReturnsTrueAfterLoadConfigString", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) handler.LoadConfigString("provider: test\n") From eafcfc127ea934686f2f3836710c5814700ea59c Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:46:12 -0500 Subject: [PATCH 05/14] More test conformality Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../config/config_handler_public_test.go | 7 +- pkg/runtime/env/aws_env_test.go | 11 +- pkg/runtime/env/azure_env_test.go | 25 ++-- pkg/runtime/env/docker_env_test.go | 118 +++++++++--------- pkg/runtime/env/env_test.go | 17 ++- pkg/runtime/env/kube_env_test.go | 8 +- pkg/runtime/env/talos_env_test.go | 4 +- pkg/runtime/env/terraform_env_test.go | 9 +- pkg/runtime/env/windsor_env_test.go | 15 ++- pkg/runtime/runtime_test.go | 6 +- pkg/runtime/shell/secure_shell_test.go | 4 +- pkg/runtime/shell/shell_test.go | 7 +- 12 files changed, 108 insertions(+), 123 deletions(-) diff --git a/pkg/runtime/config/config_handler_public_test.go b/pkg/runtime/config/config_handler_public_test.go index 365853839..467b5a304 100644 --- a/pkg/runtime/config/config_handler_public_test.go +++ b/pkg/runtime/config/config_handler_public_test.go @@ -19,7 +19,7 @@ type ConfigTestMocks struct { } // setupConfigMocks creates a new set of mocks for testing -func setupConfigMocks(t *testing.T, opts ...func(*ConfigTestMocks)) *ConfigTestMocks { +func setupConfigMocks(t *testing.T) *ConfigTestMocks { t.Helper() tmpDir := t.TempDir() @@ -37,11 +37,6 @@ func setupConfigMocks(t *testing.T, opts ...func(*ConfigTestMocks)) *ConfigTestM Shims: NewShims(), } - // Apply any dependency injection overrides BEFORE using mocks - for _, opt := range opts { - opt(mocks) - } - t.Cleanup(func() { os.Unsetenv("WINDSOR_PROJECT_ROOT") os.Unsetenv("WINDSOR_CONTEXT") diff --git a/pkg/runtime/env/aws_env_test.go b/pkg/runtime/env/aws_env_test.go index 1e8cac668..1401b0e5f 100644 --- a/pkg/runtime/env/aws_env_test.go +++ b/pkg/runtime/env/aws_env_test.go @@ -17,11 +17,10 @@ import ( // Test Setup // ============================================================================= -func setupAwsEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupAwsEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() - // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) - mocks := setupEnvMocks(t, opts...) + mocks := setupEnvMocks(t, overrides...) // If ConfigHandler wasn't overridden, use MockConfigHandler if _, ok := mocks.ConfigHandler.(*config.MockConfigHandler); !ok { @@ -69,9 +68,9 @@ func setupAwsEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { return &v1alpha1.Context{} } - // Only load default config if ConfigHandler wasn't overridden via opts - // If ConfigHandler was injected via opts, assume test wants to control it - if len(opts) == 0 { + // Only load default config if ConfigHandler wasn't overridden + // If ConfigHandler was injected via overrides, assume test wants to control it + if len(overrides) == 0 || overrides[0] == nil || overrides[0].ConfigHandler == nil { defaultConfigStr := ` version: v1alpha1 contexts: diff --git a/pkg/runtime/env/azure_env_test.go b/pkg/runtime/env/azure_env_test.go index 9f64d61cd..12e9abd0d 100644 --- a/pkg/runtime/env/azure_env_test.go +++ b/pkg/runtime/env/azure_env_test.go @@ -14,15 +14,13 @@ import ( // Test Setup // ============================================================================= -func setupAzureEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupAzureEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() - // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) - mocks := setupEnvMocks(t, opts...) + mocks := setupEnvMocks(t, overrides...) - // Only load default config if ConfigHandler wasn't overridden via opts - // If ConfigHandler was injected via opts, assume test wants to control it - // Check by seeing if it's a MockConfigHandler (which would indicate injection) or if opts were provided - if len(opts) == 0 { + // Only load default config if ConfigHandler wasn't overridden + // If ConfigHandler was injected via overrides, assume test wants to control it + if len(overrides) == 0 || overrides[0] == nil || overrides[0].ConfigHandler == nil { configStr := ` version: v1alpha1 contexts: @@ -55,9 +53,9 @@ contexts: // ============================================================================= func TestAzureEnv_GetEnvVars(t *testing.T) { - setup := func(t *testing.T, opts ...func(*EnvTestMocks)) (*AzureEnvPrinter, *EnvTestMocks) { + setup := func(t *testing.T, overrides ...*EnvTestMocks) (*AzureEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupAzureEnvMocks(t, opts...) + mocks := setupAzureEnvMocks(t, overrides...) printer := NewAzureEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims return printer, mocks @@ -108,8 +106,8 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { mockConfigHandler.GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("error retrieving configuration root directory") } - mocks := setupAzureEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = mockConfigHandler + mocks := setupAzureEnvMocks(t, &EnvTestMocks{ + ConfigHandler: mockConfigHandler, }) printer := NewAzureEnvPrinter(mocks.Shell, mocks.ConfigHandler) @@ -127,8 +125,9 @@ func TestAzureEnv_GetEnvVars(t *testing.T) { t.Run("MissingConfiguration", func(t *testing.T) { // Given a printer with no Azure configuration - mocks := setupAzureEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = config.NewConfigHandler(m.Shell) + baseMocks := setupEnvMocks(t) + mocks := setupAzureEnvMocks(t, &EnvTestMocks{ + ConfigHandler: config.NewConfigHandler(baseMocks.Shell), }) configStr := ` version: v1alpha1 diff --git a/pkg/runtime/env/docker_env_test.go b/pkg/runtime/env/docker_env_test.go index 04320402f..8dfd24369 100644 --- a/pkg/runtime/env/docker_env_test.go +++ b/pkg/runtime/env/docker_env_test.go @@ -23,14 +23,13 @@ type DockerEnvPrinterMocks struct { } // setupDockerEnvMocks creates a new set of mocks for Docker environment tests -func setupDockerEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupDockerEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() - // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) - mocks := setupEnvMocks(t, opts...) + mocks := setupEnvMocks(t, overrides...) - // Only load default config if ConfigHandler wasn't overridden via opts - // If ConfigHandler was injected via opts, assume test wants to control it - if len(opts) == 0 { + // Only load default config if ConfigHandler wasn't overridden + // If ConfigHandler was injected via overrides, assume test wants to control it + if len(overrides) == 0 || overrides[0] == nil || overrides[0].ConfigHandler == nil { configStr := ` version: v1alpha1 contexts: @@ -604,9 +603,9 @@ func TestDockerEnvPrinter_GetAlias(t *testing.T) { // TestDockerEnvPrinter_getRegistryURL tests the getRegistryURL method of the DockerEnvPrinter func TestDockerEnvPrinter_getRegistryURL(t *testing.T) { // setup creates a new DockerEnvPrinter with the given configuration - setup := func(t *testing.T, opts ...func(*EnvTestMocks)) (*DockerEnvPrinter, *EnvTestMocks) { + setup := func(t *testing.T, overrides ...*EnvTestMocks) (*DockerEnvPrinter, *EnvTestMocks) { t.Helper() - mocks := setupDockerEnvMocks(t, opts...) + mocks := setupDockerEnvMocks(t, overrides...) printer := NewDockerEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims return printer, mocks @@ -614,8 +613,8 @@ func TestDockerEnvPrinter_getRegistryURL(t *testing.T) { t.Run("ValidRegistryURL", func(t *testing.T) { // Given a DockerEnvPrinter with a valid registry URL in config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -624,10 +623,9 @@ contexts: docker: registry_url: registry.example.com:5000 ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // And the registry URL is set in the context printer.configHandler.Set("docker.registry_url", "registry.example.com:5000") @@ -647,8 +645,8 @@ contexts: t.Run("RegistryURLWithConfig", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL and matching config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -660,10 +658,9 @@ contexts: registry.example.com: hostport: 5000 ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // And the registry URL is set in the context printer.configHandler.Set("docker.registry_url", "registry.example.com") @@ -683,8 +680,8 @@ contexts: t.Run("EmptyRegistryURL", func(t *testing.T) { // Given a DockerEnvPrinter with no registry URL but with registries config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -695,10 +692,9 @@ contexts: mock-registry-url: hostport: 5000 ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // When getting the registry URL url, err := printer.getRegistryURL() @@ -715,8 +711,9 @@ contexts: t.Run("EmptyConfig", func(t *testing.T) { // Given a DockerEnvPrinter with empty registries config - mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = config.NewConfigHandler(m.Shell) + baseMocks := setupEnvMocks(t) + mocks := setupDockerEnvMocks(t, &EnvTestMocks{ + ConfigHandler: config.NewConfigHandler(baseMocks.Shell), }) configStr := ` version: v1alpha1 @@ -748,8 +745,8 @@ contexts: t.Run("RegistryURLWithoutPortNoConfig", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL without port and no matching config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -761,10 +758,9 @@ contexts: other-registry: hostport: 5000 ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // And the registry URL is set in the context printer.configHandler.Set("docker.registry_url", "registry.example.com") @@ -784,8 +780,8 @@ contexts: t.Run("RegistryURLInvalidPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL with invalid port - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -794,10 +790,9 @@ contexts: docker: registry_url: registry.example.com:invalid ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // When getting the registry URL url, err := printer.getRegistryURL() @@ -814,8 +809,8 @@ contexts: t.Run("RegistryURLNoPortNoHostPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL without port and no hostport in config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -826,10 +821,9 @@ contexts: registries: registry.example.com: {} ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // When getting the registry URL url, err := printer.getRegistryURL() @@ -846,8 +840,8 @@ contexts: t.Run("RegistryURLEmptyRegistries", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL and empty registries config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -857,10 +851,9 @@ contexts: registry_url: registry.example.com registries: {} ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // When getting the registry URL url, err := printer.getRegistryURL() @@ -877,8 +870,9 @@ contexts: t.Run("NilDockerConfig", func(t *testing.T) { // Given a DockerEnvPrinter with no Docker config - mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = config.NewConfigHandler(m.Shell) + baseMocks := setupEnvMocks(t) + mocks := setupDockerEnvMocks(t, &EnvTestMocks{ + ConfigHandler: config.NewConfigHandler(baseMocks.Shell), }) configStr := ` version: v1alpha1 @@ -908,8 +902,8 @@ contexts: t.Run("NilRegistriesWithURL", func(t *testing.T) { // Given a DockerEnvPrinter with a registry URL but no registries config - printer, _ := setup(t, func(m *EnvTestMocks) { - configStr := ` + printer, mocks := setup(t) + configStr := ` version: v1alpha1 contexts: test-context: @@ -918,10 +912,9 @@ contexts: docker: registry_url: registry.example.com ` - if err := m.ConfigHandler.LoadConfigString(configStr); err != nil { - t.Fatalf("Failed to load config: %v", err) - } - }) + if err := mocks.ConfigHandler.LoadConfigString(configStr); err != nil { + t.Fatalf("Failed to load config: %v", err) + } // When getting the registry URL url, err := printer.getRegistryURL() @@ -938,8 +931,9 @@ contexts: t.Run("RegistryWithoutHostPort", func(t *testing.T) { // Given a DockerEnvPrinter with a registry without hostport in config - mocks := setupDockerEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = config.NewConfigHandler(m.Shell) + baseMocks := setupEnvMocks(t) + mocks := setupDockerEnvMocks(t, &EnvTestMocks{ + ConfigHandler: config.NewConfigHandler(baseMocks.Shell), }) configStr := ` version: v1alpha1 diff --git a/pkg/runtime/env/env_test.go b/pkg/runtime/env/env_test.go index d85b1d4b8..1bb406252 100644 --- a/pkg/runtime/env/env_test.go +++ b/pkg/runtime/env/env_test.go @@ -35,7 +35,7 @@ func setupDefaultShims(tmpDir string) *Shims { return shims } -func setupEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() // Store original directory and create temp dir @@ -68,9 +68,18 @@ func setupEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { Shims: shims, } - // Apply any dependency injection overrides BEFORE using mocks - for _, opt := range opts { - opt(mocks) + // Apply any selective overrides + if len(overrides) > 0 && overrides[0] != nil { + override := overrides[0] + if override.Shell != nil { + mocks.Shell = override.Shell + } + if override.ConfigHandler != nil { + mocks.ConfigHandler = override.ConfigHandler + } + if override.Shims != nil { + mocks.Shims = override.Shims + } } // Register cleanup to restore original state diff --git a/pkg/runtime/env/kube_env_test.go b/pkg/runtime/env/kube_env_test.go index 2efd0048e..7dc353535 100644 --- a/pkg/runtime/env/kube_env_test.go +++ b/pkg/runtime/env/kube_env_test.go @@ -41,9 +41,9 @@ func (m mockFileInfo) IsDir() bool { return true } func (m mockFileInfo) Sys() any { return nil } // setupKubeEnvMocks creates a base mock setup for Kubernetes environment tests -func setupKubeEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupKubeEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() - mocks := setupEnvMocks(t, opts...) + mocks := setupEnvMocks(t, overrides...) projectRoot, err := mocks.Shell.GetProjectRoot() if err != nil { t.Fatalf("Failed to get project root: %v", err) @@ -201,8 +201,8 @@ func TestKubeEnvPrinter_GetEnvVars(t *testing.T) { } // And a KubeEnvPrinter with the mock ConfigHandler - mocks := setupKubeEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = mockConfigHandler + mocks := setupKubeEnvMocks(t, &EnvTestMocks{ + ConfigHandler: mockConfigHandler, }) printer := NewKubeEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims diff --git a/pkg/runtime/env/talos_env_test.go b/pkg/runtime/env/talos_env_test.go index c743b1c06..fcf5f326e 100644 --- a/pkg/runtime/env/talos_env_test.go +++ b/pkg/runtime/env/talos_env_test.go @@ -27,8 +27,8 @@ func TestTalosEnv_GetEnvVars(t *testing.T) { return "" } - mocks := setupEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = mockConfigHandler + mocks := setupEnvMocks(t, &EnvTestMocks{ + ConfigHandler: mockConfigHandler, }) // Set up GetConfigRoot to return the correct path diff --git a/pkg/runtime/env/terraform_env_test.go b/pkg/runtime/env/terraform_env_test.go index 0cb611727..69f5e64c1 100644 --- a/pkg/runtime/env/terraform_env_test.go +++ b/pkg/runtime/env/terraform_env_test.go @@ -18,9 +18,8 @@ import ( // ============================================================================= // setupTerraformEnvMocks creates and configures mock objects for Terraform environment tests. -func setupTerraformEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { - // Pass the mock config handler to setupEnvMocks - mocks := setupEnvMocks(t, opts...) +func setupTerraformEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { + mocks := setupEnvMocks(t, overrides...) mocks.Shims.Getwd = func() (string, error) { // Use platform-agnostic path @@ -212,8 +211,8 @@ func TestTerraformEnv_GetEnvVars(t *testing.T) { configHandler.GetConfigRootFunc = func() (string, error) { return "", fmt.Errorf("mock error getting config root") } - mocks := setupTerraformEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = configHandler + mocks := setupTerraformEnvMocks(t, &EnvTestMocks{ + ConfigHandler: configHandler, }) printer := NewTerraformEnvPrinter(mocks.Shell, mocks.ConfigHandler) printer.shims = mocks.Shims diff --git a/pkg/runtime/env/windsor_env_test.go b/pkg/runtime/env/windsor_env_test.go index 96cf183f3..ea7080c6e 100644 --- a/pkg/runtime/env/windsor_env_test.go +++ b/pkg/runtime/env/windsor_env_test.go @@ -15,14 +15,13 @@ import ( // Test Setup // ============================================================================= -func setupWindsorEnvMocks(t *testing.T, opts ...func(*EnvTestMocks)) *EnvTestMocks { +func setupWindsorEnvMocks(t *testing.T, overrides ...*EnvTestMocks) *EnvTestMocks { t.Helper() - // Apply opts first to allow DI-style overrides (e.g., injecting a custom ConfigHandler) - mocks := setupEnvMocks(t, opts...) + mocks := setupEnvMocks(t, overrides...) - // Only load default config if ConfigHandler wasn't overridden via opts - // If ConfigHandler was injected via opts, assume test wants to control it - if len(opts) == 0 { + // Only load default config if ConfigHandler wasn't overridden + // If ConfigHandler was injected via overrides, assume test wants to control it + if len(overrides) == 0 || overrides[0] == nil || overrides[0].ConfigHandler == nil { configStr := ` version: v1alpha1 contexts: @@ -138,8 +137,8 @@ func TestWindsorEnv_GetEnvVars(t *testing.T) { return "mock-string" } - mocks := setupWindsorEnvMocks(t, func(m *EnvTestMocks) { - m.ConfigHandler = mockConfigHandler + mocks := setupWindsorEnvMocks(t, &EnvTestMocks{ + ConfigHandler: mockConfigHandler, }) printer := NewWindsorEnvPrinter(mocks.Shell, mocks.ConfigHandler, []secrets.SecretsProvider{}, []EnvPrinter{}) printer.shims = mocks.Shims diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 3985bb1a6..42aafab9c 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -27,7 +27,7 @@ type RuntimeTestMocks struct { } // setupRuntimeMocks creates mock components for testing the Runtime with optional overrides -func setupRuntimeMocks(t *testing.T, opts ...func(*RuntimeTestMocks)) *RuntimeTestMocks { +func setupRuntimeMocks(t *testing.T) *RuntimeTestMocks { t.Helper() configHandler := config.NewMockConfigHandler() @@ -103,10 +103,6 @@ func setupRuntimeMocks(t *testing.T, opts ...func(*RuntimeTestMocks)) *RuntimeTe Runtime: rt, } - for _, opt := range opts { - opt(mocks) - } - return mocks } diff --git a/pkg/runtime/shell/secure_shell_test.go b/pkg/runtime/shell/secure_shell_test.go index 24f773e88..359356921 100644 --- a/pkg/runtime/shell/secure_shell_test.go +++ b/pkg/runtime/shell/secure_shell_test.go @@ -27,11 +27,11 @@ type SecureMocks struct { } // setupSecureShellMocks creates a new set of mocks for testing SecureShell -func setupSecureShellMocks(t *testing.T, opts ...func(*ShellTestMocks)) *SecureMocks { +func setupSecureShellMocks(t *testing.T) *SecureMocks { t.Helper() // Set up base mocks first - baseMocks := setupShellMocks(t, opts...) + baseMocks := setupShellMocks(t) // Create default mock components mockSession := &ssh.MockSession{ diff --git a/pkg/runtime/shell/shell_test.go b/pkg/runtime/shell/shell_test.go index cc3c4951c..3d30916c1 100644 --- a/pkg/runtime/shell/shell_test.go +++ b/pkg/runtime/shell/shell_test.go @@ -190,7 +190,7 @@ func setupDefaultShims(tmpDir string) *Shims { } // setupShellMocks creates a new set of mocks for testing -func setupShellMocks(t *testing.T, opts ...func(*ShellTestMocks)) *ShellTestMocks { +func setupShellMocks(t *testing.T) *ShellTestMocks { t.Helper() // Create temp dir @@ -202,11 +202,6 @@ func setupShellMocks(t *testing.T, opts ...func(*ShellTestMocks)) *ShellTestMock TmpDir: tmpDir, } - // Apply any dependency injection overrides BEFORE using mocks - for _, opt := range opts { - opt(mocks) - } - return mocks } From 4d41dc28046851dd0ce38c683f4a422d8b844ef2 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 08:56:48 -0500 Subject: [PATCH 06/14] Make runtime/secrets conformal Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../secrets/op_cli_secrets_provider_test.go | 20 +++++++++---------- .../secrets/op_sdk_secrets_provider_test.go | 20 +++++++++---------- pkg/runtime/secrets/secrets_provider_test.go | 17 +++++++--------- .../secrets/sops_secrets_provider_test.go | 14 ++++++------- 4 files changed, 34 insertions(+), 37 deletions(-) diff --git a/pkg/runtime/secrets/op_cli_secrets_provider_test.go b/pkg/runtime/secrets/op_cli_secrets_provider_test.go index 96779c961..edad9cf4b 100644 --- a/pkg/runtime/secrets/op_cli_secrets_provider_test.go +++ b/pkg/runtime/secrets/op_cli_secrets_provider_test.go @@ -19,7 +19,7 @@ import ( func TestNewOnePasswordCLISecretsProvider(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -49,7 +49,7 @@ func TestNewOnePasswordCLISecretsProvider(t *testing.T) { func TestOnePasswordCLISecretsProvider_GetSecret(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -87,7 +87,7 @@ func TestOnePasswordCLISecretsProvider_GetSecret(t *testing.T) { t.Run("NotUnlocked", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -115,7 +115,7 @@ func TestOnePasswordCLISecretsProvider_GetSecret(t *testing.T) { t.Run("InvalidKeyFormat", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -151,7 +151,7 @@ func TestOnePasswordCLISecretsProvider_GetSecret(t *testing.T) { func TestParseSecrets(t *testing.T) { t.Run("Success", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -192,7 +192,7 @@ func TestParseSecrets(t *testing.T) { t.Run("EmptyInput", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -221,7 +221,7 @@ func TestParseSecrets(t *testing.T) { t.Run("InvalidFormat", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -251,7 +251,7 @@ func TestParseSecrets(t *testing.T) { t.Run("MalformedJSON", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -281,7 +281,7 @@ func TestParseSecrets(t *testing.T) { t.Run("MismatchedVaultID", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ @@ -311,7 +311,7 @@ func TestParseSecrets(t *testing.T) { t.Run("SecretNotFound", func(t *testing.T) { // Given a set of mock components - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // And a test vault configuration vault := secretsConfigType.OnePasswordVault{ diff --git a/pkg/runtime/secrets/op_sdk_secrets_provider_test.go b/pkg/runtime/secrets/op_sdk_secrets_provider_test.go index 7512c5c4e..a40864d3b 100644 --- a/pkg/runtime/secrets/op_sdk_secrets_provider_test.go +++ b/pkg/runtime/secrets/op_sdk_secrets_provider_test.go @@ -22,7 +22,7 @@ import ( func TestNewOnePasswordSDKSecretsProvider(t *testing.T) { t.Run("Success", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -56,7 +56,7 @@ func TestNewOnePasswordSDKSecretsProvider(t *testing.T) { func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("Success", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -104,7 +104,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("NotUnlocked", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -137,7 +137,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("InvalidKeyFormat", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -175,7 +175,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("MissingToken", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -212,7 +212,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("ClientCreationError", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -258,7 +258,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("SecretResolutionError", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -304,7 +304,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { t.Run("NilClient", func(t *testing.T) { // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ @@ -351,7 +351,7 @@ func TestOnePasswordSDKSecretsProvider_GetSecret(t *testing.T) { func TestOnePasswordSDKSecretsProvider_ParseSecrets(t *testing.T) { // setup creates and initializes a OnePasswordSDKSecretsProvider for testing - setup := func(t *testing.T) (*Mocks, *OnePasswordSDKSecretsProvider) { + setup := func(t *testing.T) (*SecretsTestMocks, *OnePasswordSDKSecretsProvider) { t.Helper() // Set environment variable @@ -359,7 +359,7 @@ func TestOnePasswordSDKSecretsProvider_ParseSecrets(t *testing.T) { t.Cleanup(func() { os.Unsetenv("OP_SERVICE_ACCOUNT_TOKEN") }) // Setup mocks - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) // Create a test vault vault := secretsConfigType.OnePasswordVault{ diff --git a/pkg/runtime/secrets/secrets_provider_test.go b/pkg/runtime/secrets/secrets_provider_test.go index 6b295a7d9..5e0bbd9e2 100644 --- a/pkg/runtime/secrets/secrets_provider_test.go +++ b/pkg/runtime/secrets/secrets_provider_test.go @@ -15,22 +15,19 @@ import ( // Test Setup // ============================================================================= -type Mocks struct { +type SecretsTestMocks struct { Shell *shell.MockShell Shims *Shims } -type SetupOptions struct { -} - -// setupMocks creates mock components for testing the secrets provider -func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { +// setupSecretsMocks creates mock components for testing the secrets provider +func setupSecretsMocks(t *testing.T) *SecretsTestMocks { t.Helper() // Create a mock shell mockShell := shell.NewMockShell() - return &Mocks{ + return &SecretsTestMocks{ Shell: mockShell, Shims: NewShims(), } @@ -42,7 +39,7 @@ func setupMocks(t *testing.T, _ ...*SetupOptions) *Mocks { func TestBaseSecretsProvider_LoadSecrets(t *testing.T) { t.Run("Success", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) provider := NewBaseSecretsProvider(mocks.Shell) err := provider.LoadSecrets() @@ -63,7 +60,7 @@ func TestBaseSecretsProvider_LoadSecrets(t *testing.T) { func TestBaseSecretsProvider_GetSecret(t *testing.T) { t.Run("PanicsWhenNotImplemented", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) provider := NewBaseSecretsProvider(mocks.Shell) defer func() { @@ -80,7 +77,7 @@ func TestBaseSecretsProvider_GetSecret(t *testing.T) { func TestBaseSecretsProvider_ParseSecrets(t *testing.T) { t.Run("PanicsWhenNotImplemented", func(t *testing.T) { - mocks := setupMocks(t) + mocks := setupSecretsMocks(t) provider := NewBaseSecretsProvider(mocks.Shell) defer func() { diff --git a/pkg/runtime/secrets/sops_secrets_provider_test.go b/pkg/runtime/secrets/sops_secrets_provider_test.go index eb9e23e09..a94249252 100644 --- a/pkg/runtime/secrets/sops_secrets_provider_test.go +++ b/pkg/runtime/secrets/sops_secrets_provider_test.go @@ -16,8 +16,8 @@ import ( // Test Setup // ============================================================================= -func setupSopsSecretsMocks(t *testing.T, opts ...*SetupOptions) *Mocks { - mocks := setupMocks(t, opts...) +func setupSopsSecretsMocks(t *testing.T) *SecretsTestMocks { + mocks := setupSecretsMocks(t) // Mock the stat function to simulate the file exists mocks.Shims.Stat = func(name string) (os.FileInfo, error) { @@ -42,7 +42,7 @@ nested: // ============================================================================= func TestNewSopsSecretsProvider(t *testing.T) { - setup := func(t *testing.T) (*SopsSecretsProvider, *Mocks) { + setup := func(t *testing.T) (*SopsSecretsProvider, *SecretsTestMocks) { mocks := setupSopsSecretsMocks(t) provider := NewSopsSecretsProvider("/valid/config/path", mocks.Shell) provider.shims = mocks.Shims @@ -65,7 +65,7 @@ func TestNewSopsSecretsProvider(t *testing.T) { // ============================================================================= func TestSopsSecretsProvider_LoadSecrets(t *testing.T) { - setup := func(t *testing.T) (*SopsSecretsProvider, *Mocks) { + setup := func(t *testing.T) (*SopsSecretsProvider, *SecretsTestMocks) { mocks := setupSopsSecretsMocks(t) provider := NewSopsSecretsProvider("/valid/config/path", mocks.Shell) provider.shims = mocks.Shims @@ -167,7 +167,7 @@ func TestSopsSecretsProvider_LoadSecrets(t *testing.T) { } func TestSopsSecretsProvider_GetSecret(t *testing.T) { - setup := func(t *testing.T) (*SopsSecretsProvider, *Mocks) { + setup := func(t *testing.T) (*SopsSecretsProvider, *SecretsTestMocks) { mocks := setupSopsSecretsMocks(t) provider := NewSopsSecretsProvider("/valid/config/path", mocks.Shell) provider.shims = mocks.Shims @@ -228,7 +228,7 @@ func TestSopsSecretsProvider_GetSecret(t *testing.T) { } func TestSopsSecretsProvider_ParseSecrets(t *testing.T) { - setup := func(t *testing.T) (*SopsSecretsProvider, *Mocks) { + setup := func(t *testing.T) (*SopsSecretsProvider, *SecretsTestMocks) { mocks := setupSopsSecretsMocks(t) provider := NewSopsSecretsProvider("/valid/config/path", mocks.Shell) provider.shims = mocks.Shims @@ -370,7 +370,7 @@ func TestSopsSecretsProvider_ParseSecrets(t *testing.T) { } func TestSopsSecretsProvider_findSecretsFilePath(t *testing.T) { - setup := func(t *testing.T) (*SopsSecretsProvider, *Mocks) { + setup := func(t *testing.T) (*SopsSecretsProvider, *SecretsTestMocks) { mocks := setupSopsSecretsMocks(t) provider := NewSopsSecretsProvider("/valid/config/path", mocks.Shell) provider.shims = mocks.Shims From bb52e092ce43eaa021a61acca96977e816800c0d Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:14:11 -0500 Subject: [PATCH 07/14] attempt windows fix Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime.go | 9 +++++++++ pkg/runtime/runtime_test.go | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index c1cf127ea..d81ede1a8 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -328,6 +328,9 @@ func (rt *Runtime) GetBuildID() (string, error) { if err != nil { return "", fmt.Errorf("failed to open project root: %w", err) } + if root == nil { + return "", fmt.Errorf("failed to open project root: root is nil") + } defer root.Close() buildIDPath := ".windsor/.build-id" @@ -497,6 +500,9 @@ func (rt *Runtime) writeBuildIDToFile(buildID string) error { if err != nil { return fmt.Errorf("failed to open project root: %w", err) } + if root == nil { + return fmt.Errorf("failed to open project root: root is nil") + } defer root.Close() buildIDPath := ".windsor/.build-id" @@ -531,6 +537,9 @@ func (rt *Runtime) generateBuildID() (string, error) { if err != nil { return "", fmt.Errorf("failed to open project root: %w", err) } + if root == nil { + return "", fmt.Errorf("failed to open project root: root is nil") + } defer root.Close() buildIDPath := ".windsor/.build-id" diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 42aafab9c..ac2cf0925 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -1877,7 +1877,7 @@ func TestRuntime_GetBuildID(t *testing.T) { // Then an error should be returned if err == nil { - t.Error("Expected error when ReadFile fails") + t.Fatal("Expected error when ReadFile fails") } if !strings.Contains(err.Error(), "failed to read build ID file") { @@ -1909,7 +1909,7 @@ func TestRuntime_GetBuildID(t *testing.T) { // Then an error should be returned if err == nil { - t.Error("Expected error when writeBuildIDToFile fails") + t.Fatal("Expected error when writeBuildIDToFile fails") } if !strings.Contains(err.Error(), "failed to set build ID") && !strings.Contains(err.Error(), "failed to read build ID file") { From 9e8e32b95b3ad6519c083cafebac36fc8052f2b2 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 12:24:47 -0500 Subject: [PATCH 08/14] windows fix Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index ac2cf0925..1d6abd583 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -2094,7 +2094,7 @@ func TestRuntime_GenerateBuildID(t *testing.T) { // Then an error should be returned if err == nil { - t.Error("Expected error when writeBuildIDToFile fails") + t.Fatal("Expected error when writeBuildIDToFile fails") } if !strings.Contains(err.Error(), "failed to set build ID") && !strings.Contains(err.Error(), "failed to read build ID file") { From 20c22f66b49fe1e59761ed72a4a1ed46af1f5250 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:00:32 -0500 Subject: [PATCH 09/14] Windows fix Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/runtime_test.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 1d6abd583..a4da03cc1 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "path/filepath" + "runtime" "strings" "testing" @@ -1849,6 +1850,10 @@ func TestRuntime_GetBuildID(t *testing.T) { }) t.Run("ErrorWhenReadFileFails", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on Windows: os.Chmod with 0000 does not prevent file operations") + } + // Given a runtime with a build ID file that cannot be read mocks := setupRuntimeMocks(t) rt := mocks.Runtime @@ -1886,6 +1891,10 @@ func TestRuntime_GetBuildID(t *testing.T) { }) t.Run("ErrorWhenWriteBuildIDToFileFails", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on Windows: os.Chmod with 0000 does not prevent file operations") + } + // Given a runtime with a build ID directory that cannot be written to mocks := setupRuntimeMocks(t) rt := mocks.Runtime @@ -2071,6 +2080,10 @@ func TestRuntime_GenerateBuildID(t *testing.T) { }) t.Run("ErrorWhenWriteBuildIDToFileFails", func(t *testing.T) { + if runtime.GOOS == "windows" { + t.Skip("Skipping on Windows: os.Chmod with 0000 does not prevent file operations") + } + // Given a runtime with a build ID directory that cannot be written to mocks := setupRuntimeMocks(t) rt := mocks.Runtime From fb2dd8fe9b9271a3dc66a04c444e7a3c426ab1ab Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 16:29:00 -0500 Subject: [PATCH 10/14] Increaes shell test coverage Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/shell/shell.go | 4 +- pkg/runtime/shell/shell_test.go | 181 +++++++++++++++++++++++++++ pkg/runtime/shell/unix_shell_test.go | 116 +++++++++++++++++ 3 files changed, 299 insertions(+), 2 deletions(-) diff --git a/pkg/runtime/shell/shell.go b/pkg/runtime/shell/shell.go index 84fcf993a..3e8353a31 100644 --- a/pkg/runtime/shell/shell.go +++ b/pkg/runtime/shell/shell.go @@ -665,9 +665,9 @@ func (s *DefaultShell) scrubString(input string) string { // The export parameter controls whether to use OS-specific export commands or plain KEY=value format func (s *DefaultShell) PrintEnvVars(envVars map[string]string, export bool) { if export { - s.renderEnvVarsWithExport(envVars) + fmt.Print(s.renderEnvVarsWithExport(envVars)) } else { - s.renderEnvVarsPlain(envVars) + fmt.Print(s.renderEnvVarsPlain(envVars)) } } diff --git a/pkg/runtime/shell/shell_test.go b/pkg/runtime/shell/shell_test.go index 3d30916c1..ddf914885 100644 --- a/pkg/runtime/shell/shell_test.go +++ b/pkg/runtime/shell/shell_test.go @@ -958,6 +958,24 @@ func TestShell_GetSessionToken(t *testing.T) { t.Errorf("Expected token length 7, got %d", len(token)) } }) + + t.Run("ReturnsCachedToken", func(t *testing.T) { + // Given a shell with a cached session token + shell, _ := setup(t) + expectedToken := "cached-token" + shell.sessionToken = expectedToken + + // When getting session token + token, err := shell.GetSessionToken() + + // Then it should return the cached token without calling Getenv + if err != nil { + t.Errorf("Expected no error, got %v", err) + } + if token != expectedToken { + t.Errorf("Expected token %q, got %q", expectedToken, token) + } + }) } func TestShell_CheckResetFlags(t *testing.T) { @@ -1118,6 +1136,20 @@ func TestShell_GenerateRandomString(t *testing.T) { t.Errorf("Expected length 10, got %d", len(result)) } }) + + t.Run("ErrorWhenRandReadFails", func(t *testing.T) { + shell, mocks := setup(t) + mocks.Shims.RandRead = func(b []byte) (n int, err error) { + return 0, fmt.Errorf("random read failed") + } + result, err := shell.generateRandomString(10) + if err == nil { + t.Errorf("Expected error, got nil") + } + if result != "" { + t.Errorf("Expected empty result on error, got %q", result) + } + }) } func TestShell_InstallHook(t *testing.T) { @@ -3249,3 +3281,152 @@ func TestScrubbingWriter(t *testing.T) { } }) } + +// TestDefaultShell_RenderEnvVars tests the RenderEnvVars method +func TestDefaultShell_RenderEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=true + result := shell.RenderEnvVars(envVars, true) + + // Then the output should contain export commands + if !strings.Contains(result, "export") { + t.Errorf("Expected export commands, got: %s", result) + } + }) + + t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=false + result := shell.RenderEnvVars(envVars, false) + + // Then the output should contain plain KEY=value format + if !strings.Contains(result, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", result) + } + if strings.Contains(result, "export") { + t.Errorf("Expected no export commands, got: %s", result) + } + }) +} + +// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method +func TestDefaultShell_PrintEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=true + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, true) + }) + + // Then the output should contain export commands + if !strings.Contains(output, "export") { + t.Errorf("Expected export commands, got: %s", output) + } + }) + + t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=false + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, false) + }) + + // Then the output should contain plain KEY=value format + if !strings.Contains(output, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", output) + } + if strings.Contains(output, "export") { + t.Errorf("Expected no export commands, got: %s", output) + } + }) +} + +// TestDefaultShell_renderEnvVarsPlain tests the renderEnvVarsPlain method +func TestDefaultShell_renderEnvVarsPlain(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersPlainEnvVars", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + "VAR3": "", + } + + // When rendering plain environment variables + result := shell.renderEnvVarsPlain(envVars) + + // Then the output should contain sorted KEY=value format + if !strings.Contains(result, "VAR1=value1\n") { + t.Errorf("Expected VAR1=value1, got: %s", result) + } + if !strings.Contains(result, "VAR2=value2\n") { + t.Errorf("Expected VAR2=value2, got: %s", result) + } + if !strings.Contains(result, "VAR3=\n") { + t.Errorf("Expected VAR3=, got: %s", result) + } + }) + + t.Run("RendersEmptyEnvVars", func(t *testing.T) { + // Given a shell with empty environment variables map + shell, _ := setup(t) + envVars := map[string]string{} + + // When rendering plain environment variables + result := shell.renderEnvVarsPlain(envVars) + + // Then the output should be empty + if result != "" { + t.Errorf("Expected empty output for empty env vars, got: %s", result) + } + }) +} diff --git a/pkg/runtime/shell/unix_shell_test.go b/pkg/runtime/shell/unix_shell_test.go index f46315e95..518421f3a 100644 --- a/pkg/runtime/shell/unix_shell_test.go +++ b/pkg/runtime/shell/unix_shell_test.go @@ -6,6 +6,7 @@ package shell import ( "os" "path/filepath" + "strings" "testing" ) @@ -159,3 +160,118 @@ func TestDefaultShell_UnsetAlias(t *testing.T) { } }) } + +// TestDefaultShell_RenderAliases tests the RenderAliases method on Unix systems +func TestDefaultShell_RenderAliases(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersAliasesWithValues", func(t *testing.T) { + // Given a shell with aliases + shell, _ := setup(t) + aliases := map[string]string{ + "ll": "ls -la", + "grep": "grep --color=auto", + "cd": "", + } + + // When rendering aliases + result := shell.RenderAliases(aliases) + + // Then the output should contain sorted alias commands + if !strings.Contains(result, "alias cd=\"\"\n") && !strings.Contains(result, "unalias cd\n") { + t.Errorf("Expected unalias or empty alias for cd, got: %s", result) + } + if !strings.Contains(result, "alias grep=\"grep --color=auto\"\n") { + t.Errorf("Expected alias for grep, got: %s", result) + } + if !strings.Contains(result, "alias ll=\"ls -la\"\n") { + t.Errorf("Expected alias for ll, got: %s", result) + } + }) + + t.Run("RendersEmptyAliases", func(t *testing.T) { + // Given a shell with empty aliases map + shell, _ := setup(t) + aliases := map[string]string{} + + // When rendering aliases + result := shell.RenderAliases(aliases) + + // Then the output should be empty + if result != "" { + t.Errorf("Expected empty output for empty aliases, got: %s", result) + } + }) + + t.Run("RendersAliasesWithEmptyValues", func(t *testing.T) { + // Given a shell with aliases that have empty values + shell, _ := setup(t) + aliases := map[string]string{ + "alias1": "", + "alias2": "", + } + + // When rendering aliases + result := shell.RenderAliases(aliases) + + // Then the output should contain unalias commands + if !strings.Contains(result, "unalias") { + t.Errorf("Expected unalias commands for empty aliases, got: %s", result) + } + }) +} + +// TestDefaultShell_renderEnvVarsWithExport tests the renderEnvVarsWithExport method on Unix systems +func TestDefaultShell_renderEnvVarsWithExport(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + "VAR3": "", + } + + // When rendering environment variables with export + result := shell.renderEnvVarsWithExport(envVars) + + // Then the output should contain sorted export commands + if !strings.Contains(result, "export VAR1=\"value1\"\n") { + t.Errorf("Expected export VAR1, got: %s", result) + } + if !strings.Contains(result, "export VAR2=\"value2\"\n") { + t.Errorf("Expected export VAR2, got: %s", result) + } + if !strings.Contains(result, "unset VAR3\n") { + t.Errorf("Expected unset VAR3, got: %s", result) + } + }) + + t.Run("RendersEmptyEnvVars", func(t *testing.T) { + // Given a shell with empty environment variables map + shell, _ := setup(t) + envVars := map[string]string{} + + // When rendering environment variables with export + result := shell.renderEnvVarsWithExport(envVars) + + // Then the output should be empty + if result != "" { + t.Errorf("Expected empty output for empty env vars, got: %s", result) + } + }) +} From 5217e2564d488ca82ef7493aafd11df9fe8ed368 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:10:47 -0500 Subject: [PATCH 11/14] Enhance config test coverage Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- .../config/config_handler_private_test.go | 175 +++++ .../config/config_handler_public_test.go | 643 ++++++++++++++++++ pkg/runtime/config/schema_validator_test.go | 160 +++++ 3 files changed, 978 insertions(+) diff --git a/pkg/runtime/config/config_handler_private_test.go b/pkg/runtime/config/config_handler_private_test.go index cbf57ba18..34e7f73e0 100644 --- a/pkg/runtime/config/config_handler_private_test.go +++ b/pkg/runtime/config/config_handler_private_test.go @@ -149,6 +149,28 @@ func TestConfigHandler_setValueInMap(t *testing.T) { t.Error("Expected empty path to be handled gracefully") } }) + + t.Run("OverwritesNonMapValueWithMap", func(t *testing.T) { + data := map[string]any{ + "parent": "not_a_map", + } + + setValueInMap(data, []string{"parent", "child", "key"}, "nested_value") + + parent, ok := data["parent"].(map[string]any) + if !ok { + t.Fatal("Expected parent to be converted to a map") + } + + child, ok := parent["child"].(map[string]any) + if !ok { + t.Fatal("Expected child to be a map") + } + + if child["key"] != "nested_value" { + t.Errorf("Expected 'nested_value', got '%v'", child["key"]) + } + }) } func TestConfigHandler_convertInterfaceMap(t *testing.T) { @@ -191,6 +213,37 @@ func TestConfigHandler_convertInterfaceMap(t *testing.T) { } }) + t.Run("HandlesMapStringAnyValues", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + input := map[interface{}]interface{}{ + "key1": map[string]any{ + "nested": "value", + }, + "key2": map[interface{}]interface{}{ + "nested": "value2", + }, + } + + result := handler.convertInterfaceMap(input) + + key1Val, ok := result["key1"].(map[string]any) + if !ok { + t.Fatal("Expected key1 value to be map[string]any") + } + if key1Val["nested"] != "value" { + t.Errorf("Expected nested value, got '%v'", key1Val["nested"]) + } + + key2Val, ok := result["key2"].(map[string]any) + if !ok { + t.Fatal("Expected key2 value to be converted to map[string]any") + } + if key2Val["nested"] != "value2" { + t.Errorf("Expected nested value2, got '%v'", key2Val["nested"]) + } + }) + t.Run("SkipsNonStringKeys", func(t *testing.T) { handler, _ := setupPrivateTestHandler(t) @@ -485,6 +538,42 @@ func TestConfigHandler_parsePath(t *testing.T) { t.Errorf("Expected ['key'], got %v", result) } }) + + t.Run("HandlesDotInsideBrackets", func(t *testing.T) { + result := parsePath("parent[child.key].nested") + + expected := []string{"parent", "child.key", "nested"} + if len(result) != len(expected) { + t.Fatalf("Expected %d keys, got %d", len(expected), len(result)) + } + for i, key := range expected { + if result[i] != key { + t.Errorf("Expected key[%d]='%s', got '%s'", i, key, result[i]) + } + } + }) + + t.Run("HandlesBracketAtStart", func(t *testing.T) { + result := parsePath("[key].value") + + expected := []string{"key", "value"} + if len(result) != len(expected) { + t.Fatalf("Expected %d keys, got %d", len(expected), len(result)) + } + for i, key := range expected { + if result[i] != key { + t.Errorf("Expected key[%d]='%s', got '%s'", i, key, result[i]) + } + } + }) + + t.Run("HandlesEmptyPath", func(t *testing.T) { + result := parsePath("") + + if len(result) != 0 { + t.Errorf("Expected empty slice, got %v", result) + } + }) } func TestConfigHandler_convertStringValue(t *testing.T) { @@ -600,6 +689,72 @@ properties: t.Errorf("Expected 1.5 from pattern, got %v", floatResult) } }) + + t.Run("ConvertsWhenSchemaValidatorIsNil", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + handler.schemaValidator = nil + + result := handler.convertStringValue("true") + + if result != true { + t.Errorf("Expected true, got %v", result) + } + }) + + t.Run("ConvertsWhenSchemaIsNil", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + handler.schemaValidator = &SchemaValidator{ + Schema: nil, + } + + result := handler.convertStringValue("true") + + if result != true { + t.Errorf("Expected true, got %v", result) + } + }) + + t.Run("ConvertsWhenExpectedTypeIsEmpty", func(t *testing.T) { + handler, tmpDir := setupPrivateTestHandler(t) + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + test_key: + type: string +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + result := handler.convertStringValue("test_value") + + if result != "test_value" { + t.Errorf("Expected 'test_value', got '%v'", result) + } + }) + + t.Run("ConvertsWhenConvertStringToTypeReturnsNil", func(t *testing.T) { + handler, tmpDir := setupPrivateTestHandler(t) + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + invalid_bool: + type: boolean +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + result := handler.convertStringValue("not_a_bool") + + if result == nil { + t.Error("Expected result to fall back to pattern conversion, got nil") + } + }) } func TestConfigHandler_convertStringToType(t *testing.T) { @@ -859,4 +1014,24 @@ properties: {} t.Errorf("Expected empty for invalid property schema, got '%s'", typeStr) } }) + + t.Run("HandlesNonStringType", func(t *testing.T) { + handler, _ := setupPrivateTestHandler(t) + + handler.schemaValidator = &SchemaValidator{ + Schema: map[string]any{ + "properties": map[string]any{ + "test_key": map[string]any{ + "type": 123, + }, + }, + }, + } + + typeStr := handler.getExpectedTypeFromSchema("test_key") + + if typeStr != "" { + t.Errorf("Expected empty for non-string type, got '%s'", typeStr) + } + }) } diff --git a/pkg/runtime/config/config_handler_public_test.go b/pkg/runtime/config/config_handler_public_test.go index 467b5a304..f06b9dba3 100644 --- a/pkg/runtime/config/config_handler_public_test.go +++ b/pkg/runtime/config/config_handler_public_test.go @@ -1,8 +1,10 @@ package config import ( + "fmt" "os" "path/filepath" + "strings" "testing" "github.com/windsorcli/cli/api/v1alpha1" @@ -322,6 +324,344 @@ additionalProperties: false } }) + t.Run("LoadsSchemaWhenSchemaValidatorIsNil", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + handler.schemaValidator.Schema = nil + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + schema_key: + type: string + default: schema_value +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte("provider: local\n"), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + value := handler.Get("schema_key") + if value != "schema_value" { + t.Errorf("Expected schema default to be loaded, got '%v'", value) + } + }) + + t.Run("HandlesSchemaLoadError", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + handler.schemaValidator.Schema = nil + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + invalidSchema := `invalid: yaml: [[[` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(invalidSchema), 0644) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when schema loading fails") + } + }) + + t.Run("HandlesRootConfigWithoutContexts", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + handler.SetContext("test-context") + + tmpDir, _ := mocks.Shell.GetProjectRoot() + rootConfig := `version: v1alpha1 +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) + + t.Run("HandlesRootConfigWithMissingContext", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + handler.SetContext("missing-context") + + tmpDir, _ := mocks.Shell.GetProjectRoot() + rootConfig := `version: v1alpha1 +contexts: + other-context: + provider: local +` + os.WriteFile(filepath.Join(tmpDir, "windsor.yaml"), []byte(rootConfig), 0644) + + err := handler.LoadConfig() + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + }) + + t.Run("ReturnsErrorWhenShellIsNil", func(t *testing.T) { + handler := &configHandler{ + shell: nil, + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when shell is nil") + } + if !strings.Contains(err.Error(), "shell not initialized") { + t.Errorf("Expected error about shell not initialized, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenGetProjectRootFails", func(t *testing.T) { + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", fmt.Errorf("project root error") + } + + handler := NewConfigHandler(mockShell) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when GetProjectRoot fails") + } + if !strings.Contains(err.Error(), "error retrieving project root") { + t.Errorf("Expected error about retrieving project root, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenReadFileFailsForRootConfig", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + rootConfigPath := filepath.Join(tmpDir, "windsor.yaml") + os.WriteFile(rootConfigPath, []byte("version: v1alpha1\n"), 0644) + + handler.shims.ReadFile = func(name string) ([]byte, error) { + if name == rootConfigPath { + return nil, fmt.Errorf("read file error") + } + return os.ReadFile(name) + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when ReadFile fails for root config") + } + if !strings.Contains(err.Error(), "error reading root config file") { + t.Errorf("Expected error about reading root config file, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenYamlUnmarshalFailsForRootConfig", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + rootConfigPath := filepath.Join(tmpDir, "windsor.yaml") + os.WriteFile(rootConfigPath, []byte("invalid: yaml: [[["), 0644) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when YamlUnmarshal fails for root config") + } + if !strings.Contains(err.Error(), "error unmarshalling root config") { + t.Errorf("Expected error about unmarshalling root config, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenReadFileFailsForContextConfig", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + contextConfigPath := filepath.Join(contextDir, "windsor.yaml") + os.WriteFile(contextConfigPath, []byte("provider: local\n"), 0644) + + handler.shims.ReadFile = func(name string) ([]byte, error) { + if name == contextConfigPath { + return nil, fmt.Errorf("read file error") + } + return os.ReadFile(name) + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when ReadFile fails for context config") + } + if !strings.Contains(err.Error(), "error reading context config file") { + t.Errorf("Expected error about reading context config file, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenYamlUnmarshalFailsForContextConfig", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "windsor.yaml"), []byte("invalid: yaml: [[["), 0644) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when YamlUnmarshal fails for context config") + } + if !strings.Contains(err.Error(), "error unmarshalling context yaml") { + t.Errorf("Expected error about unmarshalling context yaml, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenReadFileFailsForValuesYaml", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + valuesPath := filepath.Join(contextDir, "values.yaml") + os.WriteFile(valuesPath, []byte("key: value\n"), 0644) + + handler.shims.ReadFile = func(name string) ([]byte, error) { + if name == valuesPath { + return nil, fmt.Errorf("read file error") + } + return os.ReadFile(name) + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when ReadFile fails for values.yaml") + } + if !strings.Contains(err.Error(), "error reading values.yaml") { + t.Errorf("Expected error about reading values.yaml, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenYamlUnmarshalFailsForValuesYaml", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte("invalid: yaml: [[["), 0644) + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when YamlUnmarshal fails for values.yaml") + } + if !strings.Contains(err.Error(), "error unmarshalling values.yaml") { + t.Errorf("Expected error about unmarshalling values.yaml, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenYamlMarshalFailsForContextConfig", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + rootConfigPath := filepath.Join(tmpDir, "windsor.yaml") + rootConfig := `version: v1alpha1 +contexts: + test-context: + provider: local +` + os.WriteFile(rootConfigPath, []byte(rootConfig), 0644) + + handler.shims.YamlMarshal = func(v any) ([]byte, error) { + return nil, fmt.Errorf("marshal error") + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when YamlMarshal fails for context config") + } + if !strings.Contains(err.Error(), "error marshalling context config") { + t.Errorf("Expected error about marshalling context config, got: %v", err) + } + }) + + t.Run("ReturnsErrorWhenValidateReturnsError", func(t *testing.T) { + mocks := setupConfigMocks(t) + tmpDir, _ := mocks.Shell.GetProjectRoot() + + handler := NewConfigHandler(mocks.Shell).(*configHandler) + handler.SetContext("test-context") + + schemaDir := filepath.Join(tmpDir, "contexts", "_template") + os.MkdirAll(schemaDir, 0755) + schemaContent := `$schema: https://json-schema.org/draft/2020-12/schema +type: object +properties: + test_key: + type: string + pattern: "^[a-z]+$" +` + os.WriteFile(filepath.Join(schemaDir, "schema.yaml"), []byte(schemaContent), 0644) + handler.LoadSchema(filepath.Join(schemaDir, "schema.yaml")) + + contextDir := filepath.Join(tmpDir, "contexts", "test-context") + os.MkdirAll(contextDir, 0755) + os.WriteFile(filepath.Join(contextDir, "values.yaml"), []byte("test_key: VALUE\n"), 0644) + + handler.schemaValidator.Shims.RegexpMatchString = func(pattern, s string) (bool, error) { + return false, fmt.Errorf("regex error") + } + + err := handler.LoadConfig() + + if err == nil { + t.Error("Expected error when Validate returns error") + } + if !strings.Contains(err.Error(), "values.yaml validation") { + t.Errorf("Expected error about values.yaml validation, got: %v", err) + } + }) + t.Run("HandlesYmlExtension", func(t *testing.T) { mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -649,6 +989,139 @@ custom_key: custom_value t.Errorf("Expected no error for empty string, got %v", err) } }) + + t.Run("HandlesMapAnyAnyContexts", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "test-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + yaml := `version: v1alpha1 +contexts: + test-context: + provider: local + custom: value +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + provider := handler.GetString("provider") + if provider != "local" { + t.Errorf("Expected provider='local', got '%s'", provider) + } + + custom := handler.GetString("custom") + if custom != "value" { + t.Errorf("Expected custom='value', got '%s'", custom) + } + }) + + t.Run("HandlesContextDataAsMapAnyAny", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "test-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell).(*configHandler) + + yaml := `version: v1alpha1 +contexts: + test-context: + provider: local + nested: + key: value +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + provider := handler.GetString("provider") + if provider != "local" { + t.Errorf("Expected provider='local', got '%s'", provider) + } + }) + + t.Run("HandlesMissingContextInContexts", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "missing-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + yaml := `version: v1alpha1 +contexts: + other-context: + provider: local +flat_key: flat_value +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + flatKey := handler.GetString("flat_key") + if flatKey != "flat_value" { + t.Errorf("Expected flat_key='flat_value', got '%s'", flatKey) + } + }) + + t.Run("HandlesContextsAsNonMap", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "test-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + yaml := `version: v1alpha1 +contexts: not_a_map +flat_key: flat_value +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + flatKey := handler.GetString("flat_key") + if flatKey != "flat_value" { + t.Errorf("Expected flat_key='flat_value', got '%s'", flatKey) + } + }) + + t.Run("HandlesContextDataAsNonMap", func(t *testing.T) { + os.Setenv("WINDSOR_CONTEXT", "test-context") + defer os.Unsetenv("WINDSOR_CONTEXT") + + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + yaml := `version: v1alpha1 +contexts: + test-context: not_a_map +flat_key: flat_value +` + + err := handler.LoadConfigString(yaml) + + if err != nil { + t.Fatalf("Expected no error, got %v", err) + } + + flatKey := handler.GetString("flat_key") + if flatKey != "flat_value" { + t.Errorf("Expected flat_key='flat_value', got '%s'", flatKey) + } + }) } func TestConfigHandler_Get(t *testing.T) { @@ -906,6 +1379,33 @@ func TestConfigHandler_GetInt(t *testing.T) { } }) + t.Run("HandlesUint64Overflow", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + maxUint64 := uint64(^uint(0)>>1) + 1 + handler.Set("count", maxUint64) + + result := handler.GetInt("count", 99) + + if result != 99 { + t.Errorf("Expected default value 99 for uint64 overflow, got %d", result) + } + }) + + t.Run("HandlesInvalidString", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("count", "not-a-number") + + result := handler.GetInt("count", 99) + + if result != 99 { + t.Errorf("Expected default value 99 for invalid string, got %d", result) + } + }) + t.Run("ReturnsZeroForNonNumericValue", func(t *testing.T) { mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -955,6 +1455,19 @@ func TestConfigHandler_GetBool(t *testing.T) { t.Error("Expected true, got false") } }) + + t.Run("ReturnsFalseForNonBoolValueWithDefault", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("enabled", 1) + + result := handler.GetBool("enabled", true) + + if result { + t.Error("Expected false for non-bool value even with default, got true") + } + }) } func TestConfigHandler_GetStringSlice(t *testing.T) { @@ -990,6 +1503,28 @@ func TestConfigHandler_GetStringSlice(t *testing.T) { } }) + t.Run("ConvertsInterfaceSliceWithNonStringValues", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("items", []interface{}{"x", 42, true}) + + result := handler.GetStringSlice("items") + + if len(result) != 3 { + t.Errorf("Expected length 3, got %d", len(result)) + } + if result[0] != "x" { + t.Errorf("Expected first element 'x', got '%s'", result[0]) + } + if result[1] != "42" { + t.Errorf("Expected second element '42', got '%s'", result[1]) + } + if result[2] != "true" { + t.Errorf("Expected third element 'true', got '%s'", result[2]) + } + }) + t.Run("ReturnsEmptySliceForMissingKey", func(t *testing.T) { mocks := setupConfigMocks(t) handler := NewConfigHandler(mocks.Shell) @@ -2050,6 +2585,23 @@ properties: t.Error("Expected error for invalid schema content") } }) + + t.Run("ReturnsErrorWhenSchemaValidatorIsNil", func(t *testing.T) { + // Given a handler with nil schema validator + handler, _ := setupPrivateTestHandler(t) + handler.schemaValidator = nil + + // When loading schema + err := handler.LoadSchema("/some/path/schema.yaml") + + // Then it should return error + if err == nil { + t.Error("Expected error when schema validator is nil") + } + if !strings.Contains(err.Error(), "schema validator not initialized") { + t.Errorf("Expected error about schema validator not initialized, got: %v", err) + } + }) } func TestConfigHandler_LoadSchemaFromBytes(t *testing.T) { @@ -2077,6 +2629,23 @@ properties: } }) + t.Run("ReturnsErrorWhenSchemaValidatorIsNil", func(t *testing.T) { + // Given a handler with nil schema validator + handler, _ := setupPrivateTestHandler(t) + handler.schemaValidator = nil + + // When loading schema from bytes + err := handler.LoadSchemaFromBytes([]byte("test: content")) + + // Then it should return error + if err == nil { + t.Error("Expected error when schema validator is nil") + } + if !strings.Contains(err.Error(), "schema validator not initialized") { + t.Errorf("Expected error about schema validator not initialized, got: %v", err) + } + }) + t.Run("HandlesInvalidSchemaBytes", func(t *testing.T) { // Given a handler and invalid schema bytes handler, _ := setupPrivateTestHandler(t) @@ -2265,6 +2834,80 @@ func TestConfigHandler_IsLoaded(t *testing.T) { }) } +func TestConfigHandler_IsDevMode(t *testing.T) { + t.Run("ReturnsTrueWhenDevIsSetToTrue", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("dev", true) + + result := handler.IsDevMode("production") + + if !result { + t.Error("Expected IsDevMode=true when dev=true, regardless of context name") + } + }) + + t.Run("ReturnsFalseWhenDevIsSetToFalse", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("dev", false) + + result := handler.IsDevMode("local") + + if result { + t.Error("Expected IsDevMode=false when dev=false, even for local context") + } + }) + + t.Run("ReturnsTrueForLocalContext", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + result := handler.IsDevMode("local") + + if !result { + t.Error("Expected IsDevMode=true for 'local' context") + } + }) + + t.Run("ReturnsTrueForLocalPrefixContext", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + result := handler.IsDevMode("local-dev") + + if !result { + t.Error("Expected IsDevMode=true for context starting with 'local-'") + } + }) + + t.Run("ReturnsFalseForNonLocalContext", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + result := handler.IsDevMode("production") + + if result { + t.Error("Expected IsDevMode=false for non-local context") + } + }) + + t.Run("IgnoresNonBoolDevValue", func(t *testing.T) { + mocks := setupConfigMocks(t) + handler := NewConfigHandler(mocks.Shell) + + handler.Set("dev", "true") + + result := handler.IsDevMode("local") + + if !result { + t.Error("Expected IsDevMode=true for local context when dev is non-bool") + } + }) +} + // ============================================================================= // Test Helpers // ============================================================================= diff --git a/pkg/runtime/config/schema_validator_test.go b/pkg/runtime/config/schema_validator_test.go index bd28c0c26..6cb3dc924 100644 --- a/pkg/runtime/config/schema_validator_test.go +++ b/pkg/runtime/config/schema_validator_test.go @@ -2,6 +2,7 @@ package config import ( "os" + "strings" "testing" "github.com/windsorcli/cli/pkg/runtime/shell" @@ -132,6 +133,94 @@ type: object }) } +func TestSchemaValidator_injectSubstitutionSchema(t *testing.T) { + t.Run("InjectsSubstitutionSchema", func(t *testing.T) { + validator := NewSchemaValidator(nil) + schema := map[string]any{ + "properties": map[string]any{}, + } + + validator.injectSubstitutionSchema(&schema) + + properties, ok := schema["properties"].(map[string]any) + if !ok { + t.Fatal("Expected properties to be a map") + } + + substitutions, ok := properties["substitutions"].(map[string]any) + if !ok { + t.Fatal("Expected substitutions to be injected") + } + + if substitutions["type"] != "object" { + t.Errorf("Expected substitutions type to be object, got %v", substitutions["type"]) + } + }) + + t.Run("HandlesNilSchema", func(t *testing.T) { + validator := NewSchemaValidator(nil) + + validator.injectSubstitutionSchema(nil) + }) + + t.Run("CreatesPropertiesWhenMissing", func(t *testing.T) { + validator := NewSchemaValidator(nil) + schema := map[string]any{} + + validator.injectSubstitutionSchema(&schema) + + properties, ok := schema["properties"] + if !ok { + t.Fatal("Expected properties to be created") + } + + propertiesMap, ok := properties.(map[string]any) + if !ok { + t.Fatal("Expected properties to be a map") + } + + if _, ok := propertiesMap["substitutions"]; !ok { + t.Error("Expected substitutions to be injected") + } + }) + + t.Run("HandlesNonMapProperties", func(t *testing.T) { + validator := NewSchemaValidator(nil) + schema := map[string]any{ + "properties": "not_a_map", + } + + validator.injectSubstitutionSchema(&schema) + + properties := schema["properties"] + if properties != "not_a_map" { + t.Error("Expected properties to remain unchanged when not a map") + } + }) + + t.Run("ErrorWhenSchemaIsNil", func(t *testing.T) { + validator := NewSchemaValidator(nil) + validator.Schema = nil + + values := map[string]any{ + "provider": "generic", + } + + result, err := validator.Validate(values) + + if err == nil { + t.Error("Expected error when schema is nil") + } + if result != nil { + t.Error("Expected nil result when error occurs") + } + if !strings.Contains(err.Error(), "no schema loaded") { + t.Errorf("Expected error about no schema loaded, got: %v", err) + } + }) + +} + func TestSchemaValidator_ExtractDefaults(t *testing.T) { t.Run("SuccessSimpleDefaults", func(t *testing.T) { // Given a schema validator with loaded schema @@ -570,6 +659,34 @@ func TestSchemaValidator_GetSchemaDefaults(t *testing.T) { t.Error("Expected storage to not be included when no nested defaults") } }) + + t.Run("HandlesNestedObjectWithEmptyDefaults", func(t *testing.T) { + validator := NewSchemaValidator(nil) + validator.Schema = map[string]any{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "properties": map[string]any{ + "nested": map[string]any{ + "type": "object", + "properties": map[string]any{ + "key": map[string]any{ + "type": "string", + }, + }, + }, + }, + } + + defaults, err := validator.GetSchemaDefaults() + + if err != nil { + t.Fatalf("Expected no error, got: %v", err) + } + + if len(defaults) != 0 { + t.Errorf("Expected empty defaults for nested object without defaults, got: %v", defaults) + } + }) } func TestSchemaValidator_ValidateString(t *testing.T) { @@ -2187,6 +2304,49 @@ properties: } }) + t.Run("ValidateArrayWithNonArrayValue", func(t *testing.T) { + validator := NewSchemaValidator(nil) + validator.Schema = map[string]any{ + "type": "array", + "items": map[string]any{ + "type": "string", + }, + } + + errors := validator.validateArray("not_an_array", validator.Schema, "test") + + if len(errors) != 0 { + t.Errorf("Expected no errors for non-array value, got %v", errors) + } + }) + + t.Run("ValidateArrayWithoutItems", func(t *testing.T) { + validator := NewSchemaValidator(nil) + validator.Schema = map[string]any{ + "type": "array", + } + + errors := validator.validateArray([]any{"item1", "item2"}, validator.Schema, "test") + + if len(errors) != 0 { + t.Errorf("Expected no errors when items not in schema, got %v", errors) + } + }) + + t.Run("ValidateArrayWithNonMapItems", func(t *testing.T) { + validator := NewSchemaValidator(nil) + validator.Schema = map[string]any{ + "type": "array", + "items": "not_a_map", + } + + errors := validator.validateArray([]any{"item1", "item2"}, validator.Schema, "test") + + if len(errors) != 0 { + t.Errorf("Expected no errors when items is not a map, got %v", errors) + } + }) + t.Run("IntegerValidation", func(t *testing.T) { err := validator.LoadSchemaFromBytes([]byte(`$schema: https://json-schema.org/draft/2020-12/schema type: object From 74f6ee4a348a820ca18c8037b2ab68c2b4ffd90d Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:29:06 -0500 Subject: [PATCH 12/14] Windows fixes Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/shell/windows_shell_test.go | 92 +++++++++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/pkg/runtime/shell/windows_shell_test.go b/pkg/runtime/shell/windows_shell_test.go index 62c914601..0c10d30af 100644 --- a/pkg/runtime/shell/windows_shell_test.go +++ b/pkg/runtime/shell/windows_shell_test.go @@ -157,3 +157,95 @@ func TestDefaultShell_UnsetAlias(t *testing.T) { } }) } + +func TestDefaultShell_RenderEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *Mocks) { + t.Helper() + mocks := setupMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersEnvVarsWithExport", func(t *testing.T) { + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + result := shell.RenderEnvVars(envVars, true) + + if !strings.Contains(result, "$env:VAR1='value1'") { + t.Errorf("Expected PowerShell syntax, got: %s", result) + } + if !strings.Contains(result, "$env:VAR2='value2'") { + t.Errorf("Expected PowerShell syntax, got: %s", result) + } + }) + + t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + result := shell.RenderEnvVars(envVars, false) + + if !strings.Contains(result, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", result) + } + if !strings.Contains(result, "VAR2=value2") { + t.Errorf("Expected plain format, got: %s", result) + } + }) +} + +func TestDefaultShell_PrintEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *Mocks) { + t.Helper() + mocks := setupMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, true) + }) + + if !strings.Contains(output, "$env:VAR1='value1'") { + t.Errorf("Expected PowerShell syntax, got: %s", output) + } + if !strings.Contains(output, "$env:VAR2='value2'") { + t.Errorf("Expected PowerShell syntax, got: %s", output) + } + }) + + t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, false) + }) + + if !strings.Contains(output, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", output) + } + if !strings.Contains(output, "VAR2=value2") { + t.Errorf("Expected plain format, got: %s", output) + } + }) +} From 24e896317203424552d7af0c40f30c8bc166ba17 Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Mon, 17 Nov 2025 17:31:54 -0500 Subject: [PATCH 13/14] fix lint Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/shell/windows_shell_test.go | 92 ------------------------- 1 file changed, 92 deletions(-) diff --git a/pkg/runtime/shell/windows_shell_test.go b/pkg/runtime/shell/windows_shell_test.go index 0c10d30af..62c914601 100644 --- a/pkg/runtime/shell/windows_shell_test.go +++ b/pkg/runtime/shell/windows_shell_test.go @@ -157,95 +157,3 @@ func TestDefaultShell_UnsetAlias(t *testing.T) { } }) } - -func TestDefaultShell_RenderEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell() - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("RendersEnvVarsWithExport", func(t *testing.T) { - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - result := shell.RenderEnvVars(envVars, true) - - if !strings.Contains(result, "$env:VAR1='value1'") { - t.Errorf("Expected PowerShell syntax, got: %s", result) - } - if !strings.Contains(result, "$env:VAR2='value2'") { - t.Errorf("Expected PowerShell syntax, got: %s", result) - } - }) - - t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - result := shell.RenderEnvVars(envVars, false) - - if !strings.Contains(result, "VAR1=value1") { - t.Errorf("Expected plain format, got: %s", result) - } - if !strings.Contains(result, "VAR2=value2") { - t.Errorf("Expected plain format, got: %s", result) - } - }) -} - -func TestDefaultShell_PrintEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *Mocks) { - t.Helper() - mocks := setupMocks(t) - shell := NewDefaultShell() - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, true) - }) - - if !strings.Contains(output, "$env:VAR1='value1'") { - t.Errorf("Expected PowerShell syntax, got: %s", output) - } - if !strings.Contains(output, "$env:VAR2='value2'") { - t.Errorf("Expected PowerShell syntax, got: %s", output) - } - }) - - t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, false) - }) - - if !strings.Contains(output, "VAR1=value1") { - t.Errorf("Expected plain format, got: %s", output) - } - if !strings.Contains(output, "VAR2=value2") { - t.Errorf("Expected plain format, got: %s", output) - } - }) -} From 1e528bb49d2c35262ef2a241e2c304ca2f6fb6da Mon Sep 17 00:00:00 2001 From: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> Date: Tue, 18 Nov 2025 08:49:18 -0500 Subject: [PATCH 14/14] Fix windows tests Signed-off-by: Ryan VanGundy <85766511+rmvangun@users.noreply.github.com> --- pkg/runtime/shell/shell_test.go | 100 ---------------------- pkg/runtime/shell/unix_shell_test.go | 100 ++++++++++++++++++++++ pkg/runtime/shell/windows_shell_test.go | 107 ++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 100 deletions(-) diff --git a/pkg/runtime/shell/shell_test.go b/pkg/runtime/shell/shell_test.go index ddf914885..fd4d1c364 100644 --- a/pkg/runtime/shell/shell_test.go +++ b/pkg/runtime/shell/shell_test.go @@ -3282,106 +3282,6 @@ func TestScrubbingWriter(t *testing.T) { }) } -// TestDefaultShell_RenderEnvVars tests the RenderEnvVars method -func TestDefaultShell_RenderEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { - t.Helper() - mocks := setupShellMocks(t) - shell := NewDefaultShell() - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("RendersEnvVarsWithExport", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - // When rendering environment variables with export=true - result := shell.RenderEnvVars(envVars, true) - - // Then the output should contain export commands - if !strings.Contains(result, "export") { - t.Errorf("Expected export commands, got: %s", result) - } - }) - - t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - // When rendering environment variables with export=false - result := shell.RenderEnvVars(envVars, false) - - // Then the output should contain plain KEY=value format - if !strings.Contains(result, "VAR1=value1") { - t.Errorf("Expected plain format, got: %s", result) - } - if strings.Contains(result, "export") { - t.Errorf("Expected no export commands, got: %s", result) - } - }) -} - -// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method -func TestDefaultShell_PrintEnvVars(t *testing.T) { - setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { - t.Helper() - mocks := setupShellMocks(t) - shell := NewDefaultShell() - shell.shims = mocks.Shims - return shell, mocks - } - - t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - // When printing environment variables with export=true - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, true) - }) - - // Then the output should contain export commands - if !strings.Contains(output, "export") { - t.Errorf("Expected export commands, got: %s", output) - } - }) - - t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { - // Given a shell with environment variables - shell, _ := setup(t) - envVars := map[string]string{ - "VAR1": "value1", - "VAR2": "value2", - } - - // When printing environment variables with export=false - output := captureStdout(t, func() { - shell.PrintEnvVars(envVars, false) - }) - - // Then the output should contain plain KEY=value format - if !strings.Contains(output, "VAR1=value1") { - t.Errorf("Expected plain format, got: %s", output) - } - if strings.Contains(output, "export") { - t.Errorf("Expected no export commands, got: %s", output) - } - }) -} - // TestDefaultShell_renderEnvVarsPlain tests the renderEnvVarsPlain method func TestDefaultShell_renderEnvVarsPlain(t *testing.T) { setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { diff --git a/pkg/runtime/shell/unix_shell_test.go b/pkg/runtime/shell/unix_shell_test.go index 518421f3a..fc8bf1878 100644 --- a/pkg/runtime/shell/unix_shell_test.go +++ b/pkg/runtime/shell/unix_shell_test.go @@ -275,3 +275,103 @@ func TestDefaultShell_renderEnvVarsWithExport(t *testing.T) { } }) } + +// TestDefaultShell_RenderEnvVars tests the RenderEnvVars method on Unix systems +func TestDefaultShell_RenderEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=true + result := shell.RenderEnvVars(envVars, true) + + // Then the output should contain export commands + if !strings.Contains(result, "export") { + t.Errorf("Expected export commands, got: %s", result) + } + }) + + t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=false + result := shell.RenderEnvVars(envVars, false) + + // Then the output should contain plain KEY=value format + if !strings.Contains(result, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", result) + } + if strings.Contains(result, "export") { + t.Errorf("Expected no export commands, got: %s", result) + } + }) +} + +// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method on Unix systems +func TestDefaultShell_PrintEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=true + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, true) + }) + + // Then the output should contain export commands + if !strings.Contains(output, "export") { + t.Errorf("Expected export commands, got: %s", output) + } + }) + + t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=false + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, false) + }) + + // Then the output should contain plain KEY=value format + if !strings.Contains(output, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", output) + } + if strings.Contains(output, "export") { + t.Errorf("Expected no export commands, got: %s", output) + } + }) +} diff --git a/pkg/runtime/shell/windows_shell_test.go b/pkg/runtime/shell/windows_shell_test.go index 62c914601..0011db27f 100644 --- a/pkg/runtime/shell/windows_shell_test.go +++ b/pkg/runtime/shell/windows_shell_test.go @@ -6,6 +6,7 @@ package shell import ( "os" "path/filepath" + "strings" "testing" ) @@ -157,3 +158,109 @@ func TestDefaultShell_UnsetAlias(t *testing.T) { } }) } + +// TestDefaultShell_RenderEnvVars tests the RenderEnvVars method on Windows systems +func TestDefaultShell_RenderEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("RendersEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=true + result := shell.RenderEnvVars(envVars, true) + + // Then the output should contain PowerShell syntax + if !strings.Contains(result, "$env:VAR1='value1'") { + t.Errorf("Expected PowerShell syntax, got: %s", result) + } + if !strings.Contains(result, "$env:VAR2='value2'") { + t.Errorf("Expected PowerShell syntax, got: %s", result) + } + }) + + t.Run("RendersEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When rendering environment variables with export=false + result := shell.RenderEnvVars(envVars, false) + + // Then the output should contain plain KEY=value format + if !strings.Contains(result, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", result) + } + if !strings.Contains(result, "VAR2=value2") { + t.Errorf("Expected plain format, got: %s", result) + } + }) +} + +// TestDefaultShell_PrintEnvVars tests the PrintEnvVars method on Windows systems +func TestDefaultShell_PrintEnvVars(t *testing.T) { + setup := func(t *testing.T) (*DefaultShell, *ShellTestMocks) { + t.Helper() + mocks := setupShellMocks(t) + shell := NewDefaultShell() + shell.shims = mocks.Shims + return shell, mocks + } + + t.Run("PrintsEnvVarsWithExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=true + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, true) + }) + + // Then the output should contain PowerShell syntax + if !strings.Contains(output, "$env:VAR1='value1'") { + t.Errorf("Expected PowerShell syntax, got: %s", output) + } + if !strings.Contains(output, "$env:VAR2='value2'") { + t.Errorf("Expected PowerShell syntax, got: %s", output) + } + }) + + t.Run("PrintsEnvVarsWithoutExport", func(t *testing.T) { + // Given a shell with environment variables + shell, _ := setup(t) + envVars := map[string]string{ + "VAR1": "value1", + "VAR2": "value2", + } + + // When printing environment variables with export=false + output := captureStdout(t, func() { + shell.PrintEnvVars(envVars, false) + }) + + // Then the output should contain plain KEY=value format + if !strings.Contains(output, "VAR1=value1") { + t.Errorf("Expected plain format, got: %s", output) + } + if !strings.Contains(output, "VAR2=value2") { + t.Errorf("Expected plain format, got: %s", output) + } + }) +}