diff --git a/templates/cli/internal/cmd/fanout_test.go b/templates/cli/internal/cmd/fanout_test.go index 413d38748c..514f501515 100644 --- a/templates/cli/internal/cmd/fanout_test.go +++ b/templates/cli/internal/cmd/fanout_test.go @@ -2,8 +2,10 @@ package cmd import ( "bytes" + "errors" "net/http" "net/http/httptest" + "os" "path/filepath" "slices" "strings" @@ -206,6 +208,100 @@ func TestPickerOffersOnlyRealActions(t *testing.T) { assertResolves("pull", pullPromptOrder, pullValues) } +func TestEverythingSkipsConsoleOnlyActionsUnderAnAPIKey(t *testing.T) { + for _, run := range []struct { + name string + call func(*cobra.Command, *[]string) error + }{ + {"pull", func(command *cobra.Command, ran *[]string) error { + return runPull(command, []pullAction{ + {Value: "settings", Run: refuseConsole}, + {Value: "functions", Run: record(ran, "functions")}, + }, true) + }}, + {"push", func(command *cobra.Command, ran *[]string) error { + return runPushActions(command, []pushAction{ + {Value: "settings", Run: refuseConsole}, + {Value: "functions", Run: record(ran, "functions")}, + }, true) + }}, + } { + t.Run(run.name, func(t *testing.T) { + isolateGlobalAll(t) + + command, out := captureCommand() + var ran []string + + if err := run.call(command, &ran); err != nil { + t.Fatalf("`%s all` stopped at settings: %v", run.name, err) + } + if !slices.Contains(ran, "functions") { + t.Errorf("`%s all` never reached functions", run.name) + } + if !strings.Contains(out.String(), "Skipping settings") { + t.Errorf("the skipped settings were not reported:\n%s", out) + } + }) + } +} + +func TestEverythingStillStopsOnOtherErrors(t *testing.T) { + isolateGlobalAll(t) + + var ran []string + failure := errors.New("boom") + + err := runPushActions(&cobra.Command{}, []pushAction{ + {Value: "settings", Run: func(*cobra.Command) error { return failure }}, + {Value: "functions", Run: record(&ran, "functions")}, + }, true) + if !errors.Is(err, failure) { + t.Errorf("err = %v, want the settings failure", err) + } + if len(ran) != 0 { + t.Errorf("ran %v after a failure", ran) + } +} + +func TestAllUnderAnAPIKeyLeavesOutOnlySettings(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + _, _ = w.Write([]byte(`{"total":0}`)) + })) + t.Cleanup(server.Close) + + for _, name := range []string{"pull", "push"} { + t.Run(name, func(t *testing.T) { + isolateGlobalAll(t) + + preferencesWith(t, `{"current":"ci","ci":{"endpoint":"`+server.URL+`","key":"secret"}}`) + t.Setenv("XDG_CONFIG_HOME", "") + directory := t.TempDir() + inDirectory(t, directory) + if err := os.WriteFile(filepath.Join(directory, config.LocalFileName), + []byte(`{"projectId":"probe","projectName":"probe"}`), 0o600); err != nil { + t.Fatal(err) + } + + root := NewRootCommand() + root.SetArgs([]string{name, "all", "--force"}) + out := &strings.Builder{} + root.SetOut(out) + root.SetErr(out) + + if err := root.Execute(); err != nil { + t.Fatalf("`%s all` with an API key failed: %v\n%s", name, err, out) + } + if !strings.Contains(out.String(), "Skipping settings") { + t.Errorf("the skipped settings were not reported:\n%s", out) + } + }) + } +} + +func refuseConsole(*cobra.Command) error { + return ErrConsoleNeedsSession +} + func record(ran *[]string, value string) func(*cobra.Command) error { return func(*cobra.Command) error { *ran = append(*ran, value) diff --git a/templates/cli/internal/cmd/pullall.go b/templates/cli/internal/cmd/pullall.go index b281ac705e..57e46553ae 100644 --- a/templates/cli/internal/cmd/pullall.go +++ b/templates/cli/internal/cmd/pullall.go @@ -131,6 +131,10 @@ func runPull(command *cobra.Command, actions []pullAction, everything bool) erro continue } if err := action.Run(command); err != nil { + if skipsConsoleOnly(command, action.Value, "pull", err) { + continue + } + return err } } diff --git a/templates/cli/internal/cmd/pushall.go b/templates/cli/internal/cmd/pushall.go index b6d1993250..90c203ffbc 100644 --- a/templates/cli/internal/cmd/pushall.go +++ b/templates/cli/internal/cmd/pushall.go @@ -164,6 +164,10 @@ func runPushActions(command *cobra.Command, actions []pushAction, everything boo continue } if err := action.Run(command); err != nil { + if skipsConsoleOnly(command, action.Value, "push", err) { + continue + } + return err } } diff --git a/templates/cli/internal/cmd/session.go b/templates/cli/internal/cmd/session.go index 4573fcea27..af030a8ba4 100644 --- a/templates/cli/internal/cmd/session.go +++ b/templates/cli/internal/cmd/session.go @@ -15,6 +15,7 @@ import ( "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/client" "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/config" "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/jsonx" + "github.com/{{ sdk.gitUserName }}/{{ sdk.gitRepoName | caseDash }}/internal/output" ) // Implements session handling and the `whoami` command. @@ -29,6 +30,18 @@ var ErrConsoleNeedsSession = errors.New( "(e.g. `" + app.ExecutableName + " push functions`), not console-only commands " + "(e.g. `" + app.ExecutableName + " push settings`)") +func skipsConsoleOnly(command *cobra.Command, resource, verb string, err error) bool { + if !errors.Is(err, ErrConsoleNeedsSession) { + return false + } + + output.Warn(command.OutOrStdout(), + "Skipping %s: it needs a signed-in session, and only an API key is configured. Run `%s login` to %s %s too.", + resource, app.ExecutableName, verb, resource) + + return true +} + // preferences loads the user's global preferences. func preferences() (*config.Global, error) { path, err := config.GlobalPath(app.ExecutableName)