package e2e import ( "os" "path/filepath" "testing" "github.com/hanzoai/cd/gitops-engine/pkg/health" . "github.com/hanzoai/cd/gitops-engine/pkg/sync/common" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" . "github.com/hanzoai/cd/pkg/apis/application/v1alpha1" . "github.com/hanzoai/cd/test/e2e/fixture" . "github.com/hanzoai/cd/test/e2e/fixture/app" ) // createTestPlugin creates a temporary Argo CD CLI plugin script for testing purposes. // The script is written to a temporary directory with executable permissions. func createTestPlugin(t *testing.T, name, content string) string { t.Helper() tmpDir := t.TempDir() pluginPath := filepath.Join(tmpDir, "cd-"+name) require.NoError(t, os.WriteFile(pluginPath, []byte(content), 0o755)) // Ensure the plugin is cleaned up properly t.Cleanup(func() { _ = os.Remove(pluginPath) }) return pluginPath } // TestCliAppCommand verifies the basic Argo CD CLI commands for app synchronization and listing. func TestCliAppCommand(t *testing.T) { ctx := Given(t) ctx.Path("hook"). When(). CreateApp(). And(func() { output, err := RunCli("app", "sync", ctx.AppName(), "--timeout", "90") require.NoError(t, err) vars := map[string]any{"Name": ctx.AppName(), "Namespace": ctx.DeploymentNamespace()} assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} pod Synced Progressing pod/pod created`, vars)) assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} hook Succeeded Synced Sync pod/hook created`, vars)) }). Then(). Expect(OperationPhaseIs(OperationSucceeded)). Expect(HealthIs(health.HealthStatusHealthy)). And(func(a *Application) { output, err := RunCli("app", "list") require.NoError(t, err) expected := Tmpl( t, `{{.Name}} https://kubernetes.default.svc {{.Namespace}} default Synced Healthy Manual `, map[string]any{"Name": a.GetName(), "Namespace": ctx.DeploymentNamespace()}) assert.Contains(t, NormalizeOutput(output), expected) }) } // TestNormalArgoCDCommandsExecuteOverPluginsWithSameName verifies that normal Argo CD CLI commands // take precedence over plugins with the same name when both exist in the path. func TestNormalArgoCDCommandsExecuteOverPluginsWithSameName(t *testing.T) { pluginScript := `#!/bin/bash echo "I am a plugin, not Argo CD!" exit 0` pluginPath := createTestPlugin(t, "app", pluginScript) origPath := os.Getenv("PATH") t.Cleanup(func() { t.Setenv("PATH", origPath) }) t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) ctx := Given(t) ctx.Path("hook"). Path("hook"). When(). CreateApp(). And(func() { output, err := RunCli("app", "sync", ctx.AppName(), "--timeout", "90") require.NoError(t, err) assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!") vars := map[string]any{"Name": ctx.AppName(), "Namespace": ctx.DeploymentNamespace()} assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} pod Synced Progressing pod/pod created`, vars)) assert.Contains(t, NormalizeOutput(output), Tmpl(t, `Pod {{.Namespace}} hook Succeeded Synced Sync pod/hook created`, vars)) }). Then(). Expect(OperationPhaseIs(OperationSucceeded)). Expect(HealthIs(health.HealthStatusHealthy)). And(func(_ *Application) { output, err := RunCli("app", "list") require.NoError(t, err) assert.NotContains(t, NormalizeOutput(output), "I am a plugin, not Argo CD!") expected := Tmpl( t, `{{.Name}} https://kubernetes.default.svc {{.Namespace}} default Synced Healthy Manual `, map[string]any{"Name": ctx.AppName(), "Namespace": ctx.DeploymentNamespace()}) assert.Contains(t, NormalizeOutput(output), expected) }) } // TestCliPluginExecution tests the execution of a valid Argo CD CLI plugin. func TestCliPluginExecution(t *testing.T) { pluginScript := `#!/bin/bash echo "Hello from myplugin" exit 0` pluginPath := createTestPlugin(t, "myplugin", pluginScript) origPath := os.Getenv("PATH") t.Cleanup(func() { t.Setenv("PATH", origPath) }) t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) output, err := RunPluginCli("", "myplugin") require.NoError(t, err) assert.Contains(t, NormalizeOutput(output), "Hello from myplugin") } // TestCliPluginExecutionConditions tests for plugin execution conditions func TestCliPluginExecutionConditions(t *testing.T) { createValidPlugin := func(t *testing.T, name string, executable bool) string { t.Helper() script := `#!/bin/bash echo "Hello from $0" exit 0 ` pluginPath := createTestPlugin(t, name, script) if executable { require.NoError(t, os.Chmod(pluginPath, 0o755)) } else { require.NoError(t, os.Chmod(pluginPath, 0o644)) } return pluginPath } createInvalidPlugin := func(t *testing.T, name string) string { t.Helper() script := `#!/bin/bash echo "Hello from $0" exit 0 ` tmpDir := t.TempDir() pluginPath := filepath.Join(tmpDir, "cd_"+name) // this is an invalid plugin name format require.NoError(t, os.WriteFile(pluginPath, []byte(script), 0o755)) return pluginPath } // 'cd-valid-plugin' is a valid plugin name validPlugin := createValidPlugin(t, "valid-plugin", true) // 'cd_invalid-plugin' is an invalid plugin name invalidPlugin := createInvalidPlugin(t, "invalid-plugin") // 'cd-nonexec-plugin' is a valid plugin name but lacks executable permissions noExecPlugin := createValidPlugin(t, "noexec-plugin", false) origPath := os.Getenv("PATH") defer func() { t.Setenv("PATH", origPath) }() t.Setenv("PATH", filepath.Dir(validPlugin)+":"+filepath.Dir(invalidPlugin)+":"+filepath.Dir(noExecPlugin)+":"+origPath) output, err := RunPluginCli("", "valid-plugin") require.NoError(t, err) assert.Contains(t, NormalizeOutput(output), "Hello from") _, err = RunPluginCli("", "invalid-plugin") require.Error(t, err) _, err = RunPluginCli("", "noexec-plugin") // expects error since plugin lacks executable permissions require.Error(t, err) } // TestCliPluginStatusCodes verifies that a plugin returns the correct exit codes based on its execution. func TestCliPluginStatusCodes(t *testing.T) { pluginScript := `#!/bin/bash case "$1" in "success") exit 0 ;; "error1") exit 1 ;; "error2") exit 2 ;; *) echo "Unknown argument: $1"; exit 3 ;; esac` pluginPath := createTestPlugin(t, "error-plugin", pluginScript) origPath := os.Getenv("PATH") t.Cleanup(func() { t.Setenv("PATH", origPath) }) t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) output, err := RunPluginCli("", "error-plugin", "success") require.NoError(t, err) assert.Contains(t, NormalizeOutput(output), "") _, err = RunPluginCli("", "error-plugin", "error1") require.Error(t, err) assert.Contains(t, err.Error(), "exit status 1") _, err = RunPluginCli("", "error-plugin", "error2") require.Error(t, err) assert.Contains(t, err.Error(), "exit status 2") _, err = RunPluginCli("", "error-plugin", "unknown") require.Error(t, err) assert.Contains(t, err.Error(), "exit status 3") } // TestCliPluginStdinHandling verifies that a CLI plugin correctly handles input from stdin. func TestCliPluginStdinHandling(t *testing.T) { pluginScript := `#!/bin/bash input=$(cat) echo "Received: $input" exit 0` pluginPath := createTestPlugin(t, "stdin-plugin", pluginScript) origPath := os.Getenv("PATH") t.Cleanup(func() { t.Setenv("PATH", origPath) }) t.Setenv("PATH", filepath.Dir(pluginPath)+":"+origPath) testCases := []struct { name string stdin string expected string }{ { "Single line input", "Hello, ArgoCD!", "Received: Hello, ArgoCD!", }, { "Multiline input", "Line1\nLine2\nLine3", "Received: Line1\nLine2\nLine3", }, { "Empty input", "", "Received:", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { output, err := RunPluginCli(tc.stdin, "stdin-plugin") require.NoError(t, err) assert.Contains(t, NormalizeOutput(output), tc.expected) }) } }