github.com/argoproj/argo-cd/v3@v3.2.1/cmd/argocd/commands/plugin_test.go (about)

     1  package commands
     2  
     3  import (
     4  	"bytes"
     5  	"errors"
     6  	"os"
     7  	"os/exec"
     8  	"path/filepath"
     9  	"testing"
    10  
    11  	argocdclient "github.com/argoproj/argo-cd/v3/pkg/apiclient"
    12  
    13  	"github.com/stretchr/testify/assert"
    14  	"github.com/stretchr/testify/require"
    15  )
    16  
    17  // setupPluginPath sets the PATH to the directory where plugins are stored for testing purpose
    18  func setupPluginPath(t *testing.T) {
    19  	t.Helper()
    20  	wd, err := os.Getwd()
    21  	require.NoError(t, err)
    22  	testdataPath := filepath.Join(wd, "testdata")
    23  	t.Setenv("PATH", testdataPath)
    24  }
    25  
    26  // TestNormalCommandWithPlugin ensures that a standard ArgoCD command executes correctly
    27  // even when a plugin with the same name exists in the PATH
    28  func TestNormalCommandWithPlugin(t *testing.T) {
    29  	setupPluginPath(t)
    30  
    31  	_ = NewDefaultPluginHandler([]string{"argocd"})
    32  	args := []string{"argocd", "version", "--short", "--client"}
    33  	buf := new(bytes.Buffer)
    34  	cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
    35  	cmd.SetArgs(args[1:])
    36  	cmd.SetOut(buf)
    37  	cmd.SilenceErrors = true
    38  	cmd.SilenceUsage = true
    39  
    40  	err := cmd.Execute()
    41  	require.NoError(t, err)
    42  	output := buf.String()
    43  	assert.Equal(t, "argocd: v99.99.99+unknown\n", output)
    44  }
    45  
    46  // TestPluginExecution verifies that a plugin found in the PATH executes successfully following the correct naming conventions
    47  func TestPluginExecution(t *testing.T) {
    48  	setupPluginPath(t)
    49  
    50  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
    51  	cmd := NewCommand()
    52  	cmd.SilenceErrors = true
    53  	cmd.SilenceUsage = true
    54  
    55  	tests := []struct {
    56  		name              string
    57  		args              []string
    58  		expectedPluginErr string
    59  	}{
    60  		{
    61  			name:              "'argocd-foo' binary exists in the PATH",
    62  			args:              []string{"argocd", "foo"},
    63  			expectedPluginErr: "",
    64  		},
    65  		{
    66  			name:              "'argocd-demo_plugin' binary exists in the PATH",
    67  			args:              []string{"argocd", "demo_plugin"},
    68  			expectedPluginErr: "",
    69  		},
    70  		{
    71  			name:              "'my-plugin' binary exists in the PATH",
    72  			args:              []string{"argocd", "my-plugin"},
    73  			expectedPluginErr: "unknown command \"my-plugin\" for \"argocd\"",
    74  		},
    75  		{
    76  			name:              "'argocd_my-plugin' binary exists in the PATH",
    77  			args:              []string{"argocd", "my-plugin"},
    78  			expectedPluginErr: "unknown command \"my-plugin\" for \"argocd\"",
    79  		},
    80  	}
    81  
    82  	for _, tt := range tests {
    83  		t.Run(tt.name, func(t *testing.T) {
    84  			cmd.SetArgs(tt.args[1:])
    85  
    86  			err := cmd.Execute()
    87  			require.Error(t, err)
    88  
    89  			// since the command is not a valid argocd command, check for plugin execution
    90  			pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
    91  			if tt.expectedPluginErr == "" {
    92  				require.NoError(t, pluginErr)
    93  			} else {
    94  				require.EqualError(t, pluginErr, tt.expectedPluginErr)
    95  			}
    96  		})
    97  	}
    98  }
    99  
   100  // TestNormalCommandError checks for an error when executing a normal ArgoCD command with invalid flags
   101  func TestNormalCommandError(t *testing.T) {
   102  	setupPluginPath(t)
   103  
   104  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   105  	args := []string{"argocd", "version", "--non-existent-flag"}
   106  	cmd := NewVersionCmd(&argocdclient.ClientOptions{}, nil)
   107  	cmd.SetArgs(args[1:])
   108  	cmd.SilenceErrors = true
   109  	cmd.SilenceUsage = true
   110  
   111  	err := cmd.Execute()
   112  	require.Error(t, err)
   113  
   114  	pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
   115  	assert.EqualError(t, pluginErr, "unknown flag: --non-existent-flag")
   116  }
   117  
   118  // TestUnknownCommandNoPlugin tests the scenario when the command is neither a normal ArgoCD command
   119  // nor exists as a plugin
   120  func TestUnknownCommandNoPlugin(t *testing.T) {
   121  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   122  	cmd := NewCommand()
   123  	cmd.SilenceErrors = true
   124  	cmd.SilenceUsage = true
   125  	args := []string{"argocd", "non-existent"}
   126  	cmd.SetArgs(args[1:])
   127  
   128  	err := cmd.Execute()
   129  	require.Error(t, err)
   130  
   131  	pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
   132  	require.Error(t, pluginErr)
   133  	assert.Equal(t, err, pluginErr)
   134  }
   135  
   136  // TestPluginNoExecutePermission verifies the behavior when a plugin doesn't have executable permissions
   137  func TestPluginNoExecutePermission(t *testing.T) {
   138  	setupPluginPath(t)
   139  
   140  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   141  	cmd := NewCommand()
   142  	cmd.SilenceErrors = true
   143  	cmd.SilenceUsage = true
   144  	args := []string{"argocd", "no-permission"}
   145  	cmd.SetArgs(args[1:])
   146  
   147  	err := cmd.Execute()
   148  	require.Error(t, err)
   149  
   150  	pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
   151  	require.Error(t, pluginErr)
   152  	assert.EqualError(t, pluginErr, "unknown command \"no-permission\" for \"argocd\"")
   153  }
   154  
   155  // TestPluginExecutionError checks for errors that occur during plugin execution
   156  func TestPluginExecutionError(t *testing.T) {
   157  	setupPluginPath(t)
   158  
   159  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   160  	cmd := NewCommand()
   161  	cmd.SilenceErrors = true
   162  	cmd.SilenceUsage = true
   163  	args := []string{"argocd", "error"}
   164  	cmd.SetArgs(args[1:])
   165  
   166  	err := cmd.Execute()
   167  	require.Error(t, err)
   168  
   169  	pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
   170  	require.Error(t, pluginErr)
   171  	assert.EqualError(t, pluginErr, "exit status 1")
   172  }
   173  
   174  // TestPluginInRelativePathIgnored ensures that plugins in a relative path, even if the path is included in PATH,
   175  // are ignored and not executed.
   176  func TestPluginInRelativePathIgnored(t *testing.T) {
   177  	setupPluginPath(t)
   178  
   179  	relativePath := "./relative-plugins"
   180  	err := os.MkdirAll(relativePath, 0o755)
   181  	require.NoError(t, err)
   182  	defer os.RemoveAll(relativePath)
   183  
   184  	relativePluginPath := filepath.Join(relativePath, "argocd-ignore-plugin")
   185  	err = os.WriteFile(relativePluginPath, []byte("#!/bin/bash\necho 'This should not execute'\n"), 0o755)
   186  	require.NoError(t, err)
   187  
   188  	t.Setenv("PATH", os.Getenv("PATH")+string(os.PathListSeparator)+relativePath)
   189  
   190  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   191  	cmd := NewCommand()
   192  	cmd.SilenceErrors = true
   193  	cmd.SilenceUsage = true
   194  	args := []string{"argocd", "ignore-plugin"}
   195  	cmd.SetArgs(args[1:])
   196  
   197  	err = cmd.Execute()
   198  	require.Error(t, err)
   199  
   200  	pluginErr := pluginHandler.HandleCommandExecutionError(err, true, args)
   201  	require.Error(t, pluginErr)
   202  	assert.EqualError(t, pluginErr, "unknown command \"ignore-plugin\" for \"argocd\"")
   203  }
   204  
   205  // TestPluginFlagParsing checks that the flags are parsed correctly by the plugin handler
   206  func TestPluginFlagParsing(t *testing.T) {
   207  	setupPluginPath(t)
   208  
   209  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   210  
   211  	tests := []struct {
   212  		name           string
   213  		args           []string
   214  		shouldFail     bool
   215  		expectedErrMsg string
   216  	}{
   217  		{
   218  			name:           "Valid flags",
   219  			args:           []string{"argocd", "test-plugin", "--flag1", "value1", "--flag2", "value2"},
   220  			shouldFail:     false,
   221  			expectedErrMsg: "",
   222  		},
   223  		{
   224  			name:           "Unknown flag",
   225  			args:           []string{"argocd", "test-plugin", "--flag3", "invalid"},
   226  			shouldFail:     true,
   227  			expectedErrMsg: "exit status 1",
   228  		},
   229  	}
   230  
   231  	for _, tt := range tests {
   232  		t.Run(tt.name, func(t *testing.T) {
   233  			cmd := NewCommand()
   234  			cmd.SilenceErrors = true
   235  			cmd.SilenceUsage = true
   236  
   237  			cmd.SetArgs(tt.args[1:])
   238  
   239  			err := cmd.Execute()
   240  			require.Error(t, err)
   241  
   242  			pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
   243  
   244  			if tt.shouldFail {
   245  				require.Error(t, pluginErr)
   246  				assert.Equal(t, tt.expectedErrMsg, pluginErr.Error(), "Unexpected error message")
   247  			} else {
   248  				require.NoError(t, pluginErr, "Expected no error for valid flags")
   249  			}
   250  		})
   251  	}
   252  }
   253  
   254  // TestPluginStatusCode checks for a correct status code that a plugin binary would generate
   255  func TestPluginStatusCode(t *testing.T) {
   256  	setupPluginPath(t)
   257  
   258  	pluginHandler := NewDefaultPluginHandler([]string{"argocd"})
   259  
   260  	tests := []struct {
   261  		name       string
   262  		args       []string
   263  		wantStatus int
   264  		throwErr   bool
   265  	}{
   266  		{
   267  			name:       "plugin generates the successful exit code",
   268  			args:       []string{"argocd", "status-code-plugin", "--flag1", "value1"},
   269  			wantStatus: 0,
   270  			throwErr:   false,
   271  		},
   272  		{
   273  			name:       "plugin generates an error status code",
   274  			args:       []string{"argocd", "status-code-plugin", "--flag3", "value3"},
   275  			wantStatus: 1,
   276  			throwErr:   true,
   277  		},
   278  		{
   279  			name:       "plugin generates a status code for an invalid command",
   280  			args:       []string{"argocd", "status-code-plugin", "invalid"},
   281  			wantStatus: 127,
   282  			throwErr:   true,
   283  		},
   284  	}
   285  
   286  	for _, tt := range tests {
   287  		t.Run(tt.name, func(t *testing.T) {
   288  			cmd := NewCommand()
   289  			cmd.SilenceErrors = true
   290  			cmd.SilenceUsage = true
   291  
   292  			cmd.SetArgs(tt.args[1:])
   293  
   294  			err := cmd.Execute()
   295  			require.Error(t, err)
   296  
   297  			pluginErr := pluginHandler.HandleCommandExecutionError(err, true, tt.args)
   298  			if !tt.throwErr {
   299  				require.NoError(t, pluginErr)
   300  			} else {
   301  				require.Error(t, pluginErr)
   302  				var exitErr *exec.ExitError
   303  				if errors.As(pluginErr, &exitErr) {
   304  					assert.Equal(t, tt.wantStatus, exitErr.ExitCode(), "unexpected exit code")
   305  				} else {
   306  					t.Fatalf("expected an exit error, got: %v", pluginErr)
   307  				}
   308  			}
   309  		})
   310  	}
   311  }