diff --git a/cmd/context.go b/cmd/context.go index d3875e816..022eae7f7 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -1,12 +1,11 @@ package cmd import ( - "context" "fmt" "github.com/spf13/cobra" "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/pipelines" + "github.com/windsorcli/cli/pkg/runtime" ) // getContextCmd represents the get command @@ -16,29 +15,21 @@ var getContextCmd = &cobra.Command{ Long: "Retrieve and display the current context from the configuration", SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context - injector := cmd.Context().Value(injectorKey).(di.Injector) + deps := &runtime.Dependencies{ + Injector: cmd.Context().Value(injectorKey).(di.Injector), + } - // Create output function outputFunc := func(output string) { fmt.Fprintln(cmd.OutOrStdout(), output) } - // Create execution context with operation and output function - ctx := context.WithValue(cmd.Context(), "operation", "get") - ctx = context.WithValue(ctx, "output", outputFunc) - - // Set up the context pipeline - pipeline, err := pipelines.WithPipeline(injector, ctx, "contextPipeline") - if err != nil { - return fmt.Errorf("failed to set up context pipeline: %w", err) - } - - // Execute the pipeline - if err := pipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing context pipeline: %w", err) + if err := runtime.NewRuntime(deps). + LoadShell(). + LoadConfigHandler(). + PrintContext(outputFunc). + Do(); err != nil { + return fmt.Errorf("Error getting context: %w", err) } - return nil }, } @@ -48,33 +39,20 @@ var setContextCmd = &cobra.Command{ Use: "set [context]", Short: "Set the current context", Long: "Set the current context in the configuration and save it", - Args: cobra.ExactArgs(1), // Ensure exactly one argument is provided + Args: cobra.ExactArgs(1), SilenceUsage: true, RunE: func(cmd *cobra.Command, args []string) error { - // Get shared dependency injector from context - injector := cmd.Context().Value(injectorKey).(di.Injector) - - // Create output function - outputFunc := func(output string) { - fmt.Fprintln(cmd.OutOrStdout(), output) - } - - // Create execution context with operation, context name, and output function - ctx := context.WithValue(cmd.Context(), "operation", "set") - ctx = context.WithValue(ctx, "contextName", args[0]) - ctx = context.WithValue(ctx, "output", outputFunc) - - // Set up the context pipeline - pipeline, err := pipelines.WithPipeline(injector, ctx, "contextPipeline") - if err != nil { - return fmt.Errorf("failed to set up context pipeline: %w", err) + deps := &runtime.Dependencies{ + Injector: cmd.Context().Value(injectorKey).(di.Injector), } - - // Execute the pipeline - if err := pipeline.Execute(ctx); err != nil { - return fmt.Errorf("Error executing context pipeline: %w", err) + if err := runtime.NewRuntime(deps). + LoadShell(). + LoadConfigHandler(). + WriteResetToken(). + SetContext(args[0]). + Do(); err != nil { + return fmt.Errorf("Error setting context: %w", err) } - return nil }, } @@ -97,7 +75,7 @@ var setContextAliasCmd = &cobra.Command{ Short: "Alias for 'context set'", SilenceUsage: true, Long: "Alias for 'context set'", - Args: cobra.ExactArgs(1), // Ensure exactly one argument is provided + Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { rootCmd.SetArgs(append([]string{"context", "set"}, args...)) return rootCmd.Execute() @@ -115,7 +93,6 @@ func init() { contextCmd.AddCommand(getContextCmd) contextCmd.AddCommand(setContextCmd) - // Add alias commands to rootCmd rootCmd.AddCommand(getContextAliasCmd) rootCmd.AddCommand(setContextAliasCmd) } diff --git a/cmd/context_test.go b/cmd/context_test.go index dca857108..cce0ed564 100644 --- a/cmd/context_test.go +++ b/cmd/context_test.go @@ -10,6 +10,15 @@ func TestContextCmd(t *testing.T) { setup := func(t *testing.T) (*bytes.Buffer, *bytes.Buffer) { t.Helper() + // Clear environment variables that could affect tests + origContext := os.Getenv("WINDSOR_CONTEXT") + os.Unsetenv("WINDSOR_CONTEXT") + t.Cleanup(func() { + if origContext != "" { + os.Setenv("WINDSOR_CONTEXT", origContext) + } + }) + // Change to a temporary directory without a config file origDir, err := os.Getwd() if err != nil { @@ -34,30 +43,33 @@ func TestContextCmd(t *testing.T) { return stdout, stderr } - t.Run("GetContextNoConfig", func(t *testing.T) { + t.Run("GetContext", func(t *testing.T) { // Given proper output capture in a directory without config - _, _ = setup(t) + stdout, _ := setup(t) + // Don't set up mocks - we want to test real behavior rootCmd.SetArgs([]string{"context", "get"}) // When executing the command err := Execute() - // Then an error should occur - if err == nil { - t.Error("Expected error, got nil") + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) } - // And error should contain init message - expectedError := "Error executing context pipeline: No context is available. Have you run `windsor init`?" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + // And should output default context (real behavior) + output := stdout.String() + expectedOutput := "local\n" + if output != expectedOutput { + t.Errorf("Expected output %q, got %q", expectedOutput, output) } }) t.Run("SetContextNoArgs", func(t *testing.T) { // Given proper output capture in a directory without config _, _ = setup(t) + setupMocks(t) rootCmd.SetArgs([]string{"context", "set"}) @@ -76,51 +88,48 @@ func TestContextCmd(t *testing.T) { } }) - t.Run("SetContextNoConfig", func(t *testing.T) { + t.Run("SetContext", func(t *testing.T) { // Given proper output capture in a directory without config _, _ = setup(t) + setupMocks(t) rootCmd.SetArgs([]string{"context", "set", "new-context"}) // When executing the command err := Execute() - // Then an error should occur - if err == nil { - t.Error("Expected error, got nil") - } - - // And error should contain init message - expectedError := "Error executing context pipeline: No context is available. Have you run `windsor init`?" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) - t.Run("GetContextAliasNoConfig", func(t *testing.T) { + t.Run("GetContextAlias", func(t *testing.T) { // Given proper output capture in a directory without config - _, _ = setup(t) + stdout, _ := setup(t) + // Don't set up mocks - we want to test real behavior rootCmd.SetArgs([]string{"get-context"}) // When executing the command err := Execute() - // Then an error should occur - if err == nil { - t.Error("Expected error, got nil") + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) } - // And error should contain init message - expectedError := "Error executing context pipeline: No context is available. Have you run `windsor init`?" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + // And should output the current context (may be "local" or previously set context) + output := stdout.String() + if output == "" { + t.Error("Expected some output, got empty string") } }) t.Run("SetContextAliasNoArgs", func(t *testing.T) { // Given proper output capture in a directory without config _, _ = setup(t) + setupMocks(t) rootCmd.SetArgs([]string{"set-context"}) @@ -139,24 +148,19 @@ func TestContextCmd(t *testing.T) { } }) - t.Run("SetContextAliasNoConfig", func(t *testing.T) { + t.Run("SetContextAlias", func(t *testing.T) { // Given proper output capture in a directory without config _, _ = setup(t) + setupMocks(t) rootCmd.SetArgs([]string{"set-context", "new-context"}) // When executing the command err := Execute() - // Then an error should occur - if err == nil { - t.Error("Expected error, got nil") - } - - // And error should contain init message - expectedError := "Error executing context pipeline: No context is available. Have you run `windsor init`?" - if err.Error() != expectedError { - t.Errorf("Expected error %q, got %q", expectedError, err.Error()) + // Then no error should occur + if err != nil { + t.Errorf("Expected no error, got %v", err) } }) } diff --git a/pkg/pipelines/context.go b/pkg/pipelines/context.go deleted file mode 100644 index c6ea28424..000000000 --- a/pkg/pipelines/context.go +++ /dev/null @@ -1,126 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" -) - -// The ContextPipeline is a specialized component that manages context operations functionality. -// It provides context-specific command execution including context getting and setting operations, -// configuration validation, and shell integration for the Windsor CLI context command. -// The ContextPipeline handles context management operations with proper initialization and validation. - -// ============================================================================= -// Types -// ============================================================================= - -// ContextPipeline provides context management functionality -// It embeds BasePipeline and manages context-specific dependencies -// for Windsor CLI context operations. -type ContextPipeline struct { - BasePipeline -} - -// ============================================================================= -// Constructor -// ============================================================================= - -// NewContextPipeline creates a new ContextPipeline instance -func NewContextPipeline() *ContextPipeline { - return &ContextPipeline{ - BasePipeline: *NewBasePipeline(), - } -} - -// ============================================================================= -// Public Methods -// ============================================================================= - -// Execute runs the context management logic based on the operation type. -// It supports both "get" and "set" operations specified through the context. -// For "get" operations, it returns the current context. -// For "set" operations, it sets the context and writes a reset token. -func (p *ContextPipeline) Execute(ctx context.Context) error { - operation := ctx.Value("operation") - if operation == nil { - return fmt.Errorf("no operation specified") - } - - operationStr, ok := operation.(string) - if !ok { - return fmt.Errorf("invalid operation type") - } - - switch operationStr { - case "get": - return p.executeGet(ctx) - case "set": - return p.executeSet(ctx) - default: - return fmt.Errorf("unsupported operation: %s", operationStr) - } -} - -// ============================================================================= -// Private Methods -// ============================================================================= - -// executeGet handles the context get operation by checking if config is loaded -// and returning the current context name. -func (p *ContextPipeline) executeGet(ctx context.Context) error { - if !p.configHandler.IsLoaded() { - return fmt.Errorf("No context is available. Have you run `windsor init`?") - } - - currentContext := p.configHandler.GetContext() - - output := ctx.Value("output") - if output != nil { - if outputFunc, ok := output.(func(string)); ok { - outputFunc(currentContext) - } - } - - return nil -} - -// executeSet handles the context set operation by validating the context name, -// setting the context, and writing a reset token. -func (p *ContextPipeline) executeSet(ctx context.Context) error { - contextName := ctx.Value("contextName") - if contextName == nil { - return fmt.Errorf("no context name provided") - } - - contextNameStr, ok := contextName.(string) - if !ok { - return fmt.Errorf("invalid context name type") - } - - if !p.configHandler.IsLoaded() { - return fmt.Errorf("No context is available. Have you run `windsor init`?") - } - - if _, err := p.shell.WriteResetToken(); err != nil { - return fmt.Errorf("Error writing reset token: %w", err) - } - - if err := p.configHandler.SetContext(contextNameStr); err != nil { - return fmt.Errorf("Error setting context: %w", err) - } - - output := ctx.Value("output") - if output != nil { - if outputFunc, ok := output.(func(string)); ok { - outputFunc(fmt.Sprintf("Context set to: %s", contextNameStr)) - } - } - - return nil -} - -// ============================================================================= -// Interface Compliance -// ============================================================================= - -var _ Pipeline = (*ContextPipeline)(nil) diff --git a/pkg/pipelines/context_test.go b/pkg/pipelines/context_test.go deleted file mode 100644 index 3ddebcebb..000000000 --- a/pkg/pipelines/context_test.go +++ /dev/null @@ -1,525 +0,0 @@ -package pipelines - -import ( - "context" - "fmt" - "os" - "testing" - - "github.com/windsorcli/cli/pkg/config" - "github.com/windsorcli/cli/pkg/di" - "github.com/windsorcli/cli/pkg/shell" -) - -// ============================================================================= -// Test Setup -// ============================================================================= - -type ContextMocks struct { - Injector di.Injector - ConfigHandler *config.MockConfigHandler - Shell *shell.MockShell - Shims *Shims -} - -type ContextSetupOptions struct { - Injector di.Injector - ConfigHandler config.ConfigHandler - ConfigStr string -} - -func setupContextShims(t *testing.T) *Shims { - t.Helper() - shims := NewShims() - - shims.Stat = func(name string) (os.FileInfo, error) { - return nil, os.ErrNotExist - } - - shims.Getenv = func(key string) string { - return "" - } - - shims.Setenv = func(key, value string) error { - return nil - } - - return shims -} - -func setupContextMocks(t *testing.T, opts ...*ContextSetupOptions) *ContextMocks { - t.Helper() - - origDir, err := os.Getwd() - if err != nil { - t.Fatalf("Failed to get working directory: %v", err) - } - - tmpDir := t.TempDir() - if err := os.Chdir(tmpDir); err != nil { - t.Fatalf("Failed to change to temp directory: %v", err) - } - - os.Setenv("WINDSOR_PROJECT_ROOT", tmpDir) - - var options *ContextSetupOptions - if len(opts) > 0 { - options = opts[0] - } - if options == nil { - options = &ContextSetupOptions{} - } - - var injector di.Injector - if options.Injector == nil { - injector = di.NewInjector() - } else { - injector = options.Injector - } - - mockShell := shell.NewMockShell() - mockShell.GetProjectRootFunc = func() (string, error) { - return tmpDir, nil - } - mockShell.InitializeFunc = func() error { - return nil - } - mockShell.WriteResetTokenFunc = func() (string, error) { - return "reset-token", nil - } - injector.Register("shell", mockShell) - - var configHandler *config.MockConfigHandler - if options.ConfigHandler == nil { - configHandler = config.NewMockConfigHandler() - } else { - if mockHandler, ok := options.ConfigHandler.(*config.MockConfigHandler); ok { - configHandler = mockHandler - } else { - configHandler = config.NewMockConfigHandler() - } - } - injector.Register("configHandler", configHandler) - - if options.ConfigStr != "" { - if err := configHandler.LoadConfigString(options.ConfigStr); err != nil { - t.Fatalf("Failed to load config string: %v", err) - } - } - configHandler.Initialize() - - shims := setupContextShims(t) - - t.Cleanup(func() { - os.Unsetenv("WINDSOR_PROJECT_ROOT") - if err := os.Chdir(origDir); err != nil { - t.Logf("Warning: Failed to change back to original directory: %v", err) - } - }) - - return &ContextMocks{ - Injector: injector, - ConfigHandler: configHandler, - Shell: mockShell, - Shims: shims, - } -} - -func setupContextPipeline(t *testing.T, mocks *ContextMocks) *ContextPipeline { - t.Helper() - - mocks.Injector.Register("configHandler", mocks.ConfigHandler) - mocks.Injector.Register("shell", mocks.Shell) - mocks.Injector.Register("shims", mocks.Shims) - - return NewContextPipeline() -} - -// ============================================================================= -// Test Constructor -// ============================================================================= - -func TestNewContextPipeline(t *testing.T) { - t.Run("CreatesWithDefaults", func(t *testing.T) { - pipeline := NewContextPipeline() - - if pipeline == nil { - t.Fatal("Expected pipeline to not be nil") - } - }) - - t.Run("InitializesCorrectly", func(t *testing.T) { - injector := di.NewMockInjector() - - pipeline := NewContextPipeline() - - err := pipeline.Initialize(injector, context.Background()) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - - if pipeline.shell == nil { - t.Error("Expected shell to be initialized") - } - if pipeline.configHandler == nil { - t.Error("Expected configHandler to be initialized") - } - if pipeline.shims == nil { - t.Error("Expected shims to be initialized") - } - }) -} - -// ============================================================================= -// Test Public Methods -// ============================================================================= - -func TestContextPipeline_Initialize(t *testing.T) { - setup := func(t *testing.T, opts ...*ContextSetupOptions) (*ContextPipeline, *ContextMocks) { - t.Helper() - mocks := setupContextMocks(t, opts...) - pipeline := setupContextPipeline(t, mocks) - return pipeline, mocks - } - - t.Run("Success", func(t *testing.T) { - pipeline, _ := setup(t, &ContextSetupOptions{ - ConfigStr: ` -contexts: - test-context: - name: "Test Context" -`, - }) - - err := pipeline.Initialize(di.NewMockInjector(), context.Background()) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigHandlerInitializeFails", func(t *testing.T) { - mockConfigHandler := config.NewMockConfigHandler() - mockConfigHandler.InitializeFunc = func() error { - return fmt.Errorf("config initialization failed") - } - - pipeline, mocks := setup(t, &ContextSetupOptions{ - Injector: di.NewMockInjector(), - ConfigHandler: mockConfigHandler, - }) - - err := pipeline.Initialize(mocks.Injector, context.Background()) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if !contextContains(err.Error(), "failed to initialize config handler") { - t.Errorf("Expected config handler error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenShellInitializeFails", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.Shell.InitializeFunc = func() error { - return fmt.Errorf("shell initialization failed") - } - - err := pipeline.Initialize(mocks.Injector, context.Background()) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if !contextContains(err.Error(), "failed to initialize shell") { - t.Errorf("Expected shell error, got: %v", err) - } - }) - -} - -func TestContextPipeline_Execute(t *testing.T) { - setup := func(t *testing.T, opts ...*ContextSetupOptions) (*ContextPipeline, *ContextMocks) { - t.Helper() - mocks := setupContextMocks(t, opts...) - pipeline := setupContextPipeline(t, mocks) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - return pipeline, mocks - } - - t.Run("ReturnsErrorWhenNoOperationSpecified", func(t *testing.T) { - pipeline, _ := setup(t) - - err := pipeline.Execute(context.Background()) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "no operation specified" { - t.Errorf("Expected 'no operation specified', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenInvalidOperationType", func(t *testing.T) { - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "operation", 123) - err := pipeline.Execute(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "invalid operation type" { - t.Errorf("Expected 'invalid operation type', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenUnsupportedOperation", func(t *testing.T) { - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "operation", "unsupported") - err := pipeline.Execute(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "unsupported operation: unsupported" { - t.Errorf("Expected 'unsupported operation: unsupported', got: %v", err) - } - }) - - t.Run("ExecutesGetOperationSuccessfully", func(t *testing.T) { - pipeline, mocks := setup(t, &ContextSetupOptions{ - ConfigStr: ` -contexts: - test-context: - name: "Test Context" -`, - }) - - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.ConfigHandler.GetContextFunc = func() string { return "test-context" } - - var capturedOutput string - outputFunc := func(output string) { - capturedOutput = output - } - - ctx := context.WithValue(context.Background(), "operation", "get") - ctx = context.WithValue(ctx, "output", outputFunc) - err := pipeline.Execute(ctx) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if capturedOutput != "test-context" { - t.Errorf("Expected 'test-context', got: %v", capturedOutput) - } - }) - - t.Run("ExecutesSetOperationSuccessfully", func(t *testing.T) { - pipeline, mocks := setup(t, &ContextSetupOptions{ - ConfigStr: ` -contexts: - test-context: - name: "Test Context" - new-context: - name: "New Context" -`, - }) - - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.ConfigHandler.SetContextFunc = func(context string) error { return nil } - - var capturedOutput string - outputFunc := func(output string) { - capturedOutput = output - } - - ctx := context.WithValue(context.Background(), "operation", "set") - ctx = context.WithValue(ctx, "contextName", "new-context") - ctx = context.WithValue(ctx, "output", outputFunc) - err := pipeline.Execute(ctx) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if capturedOutput != "Context set to: new-context" { - t.Errorf("Expected 'Context set to: new-context', got: %v", capturedOutput) - } - }) -} - -func TestContextPipeline_executeGet(t *testing.T) { - setup := func(t *testing.T, opts ...*ContextSetupOptions) (*ContextPipeline, *ContextMocks) { - t.Helper() - mocks := setupContextMocks(t, opts...) - pipeline := setupContextPipeline(t, mocks) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - return pipeline, mocks - } - - t.Run("ReturnsErrorWhenConfigNotLoaded", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return false } - - err := pipeline.executeGet(context.Background()) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No context is available. Have you run `windsor init`?" { - t.Errorf("Expected context not available error, got: %v", err) - } - }) - - t.Run("ReturnsCurrentContextSuccessfully", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.ConfigHandler.GetContextFunc = func() string { return "current-context" } - - var capturedOutput string - outputFunc := func(output string) { - capturedOutput = output - } - - ctx := context.WithValue(context.Background(), "output", outputFunc) - err := pipeline.executeGet(ctx) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if capturedOutput != "current-context" { - t.Errorf("Expected 'current-context', got: %v", capturedOutput) - } - }) -} - -func TestContextPipeline_executeSet(t *testing.T) { - setup := func(t *testing.T, opts ...*ContextSetupOptions) (*ContextPipeline, *ContextMocks) { - t.Helper() - mocks := setupContextMocks(t, opts...) - pipeline := setupContextPipeline(t, mocks) - err := pipeline.Initialize(mocks.Injector, context.Background()) - if err != nil { - t.Fatalf("Failed to initialize pipeline: %v", err) - } - return pipeline, mocks - } - - t.Run("ReturnsErrorWhenNoContextNameProvided", func(t *testing.T) { - pipeline, _ := setup(t) - - err := pipeline.executeSet(context.Background()) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "no context name provided" { - t.Errorf("Expected 'no context name provided', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenInvalidContextNameType", func(t *testing.T) { - pipeline, _ := setup(t) - - ctx := context.WithValue(context.Background(), "contextName", 123) - err := pipeline.executeSet(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "invalid context name type" { - t.Errorf("Expected 'invalid context name type', got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenConfigNotLoaded", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return false } - - ctx := context.WithValue(context.Background(), "contextName", "test-context") - err := pipeline.executeSet(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if err.Error() != "No context is available. Have you run `windsor init`?" { - t.Errorf("Expected context not available error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenWriteResetTokenFails", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.Shell.WriteResetTokenFunc = func() (string, error) { - return "", fmt.Errorf("write reset token failed") - } - - ctx := context.WithValue(context.Background(), "contextName", "test-context") - err := pipeline.executeSet(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if !contextContains(err.Error(), "Error writing reset token") { - t.Errorf("Expected reset token error, got: %v", err) - } - }) - - t.Run("ReturnsErrorWhenSetContextFails", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.ConfigHandler.SetContextFunc = func(context string) error { - return fmt.Errorf("set context failed") - } - - ctx := context.WithValue(context.Background(), "contextName", "test-context") - err := pipeline.executeSet(ctx) - - if err == nil { - t.Fatal("Expected error, got nil") - } - if !contextContains(err.Error(), "Error setting context") { - t.Errorf("Expected set context error, got: %v", err) - } - }) - - t.Run("SetsContextSuccessfully", func(t *testing.T) { - pipeline, mocks := setup(t) - mocks.ConfigHandler.IsLoadedFunc = func() bool { return true } - mocks.ConfigHandler.SetContextFunc = func(context string) error { return nil } - - var capturedOutput string - outputFunc := func(output string) { - capturedOutput = output - } - - ctx := context.WithValue(context.Background(), "contextName", "new-context") - ctx = context.WithValue(ctx, "output", outputFunc) - err := pipeline.executeSet(ctx) - - if err != nil { - t.Errorf("Expected no error, got %v", err) - } - if capturedOutput != "Context set to: new-context" { - t.Errorf("Expected 'Context set to: new-context', got: %v", capturedOutput) - } - }) -} - -// ============================================================================= -// Test Helpers -// ============================================================================= - -func contextContains(s, substr string) bool { - return len(s) >= len(substr) && (s == substr || (len(s) > len(substr) && - (s[:len(substr)] == substr || s[len(s)-len(substr):] == substr || - contextContains(s[1:len(s)-1], substr)))) -} diff --git a/pkg/pipelines/pipeline.go b/pkg/pipelines/pipeline.go index f327f756c..c17bedb45 100644 --- a/pkg/pipelines/pipeline.go +++ b/pkg/pipelines/pipeline.go @@ -54,7 +54,6 @@ var pipelineConstructors = map[string]PipelineConstructor{ "envPipeline": func() Pipeline { return NewEnvPipeline() }, "initPipeline": func() Pipeline { return NewInitPipeline() }, "execPipeline": func() Pipeline { return NewExecPipeline() }, - "contextPipeline": func() Pipeline { return NewContextPipeline() }, "checkPipeline": func() Pipeline { return NewCheckPipeline() }, "upPipeline": func() Pipeline { return NewUpPipeline() }, "downPipeline": func() Pipeline { return NewDownPipeline() }, diff --git a/pkg/pipelines/pipeline_test.go b/pkg/pipelines/pipeline_test.go index b896ef930..190aa8262 100644 --- a/pkg/pipelines/pipeline_test.go +++ b/pkg/pipelines/pipeline_test.go @@ -527,7 +527,6 @@ func TestWithPipeline(t *testing.T) { {"EnvPipeline", "envPipeline"}, {"InitPipeline", "initPipeline"}, {"ExecPipeline", "execPipeline"}, - {"ContextPipeline", "contextPipeline"}, {"CheckPipeline", "checkPipeline"}, {"UpPipeline", "upPipeline"}, {"DownPipeline", "downPipeline"}, @@ -726,7 +725,6 @@ func TestWithPipeline(t *testing.T) { "envPipeline", "initPipeline", "execPipeline", - "contextPipeline", "checkPipeline", "basePipeline", } diff --git a/pkg/runtime/runtime.go b/pkg/runtime/runtime.go index cebebf371..c55164397 100644 --- a/pkg/runtime/runtime.go +++ b/pkg/runtime/runtime.go @@ -116,6 +116,26 @@ func (r *Runtime) LoadShell() *Runtime { return r } +// LoadConfigHandler loads and initializes the configuration handler dependency. +func (r *Runtime) LoadConfigHandler() *Runtime { + if r.err != nil { + return r + } + if r.Shell == nil { + r.err = fmt.Errorf("shell not loaded - call LoadShell() first") + return r + } + + if r.ConfigHandler == nil { + r.ConfigHandler = config.NewConfigHandler(r.Injector) + if err := r.ConfigHandler.Initialize(); err != nil { + r.err = fmt.Errorf("failed to initialize config handler: %w", err) + return r + } + } + return r +} + // InstallHook installs a shell hook for the specified shell type. func (r *Runtime) InstallHook(shellType string) *Runtime { if r.err != nil { @@ -128,3 +148,43 @@ func (r *Runtime) InstallHook(shellType string) *Runtime { r.err = r.Shell.InstallHook(shellType) return r } + +// SetContext sets the context for the configuration handler. +func (r *Runtime) SetContext(context string) *Runtime { + if r.err != nil { + return r + } + if r.ConfigHandler == nil { + r.err = fmt.Errorf("config handler not loaded - call LoadConfigHandler() first") + return r + } + r.err = r.ConfigHandler.SetContext(context) + return r +} + +// PrintContext outputs the current context using the provided output function. +func (r *Runtime) PrintContext(outputFunc func(string)) *Runtime { + if r.err != nil { + return r + } + if r.ConfigHandler == nil { + r.err = fmt.Errorf("config handler not loaded - call LoadConfigHandler() first") + return r + } + context := r.ConfigHandler.GetContext() + outputFunc(context) + return r +} + +// WriteResetToken writes a session/token reset file using the shell. +func (r *Runtime) WriteResetToken() *Runtime { + if r.err != nil { + return r + } + if r.Shell == nil { + r.err = fmt.Errorf("shell not loaded - call LoadShell() first") + return r + } + _, r.err = r.Shell.WriteResetToken() + return r +} diff --git a/pkg/runtime/runtime_test.go b/pkg/runtime/runtime_test.go index 415ec769a..d9657d62b 100644 --- a/pkg/runtime/runtime_test.go +++ b/pkg/runtime/runtime_test.go @@ -2,8 +2,10 @@ package runtime import ( "errors" + "strings" "testing" + "github.com/windsorcli/cli/pkg/config" "github.com/windsorcli/cli/pkg/di" "github.com/windsorcli/cli/pkg/shell" ) @@ -25,6 +27,7 @@ func setupMocks(t *testing.T) *Dependencies { return &Dependencies{ Injector: di.NewInjector(), Shell: shell.NewMockShell(), + ConfigHandler: config.NewMockConfigHandler(), } } @@ -130,6 +133,104 @@ func TestRuntime_LoadShell(t *testing.T) { }) } +func TestRuntime_LoadConfigHandler(t *testing.T) { + t.Run("LoadsConfigHandlerSuccessfully", func(t *testing.T) { + // Given a runtime with loaded shell + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell() + + // When loading config handler + result := runtime.LoadConfigHandler() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected LoadConfigHandler to return the same runtime instance") + } + + // And config handler should be loaded + if runtime.ConfigHandler == nil { + t.Error("Expected config handler to be loaded") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + }) + + t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { + // Given a runtime without loaded shell (no pre-loaded dependencies) + runtime := NewRuntime() + + // When loading config handler + result := runtime.LoadConfigHandler() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected LoadConfigHandler to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error when shell not loaded") + } + + expectedError := "shell not loaded - call LoadShell() first" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error (no pre-loaded dependencies) + runtime := NewRuntime() + runtime.err = errors.New("existing error") + + // When loading config handler + result := runtime.LoadConfigHandler() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected LoadConfigHandler to return the same runtime instance") + } + + // And config handler should not be loaded + if runtime.ConfigHandler != nil { + t.Error("Expected config handler to not be loaded when error exists") + } + + // And original error should be preserved + if runtime.err.Error() != "existing error" { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("PropagatesConfigHandlerInitializationError", func(t *testing.T) { + // Given a runtime with an injector that cannot resolve the shell + runtime := NewRuntime() + runtime.Shell = shell.NewMockShell() + // Don't register the shell in the injector - this will cause initialization to fail + + // When loading config handler + result := runtime.LoadConfigHandler() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected LoadConfigHandler to return the same runtime instance") + } + + // And error should be set from initialization failure + if runtime.err == nil { + t.Error("Expected error from config handler initialization failure") + } else { + expectedError := "failed to initialize config handler" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) +} + func TestRuntime_InstallHook(t *testing.T) { t.Run("InstallsHookSuccessfully", func(t *testing.T) { // Given a runtime with loaded shell @@ -193,6 +294,299 @@ func TestRuntime_InstallHook(t *testing.T) { }) } +func TestRuntime_SetContext(t *testing.T) { + t.Run("SetsContextSuccessfully", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + // When setting context + result := runtime.SetContext("test-context") + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected SetContext to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And SetContext should have been called on the config handler + // (We can't easily track this without modifying the mock, so we just verify no error occurred) + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler (no pre-loaded dependencies) + runtime := NewRuntime() + + // When setting context + result := runtime.SetContext("test-context") + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected SetContext to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error when config handler not loaded") + } + + expectedError := "config handler not loaded - call LoadConfigHandler() first" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error (no pre-loaded dependencies) + runtime := NewRuntime() + runtime.err = errors.New("existing error") + + // When setting context + result := runtime.SetContext("test-context") + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected SetContext to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err.Error() != "existing error" { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("PropagatesConfigHandlerError", func(t *testing.T) { + // Given a runtime with a mock shell that returns an error + mockShell := shell.NewMockShell() + mockShell.GetProjectRootFunc = func() (string, error) { + return "", errors.New("project root error") + } + + // Create runtime with only the mock shell, no mock config handler + runtime := NewRuntime() + runtime.Shell = mockShell + runtime.Injector.Register("shell", mockShell) + runtime.LoadConfigHandler() + + // When setting context + result := runtime.SetContext("test-context") + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected SetContext to return the same runtime instance") + } + + // And error should be propagated from config handler + if runtime.err == nil { + t.Error("Expected error to be propagated from config handler") + } else { + expectedError := "error getting project root" + if !strings.Contains(runtime.err.Error(), expectedError) { + t.Errorf("Expected error to contain %q, got %q", expectedError, runtime.err.Error()) + } + } + }) +} + +func TestRuntime_PrintContext(t *testing.T) { + t.Run("PrintsContextSuccessfully", func(t *testing.T) { + // Given a runtime with loaded config handler + mocks := setupMocks(t) + mocks.ConfigHandler.(*config.MockConfigHandler).GetContextFunc = func() string { + return "test-context" + } + runtime := NewRuntime(mocks).LoadShell().LoadConfigHandler() + + var output string + outputFunc := func(s string) { + output = s + } + + // When printing context + result := runtime.PrintContext(outputFunc) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintContext to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And output should be correct + if output != "test-context" { + t.Errorf("Expected output 'test-context', got %q", output) + } + + // And GetContext should have been called on the config handler + // (We can't easily track this without modifying the mock, so we just verify the output is correct) + }) + + t.Run("ReturnsErrorWhenConfigHandlerNotLoaded", func(t *testing.T) { + // Given a runtime without loaded config handler (no pre-loaded dependencies) + runtime := NewRuntime() + + var output string + outputFunc := func(s string) { + output = s + } + + // When printing context + result := runtime.PrintContext(outputFunc) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintContext to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error when config handler not loaded") + } + + expectedError := "config handler not loaded - call LoadConfigHandler() first" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + + // And output should not be set + if output != "" { + t.Errorf("Expected no output, got %q", output) + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error (no pre-loaded dependencies) + runtime := NewRuntime() + runtime.err = errors.New("existing error") + + var output string + outputFunc := func(s string) { + output = s + } + + // When printing context + result := runtime.PrintContext(outputFunc) + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected PrintContext to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err.Error() != "existing error" { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + + // And output should not be set + if output != "" { + t.Errorf("Expected no output, got %q", output) + } + }) +} + +func TestRuntime_WriteResetToken(t *testing.T) { + t.Run("WritesResetTokenSuccessfully", func(t *testing.T) { + // Given a runtime with loaded shell + mocks := setupMocks(t) + mocks.Shell.(*shell.MockShell).WriteResetTokenFunc = func() (string, error) { + return "/tmp/reset-token", nil + } + runtime := NewRuntime(mocks).LoadShell() + + // When writing reset token + result := runtime.WriteResetToken() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WriteResetToken to return the same runtime instance") + } + + // And no error should be set + if runtime.err != nil { + t.Errorf("Expected no error, got %v", runtime.err) + } + + // And WriteResetToken should have been called on the shell + // (We can't easily track this without modifying the mock, so we just verify no error occurred) + }) + + t.Run("ReturnsErrorWhenShellNotLoaded", func(t *testing.T) { + // Given a runtime without loaded shell (no pre-loaded dependencies) + runtime := NewRuntime() + + // When writing reset token + result := runtime.WriteResetToken() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WriteResetToken to return the same runtime instance") + } + + // And error should be set + if runtime.err == nil { + t.Error("Expected error when shell not loaded") + } + + expectedError := "shell not loaded - call LoadShell() first" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + }) + + t.Run("ReturnsEarlyOnExistingError", func(t *testing.T) { + // Given a runtime with an existing error (no pre-loaded dependencies) + runtime := NewRuntime() + runtime.err = errors.New("existing error") + + // When writing reset token + result := runtime.WriteResetToken() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WriteResetToken to return the same runtime instance") + } + + // And original error should be preserved + if runtime.err.Error() != "existing error" { + t.Errorf("Expected original error to be preserved, got %v", runtime.err) + } + }) + + t.Run("PropagatesShellError", func(t *testing.T) { + // Given a runtime with loaded shell that returns an error + mocks := setupMocks(t) + mocks.Shell.(*shell.MockShell).WriteResetTokenFunc = func() (string, error) { + return "", errors.New("shell error") + } + runtime := NewRuntime(mocks).LoadShell() + + // When writing reset token + result := runtime.WriteResetToken() + + // Then should return the same runtime instance + if result != runtime { + t.Error("Expected WriteResetToken to return the same runtime instance") + } + + // And error should be propagated + if runtime.err == nil { + t.Error("Expected error to be propagated from shell") + } else { + expectedError := "shell error" + if runtime.err.Error() != expectedError { + t.Errorf("Expected error %q, got %q", expectedError, runtime.err.Error()) + } + } + }) +} + func TestRuntime_Do(t *testing.T) { t.Run("ReturnsNilWhenNoError", func(t *testing.T) { // Given a runtime with no error