From eeab7e95df7f1188615cafd32f990b9c6fd0f31e Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 27 Jan 2026 15:57:05 -0500 Subject: [PATCH 1/4] Add pins API support (pin, unpin, pin list) Adds card pin/unpin subcommands and a top-level pin list command to support the new pins API endpoints. --- README.md | 11 ++ e2e/tests/pin_test.go | 168 ++++++++++++++++++++++++ internal/commands/card.go | 70 ++++++++++ internal/commands/pin.go | 52 ++++++++ internal/commands/pin_test.go | 235 ++++++++++++++++++++++++++++++++++ 5 files changed, 536 insertions(+) create mode 100644 e2e/tests/pin_test.go create mode 100644 internal/commands/pin.go create mode 100644 internal/commands/pin_test.go 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/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/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/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) + } + }) +} From 356703a079a02ef312e964c2815d07c0fb948c50 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 27 Jan 2026 15:57:12 -0500 Subject: [PATCH 2/4] fix: use correct endpoints for notification read/unread The API uses POST/DELETE on /reading.json, not /read.json and /unread.json. Also handle nil response data from 204 No Content. --- internal/commands/notification.go | 16 ++++++++++++---- internal/commands/notification_test.go | 10 +++++----- 2 files changed, 17 insertions(+), 9 deletions(-) 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) } }) } From d3814053bb40afe8de96093059f2f1a5d428d24e Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 27 Jan 2026 15:57:18 -0500 Subject: [PATCH 3/4] fix: check HTTP status before parsing JSON in client When the API returns non-JSON error responses (e.g. HTML 401 pages), the JSON parse error was masking the proper HTTP status error. Moving the status check first ensures correct error codes are returned. --- internal/client/client.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) 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 } From 9e529b73c897d0925ad82ca72d5426b4d1004454 Mon Sep 17 00:00:00 2001 From: Rob Zolkos Date: Tue, 27 Jan 2026 15:57:25 -0500 Subject: [PATCH 4/4] fix: e2e tests that relied on absence of global config Tests for missing --board and auth login checked wrong config path or leaked the default board from the global config file. Use temp HOME to isolate, check both config paths, and drop pagination assertion for single-page results. --- e2e/tests/auth_test.go | 13 +++++++------ e2e/tests/card_test.go | 6 ++++-- e2e/tests/column_test.go | 6 ++++-- e2e/tests/error_test.go | 6 ++---- 4 files changed, 17 insertions(+), 14 deletions(-) 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 {