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 }