diff --git a/README.md b/README.md index e8dd770..da52a19 100644 --- a/README.md +++ b/README.md @@ -241,6 +241,10 @@ fizzy card unwatch 42 # Remove card header image fizzy card image-remove 42 +# Pin/unpin a card +fizzy card pin 42 +fizzy card unpin 42 + # Mark/unmark card as golden fizzy card golden 42 fizzy card ungolden 42 @@ -349,6 +353,13 @@ fizzy user show USER_ID fizzy tag list ``` +### Pins + +```bash +# List your pinned cards +fizzy pin list +``` + ### Search ```bash diff --git a/e2e/tests/auth_test.go b/e2e/tests/auth_test.go index be1c7e7..28cb256 100644 --- a/e2e/tests/auth_test.go +++ b/e2e/tests/auth_test.go @@ -110,8 +110,6 @@ func TestAuthLogin(t *testing.T) { } defer os.RemoveAll(tmpDir) - configDir := filepath.Join(tmpDir, ".fizzy") - t.Run("saves token to config file", func(t *testing.T) { // Run login with HOME set to temp directory result := harness.Execute(cfg.BinaryPath, []string{"auth", "login", cfg.Token}, map[string]string{ @@ -122,10 +120,13 @@ func TestAuthLogin(t *testing.T) { t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) } - // Check config file was created - configPath := filepath.Join(configDir, "config.yaml") - if _, err := os.Stat(configPath); os.IsNotExist(err) { - t.Error("config file was not created") + // Check config file was created (preferred path is ~/.config/fizzy/config.yaml) + preferredPath := filepath.Join(tmpDir, ".config", "fizzy", "config.yaml") + legacyPath := filepath.Join(tmpDir, ".fizzy", "config.yaml") + _, errPreferred := os.Stat(preferredPath) + _, errLegacy := os.Stat(legacyPath) + if os.IsNotExist(errPreferred) && os.IsNotExist(errLegacy) { + t.Errorf("config file was not created at either %s or %s", preferredPath, legacyPath) } }) } diff --git a/e2e/tests/card_test.go b/e2e/tests/card_test.go index 0c590e0..09a513e 100644 --- a/e2e/tests/card_test.go +++ b/e2e/tests/card_test.go @@ -617,8 +617,10 @@ func TestCardShowNotFound(t *testing.T) { func TestCardCreateMissingBoard(t *testing.T) { h := harness.New(t) - t.Run("fails without required --board option", func(t *testing.T) { - result := h.Run("card", "create", "--title", "Test") + t.Run("fails without required --board option when no default board configured", func(t *testing.T) { + // Use a temp HOME so the global config (which may have a default board) is not found + tmpHome := t.TempDir() + result := h.RunWithEnv(map[string]string{"HOME": tmpHome}, "card", "create", "--title", "Test") // Should fail with error exit code if result.ExitCode == harness.ExitSuccess { diff --git a/e2e/tests/column_test.go b/e2e/tests/column_test.go index 63d9e4f..09e2615 100644 --- a/e2e/tests/column_test.go +++ b/e2e/tests/column_test.go @@ -35,8 +35,10 @@ func TestColumnList(t *testing.T) { } }) - t.Run("fails without --board option", func(t *testing.T) { - result := h.Run("column", "list") + t.Run("fails without --board option when no default board configured", func(t *testing.T) { + // Use a temp HOME so the global config (which may have a default board) is not found + tmpHome := t.TempDir() + result := h.RunWithEnv(map[string]string{"HOME": tmpHome}, "column", "list") if result.ExitCode == harness.ExitSuccess { t.Error("expected non-zero exit code for missing required option") diff --git a/e2e/tests/error_test.go b/e2e/tests/error_test.go index da7aa3c..d838e94 100644 --- a/e2e/tests/error_test.go +++ b/e2e/tests/error_test.go @@ -191,10 +191,8 @@ func TestSuccessResponseFormat(t *testing.T) { t.Error("expected no error in success response") } - // Pagination should be present for list operations - if result.Response.Pagination == nil { - t.Error("expected pagination in list response") - } + // Pagination may be absent when all results fit in a single page + // (the CLI only includes pagination when there is a next page) // Meta should be present if result.Response.Meta == nil { diff --git a/e2e/tests/pin_test.go b/e2e/tests/pin_test.go new file mode 100644 index 0000000..d4ec1b0 --- /dev/null +++ b/e2e/tests/pin_test.go @@ -0,0 +1,168 @@ +package tests + +import ( + "fmt" + "strconv" + "testing" + "time" + + "github.com/robzolkos/fizzy-cli/e2e/harness" +) + +func TestPinActions(t *testing.T) { + h := harness.New(t) + defer h.Cleanup.CleanupAll(h) + + boardID := createTestBoard(t, h) + + // Create a card for pin tests + title := fmt.Sprintf("Pin Test Card %d", time.Now().UnixNano()) + result := h.Run("card", "create", "--board", boardID, "--title", title) + if result.ExitCode != harness.ExitSuccess { + t.Fatalf("failed to create test card: %s\nstdout: %s", result.Stderr, result.Stdout) + } + cardNumber := result.GetNumberFromLocation() + if cardNumber == 0 { + cardNumber = result.GetDataInt("number") + } + if cardNumber == 0 { + t.Fatalf("failed to get card number from create (location: %s)", result.GetLocation()) + } + h.Cleanup.AddCard(cardNumber) + cardStr := strconv.Itoa(cardNumber) + + t.Run("pin card", func(t *testing.T) { + result := h.Run("card", "pin", cardStr) + + if result.ExitCode != harness.ExitSuccess { + t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) + } + + if !result.Response.Success { + t.Errorf("expected success=true, error: %+v", result.Response.Error) + } + }) + + t.Run("pin list includes pinned card", func(t *testing.T) { + result := h.Run("pin", "list") + + if result.ExitCode != harness.ExitSuccess { + t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) + } + + if !result.Response.Success { + t.Errorf("expected success=true, error: %+v", result.Response.Error) + } + + arr := result.GetDataArray() + if arr == nil { + t.Fatal("expected data to be an array") + } + + // Find our pinned card in the list + found := false + for _, item := range arr { + card, ok := item.(map[string]interface{}) + if !ok { + continue + } + if num, ok := card["number"].(float64); ok && int(num) == cardNumber { + found = true + break + } + } + if !found { + t.Errorf("expected pinned card #%d to appear in pin list", cardNumber) + } + }) + + t.Run("unpin card", func(t *testing.T) { + result := h.Run("card", "unpin", cardStr) + + if result.ExitCode != harness.ExitSuccess { + t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) + } + + if !result.Response.Success { + t.Errorf("expected success=true, error: %+v", result.Response.Error) + } + }) + + t.Run("pin list excludes unpinned card", func(t *testing.T) { + result := h.Run("pin", "list") + + if result.ExitCode != harness.ExitSuccess { + t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) + } + + arr := result.GetDataArray() + if arr == nil { + // Empty array is fine - card should not be there + return + } + + for _, item := range arr { + card, ok := item.(map[string]interface{}) + if !ok { + continue + } + if num, ok := card["number"].(float64); ok && int(num) == cardNumber { + t.Errorf("expected card #%d to NOT appear in pin list after unpinning", cardNumber) + } + } + }) +} + +func TestPinList(t *testing.T) { + h := harness.New(t) + + t.Run("returns list of pinned cards", func(t *testing.T) { + result := h.Run("pin", "list") + + if result.ExitCode != harness.ExitSuccess { + t.Errorf("expected exit code %d, got %d\nstderr: %s", harness.ExitSuccess, result.ExitCode, result.Stderr) + } + + if result.Response == nil { + t.Fatalf("expected JSON response, got nil\nstdout: %s", result.Stdout) + } + + if !result.Response.Success { + t.Error("expected success=true") + } + + // Data should be an array + arr := result.GetDataArray() + if arr == nil { + t.Error("expected data to be an array") + } + }) +} + +func TestPinNotFound(t *testing.T) { + h := harness.New(t) + + t.Run("pin non-existent card fails", func(t *testing.T) { + result := h.Run("card", "pin", "999999999") + + if result.ExitCode == harness.ExitSuccess { + t.Error("expected non-zero exit code for non-existent card") + } + + if result.Response != nil && result.Response.Success { + t.Error("expected success=false") + } + }) + + t.Run("unpin non-existent card fails", func(t *testing.T) { + result := h.Run("card", "unpin", "999999999") + + if result.ExitCode == harness.ExitSuccess { + t.Error("expected non-zero exit code for non-existent card") + } + + if result.Response != nil && result.Response.Success { + t.Error("expected success=false") + } + }) +} diff --git a/internal/client/client.go b/internal/client/client.go index 2f91e1e..b053950 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -143,6 +143,12 @@ func (c *Client) request(method, path string, body interface{}) (*APIResponse, e LinkNext: parseLinkNext(resp.Header.Get("Link")), } + // Check for error status codes before parsing JSON, + // since error responses may not be JSON (e.g. HTML 401 pages) + if resp.StatusCode >= 400 { + return apiResp, c.errorFromResponse(resp.StatusCode, respBody) + } + // Parse JSON body if present if len(respBody) > 0 { if err := json.Unmarshal(respBody, &apiResp.Data); err != nil { @@ -150,11 +156,6 @@ func (c *Client) request(method, path string, body interface{}) (*APIResponse, e } } - // Check for error status codes - if resp.StatusCode >= 400 { - return apiResp, c.errorFromResponse(resp.StatusCode, respBody) - } - return apiResp, nil } diff --git a/internal/commands/card.go b/internal/commands/card.go index 1cd3555..6c22ea8 100644 --- a/internal/commands/card.go +++ b/internal/commands/card.go @@ -885,6 +885,72 @@ var cardImageRemoveCmd = &cobra.Command{ }, } +var cardPinCmd = &cobra.Command{ + Use: "pin CARD_NUMBER", + Short: "Pin a card", + Long: "Pins a card for quick access.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if err := requireAuthAndAccount(); err != nil { + exitWithError(err) + } + + cardNumber := args[0] + + client := getClient() + resp, err := client.Post("/cards/"+cardNumber+"/pin.json", nil) + if err != nil { + exitWithError(err) + } + + // Build breadcrumbs + breadcrumbs := []response.Breadcrumb{ + breadcrumb("show", fmt.Sprintf("fizzy card show %s", cardNumber), "View card"), + breadcrumb("pins", "fizzy pin list", "List pinned cards"), + breadcrumb("unpin", fmt.Sprintf("fizzy card unpin %s", cardNumber), "Unpin card"), + } + + data := resp.Data + if data == nil { + data = map[string]interface{}{} + } + printSuccessWithBreadcrumbs(data, "", breadcrumbs) + }, +} + +var cardUnpinCmd = &cobra.Command{ + Use: "unpin CARD_NUMBER", + Short: "Unpin a card", + Long: "Unpins a card, removing it from your pinned list.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + if err := requireAuthAndAccount(); err != nil { + exitWithError(err) + } + + cardNumber := args[0] + + client := getClient() + resp, err := client.Delete("/cards/" + cardNumber + "/pin.json") + if err != nil { + exitWithError(err) + } + + // Build breadcrumbs + breadcrumbs := []response.Breadcrumb{ + breadcrumb("show", fmt.Sprintf("fizzy card show %s", cardNumber), "View card"), + breadcrumb("pins", "fizzy pin list", "List pinned cards"), + breadcrumb("pin", fmt.Sprintf("fizzy card pin %s", cardNumber), "Pin card"), + } + + data := resp.Data + if data == nil { + data = map[string]interface{}{} + } + printSuccessWithBreadcrumbs(data, "", breadcrumbs) + }, +} + var cardGoldenCmd = &cobra.Command{ Use: "golden CARD_NUMBER", Short: "Mark card as golden", @@ -1029,4 +1095,8 @@ func init() { // Golden cardCmd.AddCommand(cardGoldenCmd) cardCmd.AddCommand(cardUngoldenCmd) + + // Pin/Unpin + cardCmd.AddCommand(cardPinCmd) + cardCmd.AddCommand(cardUnpinCmd) } diff --git a/internal/commands/notification.go b/internal/commands/notification.go index 0af4b24..ac8082f 100644 --- a/internal/commands/notification.go +++ b/internal/commands/notification.go @@ -89,7 +89,7 @@ var notificationReadCmd = &cobra.Command{ } client := getClient() - resp, err := client.Post("/notifications/"+args[0]+"/read.json", nil) + resp, err := client.Post("/notifications/"+args[0]+"/reading.json", nil) if err != nil { exitWithError(err) } @@ -99,7 +99,11 @@ var notificationReadCmd = &cobra.Command{ breadcrumb("notifications", "fizzy notification list", "List notifications"), } - printSuccessWithBreadcrumbs(resp.Data, "", breadcrumbs) + data := resp.Data + if data == nil { + data = map[string]interface{}{} + } + printSuccessWithBreadcrumbs(data, "", breadcrumbs) }, } @@ -114,7 +118,7 @@ var notificationUnreadCmd = &cobra.Command{ } client := getClient() - resp, err := client.Post("/notifications/"+args[0]+"/unread.json", nil) + resp, err := client.Delete("/notifications/" + args[0] + "/reading.json") if err != nil { exitWithError(err) } @@ -124,7 +128,11 @@ var notificationUnreadCmd = &cobra.Command{ breadcrumb("notifications", "fizzy notification list", "List notifications"), } - printSuccessWithBreadcrumbs(resp.Data, "", breadcrumbs) + data := resp.Data + if data == nil { + data = map[string]interface{}{} + } + printSuccessWithBreadcrumbs(data, "", breadcrumbs) }, } diff --git a/internal/commands/notification_test.go b/internal/commands/notification_test.go index 18daa4a..91c6540 100644 --- a/internal/commands/notification_test.go +++ b/internal/commands/notification_test.go @@ -52,8 +52,8 @@ func TestNotificationRead(t *testing.T) { if result.ExitCode != 0 { t.Errorf("expected exit code 0, got %d", result.ExitCode) } - if mock.PostCalls[0].Path != "/notifications/notif-1/read.json" { - t.Errorf("expected path '/notifications/notif-1/read.json', got '%s'", mock.PostCalls[0].Path) + if mock.PostCalls[0].Path != "/notifications/notif-1/reading.json" { + t.Errorf("expected path '/notifications/notif-1/reading.json', got '%s'", mock.PostCalls[0].Path) } }) } @@ -61,7 +61,7 @@ func TestNotificationRead(t *testing.T) { func TestNotificationUnread(t *testing.T) { t.Run("marks notification as unread", func(t *testing.T) { mock := NewMockClient() - mock.PostResponse = &client.APIResponse{ + mock.DeleteResponse = &client.APIResponse{ StatusCode: 200, Data: map[string]interface{}{}, } @@ -77,8 +77,8 @@ func TestNotificationUnread(t *testing.T) { if result.ExitCode != 0 { t.Errorf("expected exit code 0, got %d", result.ExitCode) } - if mock.PostCalls[0].Path != "/notifications/notif-1/unread.json" { - t.Errorf("expected path '/notifications/notif-1/unread.json', got '%s'", mock.PostCalls[0].Path) + if mock.DeleteCalls[0].Path != "/notifications/notif-1/reading.json" { + t.Errorf("expected path '/notifications/notif-1/reading.json', got '%s'", mock.DeleteCalls[0].Path) } }) } diff --git a/internal/commands/pin.go b/internal/commands/pin.go new file mode 100644 index 0000000..f19f778 --- /dev/null +++ b/internal/commands/pin.go @@ -0,0 +1,52 @@ +package commands + +import ( + "fmt" + + "github.com/robzolkos/fizzy-cli/internal/response" + "github.com/spf13/cobra" +) + +var pinCmd = &cobra.Command{ + Use: "pin", + Short: "Manage pins", + Long: "Commands for managing your pinned cards.", +} + +var pinListCmd = &cobra.Command{ + Use: "list", + Short: "List pinned cards", + Long: "Lists your pinned cards (up to 100).", + Run: func(cmd *cobra.Command, args []string) { + if err := requireAuthAndAccount(); err != nil { + exitWithError(err) + } + + client := getClient() + resp, err := client.Get("/my/pins.json") + if err != nil { + exitWithError(err) + } + + // Build summary + count := 0 + if arr, ok := resp.Data.([]interface{}); ok { + count = len(arr) + } + summary := fmt.Sprintf("%d pinned cards", count) + + // Build breadcrumbs + breadcrumbs := []response.Breadcrumb{ + breadcrumb("show", "fizzy card show ", "View card details"), + breadcrumb("unpin", "fizzy card unpin ", "Unpin a card"), + breadcrumb("pin", "fizzy card pin ", "Pin a card"), + } + + printSuccessWithBreadcrumbs(resp.Data, summary, breadcrumbs) + }, +} + +func init() { + rootCmd.AddCommand(pinCmd) + pinCmd.AddCommand(pinListCmd) +} diff --git a/internal/commands/pin_test.go b/internal/commands/pin_test.go new file mode 100644 index 0000000..7f4bc60 --- /dev/null +++ b/internal/commands/pin_test.go @@ -0,0 +1,235 @@ +package commands + +import ( + "testing" + + "github.com/robzolkos/fizzy-cli/internal/client" + "github.com/robzolkos/fizzy-cli/internal/errors" +) + +func TestCardPin(t *testing.T) { + t.Run("pins a card", func(t *testing.T) { + mock := NewMockClient() + mock.PostResponse = &client.APIResponse{ + StatusCode: 200, + Data: map[string]interface{}{}, + } + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardPinCmd.Run(cardPinCmd, []string{"42"}) + }) + + if result.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", result.ExitCode) + } + if len(mock.PostCalls) != 1 { + t.Fatalf("expected 1 post call, got %d", len(mock.PostCalls)) + } + if mock.PostCalls[0].Path != "/cards/42/pin.json" { + t.Errorf("expected path '/cards/42/pin.json', got '%s'", mock.PostCalls[0].Path) + } + }) + + t.Run("requires authentication", func(t *testing.T) { + mock := NewMockClient() + result := SetTestMode(mock) + SetTestConfig("", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardPinCmd.Run(cardPinCmd, []string{"42"}) + }) + + if result.ExitCode != errors.ExitAuthFailure { + t.Errorf("expected exit code %d, got %d", errors.ExitAuthFailure, result.ExitCode) + } + }) + + t.Run("handles not found error", func(t *testing.T) { + mock := NewMockClient() + mock.PostError = errors.NewNotFoundError("Card not found") + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardPinCmd.Run(cardPinCmd, []string{"999"}) + }) + + if result.ExitCode != errors.ExitNotFound { + t.Errorf("expected exit code %d, got %d", errors.ExitNotFound, result.ExitCode) + } + }) +} + +func TestCardUnpin(t *testing.T) { + t.Run("unpins a card", func(t *testing.T) { + mock := NewMockClient() + mock.DeleteResponse = &client.APIResponse{ + StatusCode: 200, + Data: map[string]interface{}{}, + } + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardUnpinCmd.Run(cardUnpinCmd, []string{"42"}) + }) + + if result.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", result.ExitCode) + } + if len(mock.DeleteCalls) != 1 { + t.Fatalf("expected 1 delete call, got %d", len(mock.DeleteCalls)) + } + if mock.DeleteCalls[0].Path != "/cards/42/pin.json" { + t.Errorf("expected path '/cards/42/pin.json', got '%s'", mock.DeleteCalls[0].Path) + } + }) + + t.Run("requires authentication", func(t *testing.T) { + mock := NewMockClient() + result := SetTestMode(mock) + SetTestConfig("", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardUnpinCmd.Run(cardUnpinCmd, []string{"42"}) + }) + + if result.ExitCode != errors.ExitAuthFailure { + t.Errorf("expected exit code %d, got %d", errors.ExitAuthFailure, result.ExitCode) + } + }) + + t.Run("handles not found error", func(t *testing.T) { + mock := NewMockClient() + mock.DeleteError = errors.NewNotFoundError("Card not found") + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + cardUnpinCmd.Run(cardUnpinCmd, []string{"999"}) + }) + + if result.ExitCode != errors.ExitNotFound { + t.Errorf("expected exit code %d, got %d", errors.ExitNotFound, result.ExitCode) + } + }) +} + +func TestPinList(t *testing.T) { + t.Run("returns list of pinned cards", func(t *testing.T) { + mock := NewMockClient() + mock.GetResponse = &client.APIResponse{ + StatusCode: 200, + Data: []interface{}{ + map[string]interface{}{"id": "1", "title": "Pinned Card 1"}, + map[string]interface{}{"id": "2", "title": "Pinned Card 2"}, + }, + } + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + pinListCmd.Run(pinListCmd, []string{}) + }) + + if result.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", result.ExitCode) + } + if !result.Response.Success { + t.Error("expected success response") + } + if len(mock.GetCalls) != 1 { + t.Fatalf("expected 1 get call, got %d", len(mock.GetCalls)) + } + if mock.GetCalls[0].Path != "/my/pins.json" { + t.Errorf("expected path '/my/pins.json', got '%s'", mock.GetCalls[0].Path) + } + if result.Response.Summary != "2 pinned cards" { + t.Errorf("expected summary '2 pinned cards', got '%s'", result.Response.Summary) + } + }) + + t.Run("returns empty list", func(t *testing.T) { + mock := NewMockClient() + mock.GetResponse = &client.APIResponse{ + StatusCode: 200, + Data: []interface{}{}, + } + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + pinListCmd.Run(pinListCmd, []string{}) + }) + + if result.ExitCode != 0 { + t.Errorf("expected exit code 0, got %d", result.ExitCode) + } + if result.Response.Summary != "0 pinned cards" { + t.Errorf("expected summary '0 pinned cards', got '%s'", result.Response.Summary) + } + }) + + t.Run("requires authentication", func(t *testing.T) { + mock := NewMockClient() + result := SetTestMode(mock) + SetTestConfig("", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + pinListCmd.Run(pinListCmd, []string{}) + }) + + if result.ExitCode != errors.ExitAuthFailure { + t.Errorf("expected exit code %d, got %d", errors.ExitAuthFailure, result.ExitCode) + } + }) + + t.Run("requires account", func(t *testing.T) { + mock := NewMockClient() + result := SetTestMode(mock) + SetTestConfig("token", "", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + pinListCmd.Run(pinListCmd, []string{}) + }) + + if result.ExitCode != errors.ExitInvalidArgs { + t.Errorf("expected exit code %d, got %d", errors.ExitInvalidArgs, result.ExitCode) + } + }) + + t.Run("handles API error", func(t *testing.T) { + mock := NewMockClient() + mock.GetError = errors.NewError("Server error") + + result := SetTestMode(mock) + SetTestConfig("token", "account", "https://api.example.com") + defer ResetTestMode() + + RunTestCommand(func() { + pinListCmd.Run(pinListCmd, []string{}) + }) + + if result.ExitCode != errors.ExitError { + t.Errorf("expected exit code %d, got %d", errors.ExitError, result.ExitCode) + } + }) +}