From 0aa2ef08048ef811c64b52be29c5898db7440e35 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:57:59 -0600 Subject: [PATCH 01/11] test(notebooks): add failing tests for readBody helper Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index 9260f01c..e905340f 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -6,6 +6,10 @@ package cmd import ( + "bytes" + "os" + "path/filepath" + "strings" "testing" ) @@ -106,6 +110,83 @@ func TestNotebooksDeleteCmd(t *testing.T) { } } +func TestReadBody_File(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "body.json") + content := []byte(`{"data":{"attributes":{"name":"test"}}}`) + if err := os.WriteFile(tmpFile, content, 0644); err != nil { + t.Fatal(err) + } + + got, err := readBody("@" + tmpFile) + if err != nil { + t.Fatalf("readBody returned error: %v", err) + } + if !bytes.Equal(got, content) { + t.Errorf("got %s, want %s", got, content) + } +} + +func TestReadBody_Stdin(t *testing.T) { + content := `{"data":{"attributes":{"name":"test"}}}` + origReader := inputReader + inputReader = strings.NewReader(content) + defer func() { inputReader = origReader }() + + got, err := readBody("-") + if err != nil { + t.Fatalf("readBody returned error: %v", err) + } + if string(got) != content { + t.Errorf("got %s, want %s", got, content) + } +} + +func TestReadBody_MissingFile(t *testing.T) { + _, err := readBody("@/nonexistent/path/body.json") + if err == nil { + t.Fatal("expected error for missing file") + } + if !strings.Contains(err.Error(), "failed to read body file") { + t.Errorf("error = %v, want 'failed to read body file'", err) + } +} + +func TestReadBody_InvalidJSON(t *testing.T) { + tmpFile := filepath.Join(t.TempDir(), "bad.json") + if err := os.WriteFile(tmpFile, []byte("not json"), 0644); err != nil { + t.Fatal(err) + } + + _, err := readBody("@" + tmpFile) + if err == nil { + t.Fatal("expected error for invalid JSON") + } + if !strings.Contains(err.Error(), "invalid JSON in body") { + t.Errorf("error = %v, want 'invalid JSON in body'", err) + } +} + +func TestReadBody_InvalidJSON_Stdin(t *testing.T) { + origReader := inputReader + inputReader = strings.NewReader("not json") + defer func() { inputReader = origReader }() + + _, err := readBody("-") + if err == nil { + t.Fatal("expected error for invalid JSON from stdin") + } + if !strings.Contains(err.Error(), "invalid JSON in body") { + t.Errorf("error = %v, want 'invalid JSON in body'", err) + } +} + +func TestReadBody_EmptyValue(t *testing.T) { + _, err := readBody("") + if err == nil { + t.Fatal("expected error for empty body value") + } +} + func TestNotebooksCmd_ParentChild(t *testing.T) { commands := notebooksCmd.Commands() From 6468a8e5230294cf83ef6f0420ae02f673265ecc Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:58:30 -0600 Subject: [PATCH 02/11] feat(notebooks): add readBody helper for JSON file/stdin input Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index 8fa7529b..d631d88d 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -6,7 +6,11 @@ package cmd import ( + "encoding/json" "fmt" + "io" + "os" + "strings" "github.com/DataDog/datadog-api-client-go/v2/api/datadogV1" "github.com/DataDog/pup/pkg/formatter" @@ -63,6 +67,34 @@ func init() { notebooksCmd.AddCommand(notebooksListCmd, notebooksGetCmd, notebooksDeleteCmd) } +// readBody reads JSON body content from a file (@path) or stdin (-). +func readBody(value string) ([]byte, error) { + var data []byte + var err error + + switch { + case value == "-": + data, err = io.ReadAll(inputReader) + if err != nil { + return nil, fmt.Errorf("failed to read body from stdin: %w", err) + } + case strings.HasPrefix(value, "@"): + path := strings.TrimPrefix(value, "@") + data, err = os.ReadFile(path) + if err != nil { + return nil, fmt.Errorf("failed to read body file: %w", err) + } + default: + return nil, fmt.Errorf("body must be @filepath or - for stdin") + } + + if !json.Valid(data) { + return nil, fmt.Errorf("invalid JSON in body") + } + + return data, nil +} + func runNotebooksList(cmd *cobra.Command, args []string) error { client, err := getClient() if err != nil { From 03fb325101b6681209f9369e8d5dda0f6cbc3029 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 20:58:55 -0600 Subject: [PATCH 03/11] test(notebooks): add failing tests for create command structure Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks_test.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index e905340f..0887d626 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -187,6 +187,46 @@ func TestReadBody_EmptyValue(t *testing.T) { } } +func TestNotebooksCreateCmd(t *testing.T) { + if notebooksCreateCmd == nil { + t.Fatal("notebooksCreateCmd is nil") + } + + if notebooksCreateCmd.Use != "create" { + t.Errorf("Use = %s, want create", notebooksCreateCmd.Use) + } + + if notebooksCreateCmd.Short == "" { + t.Error("Short description is empty") + } + + if notebooksCreateCmd.RunE == nil { + t.Error("RunE is nil") + } + + flags := notebooksCreateCmd.Flags() + if flags.Lookup("body") == nil { + t.Error("Missing --body flag") + } +} + +func TestNotebooksCreateCmd_BodyRequired(t *testing.T) { + flag := notebooksCreateCmd.Flags().Lookup("body") + if flag == nil { + t.Fatal("--body flag not found") + } + + // Check that the flag annotation marks it as required + annotations := notebooksCreateCmd.Flags().Lookup("body").Annotations + if _, ok := annotations["cobra_annotation_bash_completion_one_required_flag"]; !ok { + // Alternative check: try running without the flag + err := notebooksCreateCmd.ValidateRequiredFlags() + if err == nil { + t.Error("expected --body to be required") + } + } +} + func TestNotebooksCmd_ParentChild(t *testing.T) { commands := notebooksCmd.Commands() From 27b9880e6616ad8be7350fe5375eadbaa7aa0db1 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:01:06 -0600 Subject: [PATCH 04/11] feat(notebooks): add create command with JSON body input Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index d631d88d..4a61e3d7 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -56,6 +56,12 @@ var notebooksGetCmd = &cobra.Command{ RunE: runNotebooksGet, } +var notebooksCreateCmd = &cobra.Command{ + Use: "create", + Short: "Create a new notebook", + RunE: runNotebooksCreate, +} + var notebooksDeleteCmd = &cobra.Command{ Use: "delete [notebook-id]", Short: "Delete a notebook", @@ -64,7 +70,12 @@ var notebooksDeleteCmd = &cobra.Command{ } func init() { - notebooksCmd.AddCommand(notebooksListCmd, notebooksGetCmd, notebooksDeleteCmd) + notebooksCreateCmd.Flags().String("body", "", "JSON body (@filepath or - for stdin) (required)") + if err := notebooksCreateCmd.MarkFlagRequired("body"); err != nil { + panic(fmt.Errorf("failed to mark flag as required: %w", err)) + } + + notebooksCmd.AddCommand(notebooksListCmd, notebooksGetCmd, notebooksCreateCmd, notebooksDeleteCmd) } // readBody reads JSON body content from a file (@path) or stdin (-). @@ -95,6 +106,40 @@ func readBody(value string) ([]byte, error) { return data, nil } +func runNotebooksCreate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + bodyFlag, _ := cmd.Flags().GetString("body") + data, err := readBody(bodyFlag) + if err != nil { + return err + } + + var body datadogV1.NotebookCreateRequest + if err := json.Unmarshal(data, &body); err != nil { + return fmt.Errorf("failed to parse notebook create request: %w", err) + } + + api := datadogV1.NewNotebooksApi(client.V1()) + resp, r, err := api.CreateNotebook(client.Context(), body) + if err != nil { + if r != nil { + return fmt.Errorf("failed to create notebook: %w (status: %d)", err, r.StatusCode) + } + return fmt.Errorf("failed to create notebook: %w", err) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + func runNotebooksList(cmd *cobra.Command, args []string) error { client, err := getClient() if err != nil { From db5ff8c7ad6bdaf316e9f65d678d690a10e77c08 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:01:26 -0600 Subject: [PATCH 05/11] test(notebooks): add failing tests for update command structure Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks_test.go | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index 0887d626..f3500128 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -227,6 +227,33 @@ func TestNotebooksCreateCmd_BodyRequired(t *testing.T) { } } +func TestNotebooksUpdateCmd(t *testing.T) { + if notebooksUpdateCmd == nil { + t.Fatal("notebooksUpdateCmd is nil") + } + + if notebooksUpdateCmd.Use != "update [notebook-id]" { + t.Errorf("Use = %s, want 'update [notebook-id]'", notebooksUpdateCmd.Use) + } + + if notebooksUpdateCmd.Short == "" { + t.Error("Short description is empty") + } + + if notebooksUpdateCmd.RunE == nil { + t.Error("RunE is nil") + } + + if notebooksUpdateCmd.Args == nil { + t.Error("Args validator is nil") + } + + flags := notebooksUpdateCmd.Flags() + if flags.Lookup("body") == nil { + t.Error("Missing --body flag") + } +} + func TestNotebooksCmd_ParentChild(t *testing.T) { commands := notebooksCmd.Commands() From 34c914b08d1038c1b85c46fb5775df3fbe5f7965 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:02:02 -0600 Subject: [PATCH 06/11] feat(notebooks): add update command with JSON body input Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 50 +++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index 4a61e3d7..b380d1ec 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -62,6 +62,13 @@ var notebooksCreateCmd = &cobra.Command{ RunE: runNotebooksCreate, } +var notebooksUpdateCmd = &cobra.Command{ + Use: "update [notebook-id]", + Short: "Update a notebook", + Args: cobra.ExactArgs(1), + RunE: runNotebooksUpdate, +} + var notebooksDeleteCmd = &cobra.Command{ Use: "delete [notebook-id]", Short: "Delete a notebook", @@ -75,7 +82,12 @@ func init() { panic(fmt.Errorf("failed to mark flag as required: %w", err)) } - notebooksCmd.AddCommand(notebooksListCmd, notebooksGetCmd, notebooksCreateCmd, notebooksDeleteCmd) + notebooksUpdateCmd.Flags().String("body", "", "JSON body (@filepath or - for stdin) (required)") + if err := notebooksUpdateCmd.MarkFlagRequired("body"); err != nil { + panic(fmt.Errorf("failed to mark flag as required: %w", err)) + } + + notebooksCmd.AddCommand(notebooksListCmd, notebooksGetCmd, notebooksCreateCmd, notebooksUpdateCmd, notebooksDeleteCmd) } // readBody reads JSON body content from a file (@path) or stdin (-). @@ -140,6 +152,42 @@ func runNotebooksCreate(cmd *cobra.Command, args []string) error { return nil } +func runNotebooksUpdate(cmd *cobra.Command, args []string) error { + client, err := getClient() + if err != nil { + return err + } + + notebookID := parseInt64(args[0]) + + bodyFlag, _ := cmd.Flags().GetString("body") + data, err := readBody(bodyFlag) + if err != nil { + return err + } + + var body datadogV1.NotebookUpdateRequest + if err := json.Unmarshal(data, &body); err != nil { + return fmt.Errorf("failed to parse notebook update request: %w", err) + } + + api := datadogV1.NewNotebooksApi(client.V1()) + resp, r, err := api.UpdateNotebook(client.Context(), notebookID, body) + if err != nil { + if r != nil { + return fmt.Errorf("failed to update notebook: %w (status: %d)", err, r.StatusCode) + } + return fmt.Errorf("failed to update notebook: %w", err) + } + + output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) + if err != nil { + return err + } + printOutput("%s\n", output) + return nil +} + func runNotebooksList(cmd *cobra.Command, args []string) error { client, err := getClient() if err != nil { From 2f098ab695e7e52f63748800bb3587bd38b031d2 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:04:11 -0600 Subject: [PATCH 07/11] test(notebooks): update subcommand test for create and update Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index f3500128..db36a22c 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -32,7 +32,7 @@ func TestNotebooksCmd(t *testing.T) { } func TestNotebooksCmd_Subcommands(t *testing.T) { - expectedCommands := []string{"list", "get", "delete"} + expectedCommands := []string{"list", "get", "create", "update", "delete"} commands := notebooksCmd.Commands() From e7c3fc90afd3bf3c565cc4912c867bc914bee7e6 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 21:57:41 -0600 Subject: [PATCH 08/11] fix(notebooks): add OAuth fallback to API keys for all notebook commands The Notebooks V1 API doesn't support OAuth authentication. Register all notebook endpoints in the auth validator fallback registry and switch all commands to use getClientForEndpoint() for automatic API key fallback. Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 10 +++++----- pkg/client/auth_validator.go | 7 +++++++ 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index b380d1ec..5312be18 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -119,7 +119,7 @@ func readBody(value string) ([]byte, error) { } func runNotebooksCreate(cmd *cobra.Command, args []string) error { - client, err := getClient() + client, err := getClientForEndpoint("POST", "/api/v1/notebooks") if err != nil { return err } @@ -153,7 +153,7 @@ func runNotebooksCreate(cmd *cobra.Command, args []string) error { } func runNotebooksUpdate(cmd *cobra.Command, args []string) error { - client, err := getClient() + client, err := getClientForEndpoint("PUT", "/api/v1/notebooks/") if err != nil { return err } @@ -189,7 +189,7 @@ func runNotebooksUpdate(cmd *cobra.Command, args []string) error { } func runNotebooksList(cmd *cobra.Command, args []string) error { - client, err := getClient() + client, err := getClientForEndpoint("GET", "/api/v1/notebooks") if err != nil { return err } @@ -212,7 +212,7 @@ func runNotebooksList(cmd *cobra.Command, args []string) error { } func runNotebooksGet(cmd *cobra.Command, args []string) error { - client, err := getClient() + client, err := getClientForEndpoint("GET", "/api/v1/notebooks/") if err != nil { return err } @@ -236,7 +236,7 @@ func runNotebooksGet(cmd *cobra.Command, args []string) error { } func runNotebooksDelete(cmd *cobra.Command, args []string) error { - client, err := getClient() + client, err := getClientForEndpoint("DELETE", "/api/v1/notebooks/") if err != nil { return err } diff --git a/pkg/client/auth_validator.go b/pkg/client/auth_validator.go index 45f9c089..cdc53c03 100644 --- a/pkg/client/auth_validator.go +++ b/pkg/client/auth_validator.go @@ -64,6 +64,13 @@ var endpointsWithoutOAuth = []EndpointAuthRequirement{ // Error Tracking API - OAuth not working in practice {Path: "/api/v2/error_tracking/issues/search", Method: "POST", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Error Tracking API requires API keys"}, {Path: "/api/v2/error_tracking/issues/", Method: "GET", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Error Tracking API requires API keys"}, + + // Notebooks API (V1) - missing OAuth scopes in spec + {Path: "/api/v1/notebooks", Method: "GET", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Notebooks API missing OAuth implementation in spec"}, + {Path: "/api/v1/notebooks", Method: "POST", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Notebooks API missing OAuth implementation in spec"}, + {Path: "/api/v1/notebooks/", Method: "GET", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Notebooks API missing OAuth implementation in spec"}, + {Path: "/api/v1/notebooks/", Method: "PUT", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Notebooks API missing OAuth implementation in spec"}, + {Path: "/api/v1/notebooks/", Method: "DELETE", SupportsOAuth: false, RequiresAPIKeys: true, Reason: "Notebooks API missing OAuth implementation in spec"}, } // AuthType represents the type of authentication being used From 5c86791e4d0a8a4412f906a92935b3de87278fad Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:11:08 -0600 Subject: [PATCH 09/11] fix(auth): use NewWithAPIKeys for OAuth fallback endpoints getClientForEndpoint was calling client.New which prefers OAuth over API keys, causing 403s on endpoints without OAuth support. Add an apiKeyClientFactory injection point that uses NewWithAPIKeys to force API key auth for fallback endpoints. Co-Authored-By: Claude Opus 4.6 --- cmd/api_keys_test.go | 8 ++++++-- cmd/root.go | 18 +++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/cmd/api_keys_test.go b/cmd/api_keys_test.go index ce1f3047..6632c6c7 100644 --- a/cmd/api_keys_test.go +++ b/cmd/api_keys_test.go @@ -157,6 +157,7 @@ func setupTestClient(t *testing.T) func() { origClient := ddClient origCfg := cfg origFactory := clientFactory + origAPIKeyFactory := apiKeyClientFactory // Create test config cfg = &config.Config{ @@ -166,10 +167,12 @@ func setupTestClient(t *testing.T) func() { AutoApprove: false, } - // Mock the client factory to return an error immediately - clientFactory = func(c *config.Config) (*client.Client, error) { + // Mock the client factories to return an error immediately + mockErr := func(c *config.Config) (*client.Client, error) { return nil, fmt.Errorf("mock client: no real API connection in tests") } + clientFactory = mockErr + apiKeyClientFactory = mockErr ddClient = nil @@ -178,6 +181,7 @@ func setupTestClient(t *testing.T) func() { ddClient = origClient cfg = origCfg clientFactory = origFactory + apiKeyClientFactory = origAPIKeyFactory } } diff --git a/cmd/root.go b/cmd/root.go index f6ed1773..0eb5520f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,6 +28,14 @@ func defaultClientFactory(cfg *config.Config) (*client.Client, error) { return client.New(cfg) } +// defaultAPIKeyClientFactory forces API key authentication +func defaultAPIKeyClientFactory(cfg *config.Config) (*client.Client, error) { + if err := cfg.Validate(); err != nil { + return nil, err + } + return client.NewWithAPIKeys(cfg) +} + var ( cfg *config.Config ddClient *client.Client @@ -36,6 +44,7 @@ var ( // Dependency injection points for testing clientFactory = defaultClientFactory + apiKeyClientFactory = defaultAPIKeyClientFactory outputWriter io.Writer = os.Stdout inputReader io.Reader = os.Stdin ) @@ -261,13 +270,8 @@ func getClientForEndpoint(method, path string) (*client.Client, error) { ) } - // Try to use the mocked factory if in test mode (allows test to fail intentionally) - // This respects the clientFactory mock in tests - c, err := clientFactory(cfg) - if err != nil { - return nil, err - } - return c, nil + // Force API key authentication for endpoints without OAuth support + return apiKeyClientFactory(cfg) } // Endpoint supports OAuth, use standard client From cbfc42d979f8141e7d4ba118bb905c799dd80a85 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:17:45 -0600 Subject: [PATCH 10/11] fix(notebooks): use printOutput and readConfirmation consistently - Switch list/get from fmt.Println to printOutput for testability - Switch delete from fmt.Scanln to readConfirmation (uses inputReader) - Switch delete from fmt.Printf/Print to printOutput - Update auth description: OAuth not supported, API keys required - Simplify BodyRequired test to use ValidateRequiredFlags directly Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 23 ++++++++++++----------- cmd/notebooks_test.go | 13 +++---------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index 5312be18..6cac5543 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -40,7 +40,8 @@ EXAMPLES: pup notebooks get notebook-id AUTHENTICATION: - Requires either OAuth2 authentication or API keys.`, + Requires API key authentication (DD_API_KEY + DD_APP_KEY). + OAuth2 is not supported for this endpoint.`, } var notebooksListCmd = &cobra.Command{ @@ -207,7 +208,7 @@ func runNotebooksList(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(output) + printOutput("%s\n", output) return nil } @@ -231,7 +232,7 @@ func runNotebooksGet(cmd *cobra.Command, args []string) error { if err != nil { return err } - fmt.Println(output) + printOutput("%s\n", output) return nil } @@ -243,16 +244,16 @@ func runNotebooksDelete(cmd *cobra.Command, args []string) error { notebookID := parseInt64(args[0]) if !cfg.AutoApprove { - fmt.Printf("⚠️ WARNING: This will permanently delete notebook %d\n", notebookID) - fmt.Print("Are you sure you want to continue? (y/N): ") - var response string - if _, err := fmt.Scanln(&response); err != nil { - // User cancelled or error reading input - fmt.Println("\nOperation cancelled") + printOutput("⚠️ WARNING: This will permanently delete notebook %d\n", notebookID) + printOutput("Are you sure you want to continue? (y/N): ") + + response, err := readConfirmation() + if err != nil { + printOutput("\nOperation cancelled\n") return nil } if response != "y" && response != "Y" { - fmt.Println("Operation cancelled") + printOutput("Operation cancelled\n") return nil } } @@ -266,6 +267,6 @@ func runNotebooksDelete(cmd *cobra.Command, args []string) error { return fmt.Errorf("failed to delete notebook: %w", err) } - fmt.Printf("Successfully deleted notebook %d\n", notebookID) + printOutput("Successfully deleted notebook %d\n", notebookID) return nil } diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index db36a22c..57d00f70 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -211,19 +211,12 @@ func TestNotebooksCreateCmd(t *testing.T) { } func TestNotebooksCreateCmd_BodyRequired(t *testing.T) { - flag := notebooksCreateCmd.Flags().Lookup("body") - if flag == nil { + if notebooksCreateCmd.Flags().Lookup("body") == nil { t.Fatal("--body flag not found") } - // Check that the flag annotation marks it as required - annotations := notebooksCreateCmd.Flags().Lookup("body").Annotations - if _, ok := annotations["cobra_annotation_bash_completion_one_required_flag"]; !ok { - // Alternative check: try running without the flag - err := notebooksCreateCmd.ValidateRequiredFlags() - if err == nil { - t.Error("expected --body to be required") - } + if err := notebooksCreateCmd.ValidateRequiredFlags(); err == nil { + t.Error("expected --body to be required") } } From 1916a4c4a9a956f0e8eb5b41b80db663c7e5e3a8 Mon Sep 17 00:00:00 2001 From: Jake Edgington <38260964+jakedgy@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:26:47 -0600 Subject: [PATCH 11/11] fix(notebooks): use formatAPIError and add missing examples/tests - Replace inline error formatting with formatAPIError for richer user-facing error messages (status-specific hints for 401, 403, etc.) - Add create, update, and delete examples to long description - Add TestNotebooksUpdateCmd_BodyRequired for symmetry with create Co-Authored-By: Claude Opus 4.6 --- cmd/notebooks.go | 37 +++++++++++++++++-------------------- cmd/notebooks_test.go | 10 ++++++++++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/cmd/notebooks.go b/cmd/notebooks.go index 6cac5543..1818f27f 100644 --- a/cmd/notebooks.go +++ b/cmd/notebooks.go @@ -39,6 +39,18 @@ EXAMPLES: # Get notebook details pup notebooks get notebook-id + # Create a notebook from file + pup notebooks create --body @notebook.json + + # Create from stdin + cat notebook.json | pup notebooks create --body - + + # Update a notebook + pup notebooks update 12345 --body @updated.json + + # Delete a notebook + pup notebooks delete 12345 + AUTHENTICATION: Requires API key authentication (DD_API_KEY + DD_APP_KEY). OAuth2 is not supported for this endpoint.`, @@ -139,10 +151,7 @@ func runNotebooksCreate(cmd *cobra.Command, args []string) error { api := datadogV1.NewNotebooksApi(client.V1()) resp, r, err := api.CreateNotebook(client.Context(), body) if err != nil { - if r != nil { - return fmt.Errorf("failed to create notebook: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to create notebook: %w", err) + return formatAPIError("create notebook", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -175,10 +184,7 @@ func runNotebooksUpdate(cmd *cobra.Command, args []string) error { api := datadogV1.NewNotebooksApi(client.V1()) resp, r, err := api.UpdateNotebook(client.Context(), notebookID, body) if err != nil { - if r != nil { - return fmt.Errorf("failed to update notebook: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to update notebook: %w", err) + return formatAPIError("update notebook", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -198,10 +204,7 @@ func runNotebooksList(cmd *cobra.Command, args []string) error { api := datadogV1.NewNotebooksApi(client.V1()) resp, r, err := api.ListNotebooks(client.Context()) if err != nil { - if r != nil { - return fmt.Errorf("failed to list notebooks: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to list notebooks: %w", err) + return formatAPIError("list notebooks", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -222,10 +225,7 @@ func runNotebooksGet(cmd *cobra.Command, args []string) error { api := datadogV1.NewNotebooksApi(client.V1()) resp, r, err := api.GetNotebook(client.Context(), notebookID) if err != nil { - if r != nil { - return fmt.Errorf("failed to get notebook: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to get notebook: %w", err) + return formatAPIError("get notebook", err, r) } output, err := formatter.FormatOutput(resp, formatter.OutputFormat(outputFormat)) @@ -261,10 +261,7 @@ func runNotebooksDelete(cmd *cobra.Command, args []string) error { api := datadogV1.NewNotebooksApi(client.V1()) r, err := api.DeleteNotebook(client.Context(), notebookID) if err != nil { - if r != nil { - return fmt.Errorf("failed to delete notebook: %w (status: %d)", err, r.StatusCode) - } - return fmt.Errorf("failed to delete notebook: %w", err) + return formatAPIError("delete notebook", err, r) } printOutput("Successfully deleted notebook %d\n", notebookID) diff --git a/cmd/notebooks_test.go b/cmd/notebooks_test.go index 57d00f70..5715073a 100644 --- a/cmd/notebooks_test.go +++ b/cmd/notebooks_test.go @@ -220,6 +220,16 @@ func TestNotebooksCreateCmd_BodyRequired(t *testing.T) { } } +func TestNotebooksUpdateCmd_BodyRequired(t *testing.T) { + if notebooksUpdateCmd.Flags().Lookup("body") == nil { + t.Fatal("--body flag not found") + } + + if err := notebooksUpdateCmd.ValidateRequiredFlags(); err == nil { + t.Error("expected --body to be required") + } +} + func TestNotebooksUpdateCmd(t *testing.T) { if notebooksUpdateCmd == nil { t.Fatal("notebooksUpdateCmd is nil")