fizzy-cli 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0 - 2026-01-11
4
+
5
+ ### Features
6
+
7
+ #### Notification Management
8
+
9
+ - `fizzy notification list` - List all notifications with optional filtering
10
+ - `fizzy notification read <notification_id>` - Mark a notification as read and display it
11
+ - `fizzy notification unread <notification_id>` - Mark a notification as unread
12
+ - `fizzy notification read-all` - Mark all unread notifications as read
13
+
14
+ #### Card Management
15
+
16
+ - `fizzy card assign <card_number> <user_id>` - Assign or unassign a user to/from a card
17
+ - `fizzy card triage <card_number> <column_id>` - Move a card from triage into a column
18
+
3
19
  ## [0.2.1] - 2025-12-20
4
20
 
5
21
  ### Fixes
package/bin/fizzy CHANGED
Binary file
package/cmd/board.go CHANGED
@@ -13,7 +13,7 @@ import (
13
13
  var boardCmd = &cobra.Command{
14
14
  Use: "board",
15
15
  Short: "Show the currently selected board",
16
- Long: `Display the name and ID of the currently selected board.
16
+ Long: `Display the name and ID of the currently selected board.
17
17
 
18
18
  Use subcommands to list, create, or manage boards:
19
19
  fizzy board list List all boards
@@ -0,0 +1,55 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strconv"
7
+
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var cardAssignCmd = &cobra.Command{
13
+ Use: "assign <card_number> <user_id>",
14
+ Short: "Assign a user to a card",
15
+ Long: `Assign or unassign a user to/from a card.
16
+
17
+ Use "me" as the user_id to assign the card to yourself.`,
18
+ Args: cobra.ExactArgs(2),
19
+ Run: func(cmd *cobra.Command, args []string) {
20
+ if err := handleAssignCard(cmd, args[0], args[1]); err != nil {
21
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
22
+ }
23
+ },
24
+ }
25
+
26
+ func handleAssignCard(cmd *cobra.Command, cardNumber, userID string) error {
27
+ cardNum, err := strconv.Atoi(cardNumber)
28
+ if err != nil {
29
+ return fmt.Errorf("invalid card number: %w", err)
30
+ }
31
+
32
+ a := app.FromContext(cmd.Context())
33
+ if a == nil || a.Client == nil {
34
+ return fmt.Errorf("API client not available")
35
+ }
36
+
37
+ if userID == "me" {
38
+ if a.Config.CurrentUserID == "" {
39
+ return fmt.Errorf("current user ID not available, please run 'fizzy login' first")
40
+ }
41
+ userID = a.Config.CurrentUserID
42
+ }
43
+
44
+ _, err = a.Client.PostCardAssignments(context.Background(), cardNum, userID)
45
+ if err != nil {
46
+ return fmt.Errorf("assigning card: %w", err)
47
+ }
48
+
49
+ fmt.Printf("✓ Card #%d assignment toggled for user %s\n", cardNum, userID)
50
+ return nil
51
+ }
52
+
53
+ func init() {
54
+ cardCmd.AddCommand(cardAssignCmd)
55
+ }
@@ -0,0 +1,130 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "net/http"
6
+ "net/http/httptest"
7
+ "testing"
8
+
9
+ "github.com/rogeriopvl/fizzy/internal/app"
10
+ "github.com/rogeriopvl/fizzy/internal/config"
11
+ "github.com/rogeriopvl/fizzy/internal/testutil"
12
+ )
13
+
14
+ func TestCardAssignCommandSuccess(t *testing.T) {
15
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16
+ if r.URL.Path != "/cards/123/assignments" {
17
+ t.Errorf("expected /cards/123/assignments, got %s", r.URL.Path)
18
+ }
19
+ if r.Method != http.MethodPost {
20
+ t.Errorf("expected POST, got %s", r.Method)
21
+ }
22
+
23
+ auth := r.Header.Get("Authorization")
24
+ if auth != "Bearer test-token" {
25
+ t.Errorf("expected Bearer test-token, got %s", auth)
26
+ }
27
+
28
+ w.WriteHeader(http.StatusNoContent)
29
+ }))
30
+ defer server.Close()
31
+
32
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
33
+ testApp := &app.App{Client: client}
34
+
35
+ cmd := cardAssignCmd
36
+ cmd.SetContext(testApp.ToContext(context.Background()))
37
+
38
+ if err := handleAssignCard(cmd, "123", "user-id-123"); err != nil {
39
+ t.Fatalf("handleAssignCard failed: %v", err)
40
+ }
41
+ }
42
+
43
+ func TestCardAssignCommandWithMe(t *testing.T) {
44
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45
+ if r.URL.Path != "/cards/123/assignments" {
46
+ t.Errorf("expected /cards/123/assignments, got %s", r.URL.Path)
47
+ }
48
+ w.WriteHeader(http.StatusNoContent)
49
+ }))
50
+ defer server.Close()
51
+
52
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
53
+ cfg := &config.Config{CurrentUserID: "my-user-id"}
54
+ testApp := &app.App{Client: client, Config: cfg}
55
+
56
+ cmd := cardAssignCmd
57
+ cmd.SetContext(testApp.ToContext(context.Background()))
58
+
59
+ if err := handleAssignCard(cmd, "123", "me"); err != nil {
60
+ t.Fatalf("handleAssignCard with 'me' failed: %v", err)
61
+ }
62
+ }
63
+
64
+ func TestCardAssignCommandAPIError(t *testing.T) {
65
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66
+ w.WriteHeader(http.StatusNotFound)
67
+ w.Write([]byte("Card not found"))
68
+ }))
69
+ defer server.Close()
70
+
71
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
72
+ testApp := &app.App{Client: client}
73
+
74
+ cmd := cardAssignCmd
75
+ cmd.SetContext(testApp.ToContext(context.Background()))
76
+
77
+ err := handleAssignCard(cmd, "999", "user-id-123")
78
+ if err == nil {
79
+ t.Errorf("expected error for API failure")
80
+ }
81
+ if err.Error() != "assigning card: unexpected status code 404: Card not found" {
82
+ t.Errorf("expected API error, got %v", err)
83
+ }
84
+ }
85
+
86
+ func TestCardAssignCommandInvalidCardNumber(t *testing.T) {
87
+ testApp := &app.App{}
88
+
89
+ cmd := cardAssignCmd
90
+ cmd.SetContext(testApp.ToContext(context.Background()))
91
+
92
+ err := handleAssignCard(cmd, "not-a-number", "user-id-123")
93
+ if err == nil {
94
+ t.Errorf("expected error for invalid card number")
95
+ }
96
+ if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
97
+ t.Errorf("expected invalid card number error, got %v", err)
98
+ }
99
+ }
100
+
101
+ func TestCardAssignCommandNoClient(t *testing.T) {
102
+ testApp := &app.App{}
103
+
104
+ cmd := cardAssignCmd
105
+ cmd.SetContext(testApp.ToContext(context.Background()))
106
+
107
+ err := handleAssignCard(cmd, "123", "user-id-123")
108
+ if err == nil {
109
+ t.Errorf("expected error when client not available")
110
+ }
111
+ if err.Error() != "API client not available" {
112
+ t.Errorf("expected 'client not available' error, got %v", err)
113
+ }
114
+ }
115
+
116
+ func TestCardAssignCommandMeWithoutUserID(t *testing.T) {
117
+ client := testutil.NewTestClient("http://localhost", "", "", "test-token")
118
+ testApp := &app.App{Client: client, Config: &config.Config{}}
119
+
120
+ cmd := cardAssignCmd
121
+ cmd.SetContext(testApp.ToContext(context.Background()))
122
+
123
+ err := handleAssignCard(cmd, "123", "me")
124
+ if err == nil {
125
+ t.Errorf("expected error when using 'me' without current user ID")
126
+ }
127
+ if err.Error() != "current user ID not available, please run 'fizzy login' first" {
128
+ t.Errorf("expected 'current user ID not available' error, got %v", err)
129
+ }
130
+ }
@@ -0,0 +1,46 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "strconv"
7
+
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var cardTriageCmd = &cobra.Command{
13
+ Use: "triage <card_number> <column_id>",
14
+ Short: "Move a card from triage into a column",
15
+ Long: `Move a card from triage into a specified column`,
16
+ Args: cobra.ExactArgs(2),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleTriageCard(cmd, args[0], args[1]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleTriageCard(cmd *cobra.Command, cardNumber string, columnID string) error {
25
+ cardNum, err := strconv.Atoi(cardNumber)
26
+ if err != nil {
27
+ return fmt.Errorf("invalid card number: %w", err)
28
+ }
29
+
30
+ a := app.FromContext(cmd.Context())
31
+ if a == nil || a.Client == nil {
32
+ return fmt.Errorf("API client not available")
33
+ }
34
+
35
+ _, err = a.Client.PostCardTriage(context.Background(), cardNum, columnID)
36
+ if err != nil {
37
+ return fmt.Errorf("triaging card: %w", err)
38
+ }
39
+
40
+ fmt.Printf("✓ Card #%d moved to column successfully\n", cardNum)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardCmd.AddCommand(cardTriageCmd)
46
+ }
@@ -41,7 +41,6 @@ func handleUpdateCard(cmd *cobra.Command, cardNumber string) error {
41
41
  return fmt.Errorf("API client not available")
42
42
  }
43
43
 
44
- // Validate that at least one field is provided for update
45
44
  if updateTitle == "" && updateDescription == "" && updateStatus == "" && len(updateTagIDs) == 0 && updateLastActiveAt == "" {
46
45
  return fmt.Errorf("must provide at least one flag to update (--title, --description, --status, --tag-id, or --last-active-at)")
47
46
  }
@@ -49,7 +49,6 @@ func TestCardUpdateCommand(t *testing.T) {
49
49
  cmd := cardUpdateCmd
50
50
  cmd.SetContext(testApp.ToContext(context.Background()))
51
51
 
52
- // Reset flags to defaults
53
52
  updateTitle = "Updated card title"
54
53
  updateDescription = "Updated description"
55
54
  updateStatus = "published"
@@ -130,7 +129,6 @@ func TestCardUpdateCommandNoFlags(t *testing.T) {
130
129
  cmd := cardUpdateCmd
131
130
  cmd.SetContext(testApp.ToContext(context.Background()))
132
131
 
133
- // Reset all flags
134
132
  updateTitle = ""
135
133
  updateDescription = ""
136
134
  updateStatus = ""
package/cmd/login.go CHANGED
@@ -47,8 +47,9 @@ func handleLogin(cmd *cobra.Command) error {
47
47
 
48
48
  fmt.Printf("\nSelected account: %s (%s)\n", selected.Name, selected.Slug)
49
49
 
50
- // Save the selected account to config
50
+ // Save the selected account and current user ID to config
51
51
  a.Config.SelectedAccount = selected.Slug
52
+ a.Config.CurrentUserID = selected.User.ID
52
53
  if err := a.Config.Save(); err != nil {
53
54
  return fmt.Errorf("saving config: %w", err)
54
55
  }
@@ -0,0 +1,14 @@
1
+ // Package cmd
2
+ package cmd
3
+
4
+ import "github.com/spf13/cobra"
5
+
6
+ var notificationCmd = &cobra.Command{
7
+ Use: "notification",
8
+ Short: "Manage notifications",
9
+ Long: `Manage notifications in Fizzy`,
10
+ }
11
+
12
+ func init() {
13
+ rootCmd.AddCommand(notificationCmd)
14
+ }
@@ -0,0 +1,69 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/rogeriopvl/fizzy/internal/api"
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/rogeriopvl/fizzy/internal/ui"
10
+ "github.com/spf13/cobra"
11
+ )
12
+
13
+ var notificationListCmd = &cobra.Command{
14
+ Use: "list",
15
+ Short: "List all notifications",
16
+ Long: `Retrieve and display all notifications from Fizzy`,
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleListNotifications(cmd); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleListNotifications(cmd *cobra.Command) error {
25
+ a := app.FromContext(cmd.Context())
26
+ if a == nil || a.Client == nil {
27
+ return fmt.Errorf("API client not available")
28
+ }
29
+
30
+ notifications, err := a.Client.GetNotifications(context.Background())
31
+ if err != nil {
32
+ return fmt.Errorf("fetching notifications: %w", err)
33
+ }
34
+
35
+ read, _ := cmd.Flags().GetBool("read")
36
+ unread, _ := cmd.Flags().GetBool("unread")
37
+
38
+ filtered := filterNotifications(notifications, read, unread)
39
+
40
+ if len(filtered) == 0 {
41
+ fmt.Println("No notifications found")
42
+ return nil
43
+ }
44
+
45
+ return ui.DisplayNotifications(filtered)
46
+ }
47
+
48
+ func filterNotifications(notifications []api.Notification, read bool, unread bool) []api.Notification {
49
+ if !read && !unread {
50
+ return notifications
51
+ }
52
+
53
+ var filtered []api.Notification
54
+ for _, notification := range notifications {
55
+ if read && notification.Read {
56
+ filtered = append(filtered, notification)
57
+ } else if unread && !notification.Read {
58
+ filtered = append(filtered, notification)
59
+ }
60
+ }
61
+
62
+ return filtered
63
+ }
64
+
65
+ func init() {
66
+ notificationCmd.AddCommand(notificationListCmd)
67
+ notificationListCmd.Flags().BoolP("read", "r", false, "Show only read notifications")
68
+ notificationListCmd.Flags().BoolP("unread", "u", false, "Show only unread notifications")
69
+ }
@@ -0,0 +1,288 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "testing"
9
+
10
+ "github.com/rogeriopvl/fizzy/internal/api"
11
+ "github.com/rogeriopvl/fizzy/internal/app"
12
+ "github.com/rogeriopvl/fizzy/internal/config"
13
+ "github.com/rogeriopvl/fizzy/internal/testutil"
14
+ )
15
+
16
+ func TestNotificationListCommand(t *testing.T) {
17
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
+ if r.URL.Path != "/notifications" {
19
+ t.Errorf("expected /notifications, got %s", r.URL.Path)
20
+ }
21
+ if r.Method != http.MethodGet {
22
+ t.Errorf("expected GET, got %s", r.Method)
23
+ }
24
+
25
+ auth := r.Header.Get("Authorization")
26
+ if auth != "Bearer test-token" {
27
+ t.Errorf("expected Bearer test-token, got %s", auth)
28
+ }
29
+
30
+ w.Header().Set("Content-Type", "application/json")
31
+ response := []api.Notification{
32
+ {
33
+ ID: "notif-123",
34
+ Read: false,
35
+ ReadAt: "",
36
+ CreatedAt: "2025-01-01T00:00:00Z",
37
+ Title: "Plain text mentions",
38
+ Body: "Assigned to self",
39
+ Creator: api.User{
40
+ ID: "user-123",
41
+ Name: "David Heinemeier Hansson",
42
+ Email: "david@example.com",
43
+ Role: "owner",
44
+ Active: true,
45
+ CreatedAt: "2025-12-05T19:36:35.401Z",
46
+ URL: "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7",
47
+ },
48
+ Card: api.CardReference{
49
+ ID: "card-123",
50
+ Title: "Plain text mentions",
51
+ Status: "published",
52
+ URL: "http://fizzy.localhost:3006/897362094/cards/3",
53
+ },
54
+ URL: "http://fizzy.localhost:3006/897362094/notifications/03f5va03bpuvkcjemcxl73ho2",
55
+ },
56
+ {
57
+ ID: "notif-456",
58
+ Read: true,
59
+ ReadAt: "2025-01-02T00:00:00Z",
60
+ CreatedAt: "2025-01-01T12:00:00Z",
61
+ Title: "Comment reply",
62
+ Body: "Someone replied to your comment",
63
+ Creator: api.User{
64
+ ID: "user-456",
65
+ Name: "Jason Fried",
66
+ Email: "jason@example.com",
67
+ Role: "member",
68
+ Active: true,
69
+ CreatedAt: "2025-12-05T19:36:35.419Z",
70
+ URL: "http://fizzy.localhost:3006/897362094/users/03f5v9zjysoy0fqs9yg0ei3hq",
71
+ },
72
+ Card: api.CardReference{
73
+ ID: "card-456",
74
+ Title: "Fix bug",
75
+ Status: "in_progress",
76
+ URL: "http://fizzy.localhost:3006/897362094/cards/4",
77
+ },
78
+ URL: "http://fizzy.localhost:3006/897362094/notifications/notif-456",
79
+ },
80
+ }
81
+ json.NewEncoder(w).Encode(response)
82
+ }))
83
+ defer server.Close()
84
+
85
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
86
+ testApp := &app.App{
87
+ Client: client,
88
+ Config: &config.Config{SelectedBoard: "board-123"},
89
+ }
90
+
91
+ cmd := notificationListCmd
92
+ cmd.SetContext(testApp.ToContext(context.Background()))
93
+
94
+ if err := handleListNotifications(cmd); err != nil {
95
+ t.Fatalf("handleListNotifications failed: %v", err)
96
+ }
97
+ }
98
+
99
+ func TestNotificationListCommandNoNotifications(t *testing.T) {
100
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
101
+ w.Header().Set("Content-Type", "application/json")
102
+ json.NewEncoder(w).Encode([]api.Notification{})
103
+ }))
104
+ defer server.Close()
105
+
106
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
107
+ testApp := &app.App{
108
+ Client: client,
109
+ Config: &config.Config{SelectedBoard: "board-123"},
110
+ }
111
+
112
+ cmd := notificationListCmd
113
+ cmd.SetContext(testApp.ToContext(context.Background()))
114
+
115
+ if err := handleListNotifications(cmd); err != nil {
116
+ t.Fatalf("handleListNotifications failed: %v", err)
117
+ }
118
+ }
119
+
120
+ func TestNotificationListCommandAPIError(t *testing.T) {
121
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
122
+ w.WriteHeader(http.StatusInternalServerError)
123
+ w.Write([]byte("Internal Server Error"))
124
+ }))
125
+ defer server.Close()
126
+
127
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
128
+ testApp := &app.App{
129
+ Client: client,
130
+ Config: &config.Config{SelectedBoard: "board-123"},
131
+ }
132
+
133
+ cmd := notificationListCmd
134
+ cmd.SetContext(testApp.ToContext(context.Background()))
135
+
136
+ err := handleListNotifications(cmd)
137
+ if err == nil {
138
+ t.Errorf("expected error for API failure")
139
+ }
140
+ if err.Error() != "fetching notifications: unexpected status code 500: Internal Server Error" {
141
+ t.Errorf("expected API error, got %v", err)
142
+ }
143
+ }
144
+
145
+ func TestNotificationListCommandNoClient(t *testing.T) {
146
+ testApp := &app.App{}
147
+
148
+ cmd := notificationListCmd
149
+ cmd.SetContext(testApp.ToContext(context.Background()))
150
+
151
+ err := handleListNotifications(cmd)
152
+ if err == nil {
153
+ t.Errorf("expected error when client not available")
154
+ }
155
+ if err.Error() != "API client not available" {
156
+ t.Errorf("expected 'client not available' error, got %v", err)
157
+ }
158
+ }
159
+
160
+ func TestNotificationListCommandWithReadFilter(t *testing.T) {
161
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162
+ w.Header().Set("Content-Type", "application/json")
163
+ response := []api.Notification{
164
+ {
165
+ ID: "notif-123",
166
+ Read: false,
167
+ CreatedAt: "2025-01-01T00:00:00Z",
168
+ Title: "Unread notification",
169
+ Body: "This should be filtered out",
170
+ Creator: api.User{
171
+ ID: "user-123",
172
+ Name: "David Heinemeier Hansson",
173
+ Email: "david@example.com",
174
+ Role: "owner",
175
+ Active: true,
176
+ CreatedAt: "2025-12-05T19:36:35.401Z",
177
+ },
178
+ Card: api.CardReference{
179
+ ID: "card-123",
180
+ Title: "Test card",
181
+ Status: "published",
182
+ },
183
+ },
184
+ {
185
+ ID: "notif-456",
186
+ Read: true,
187
+ ReadAt: "2025-01-02T00:00:00Z",
188
+ CreatedAt: "2025-01-01T12:00:00Z",
189
+ Title: "Read notification",
190
+ Body: "This should be shown",
191
+ Creator: api.User{
192
+ ID: "user-456",
193
+ Name: "Jason Fried",
194
+ Email: "jason@example.com",
195
+ Role: "member",
196
+ Active: true,
197
+ CreatedAt: "2025-12-05T19:36:35.419Z",
198
+ },
199
+ Card: api.CardReference{
200
+ ID: "card-456",
201
+ Title: "Another card",
202
+ Status: "in_progress",
203
+ },
204
+ },
205
+ }
206
+ json.NewEncoder(w).Encode(response)
207
+ }))
208
+ defer server.Close()
209
+
210
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
211
+ testApp := &app.App{
212
+ Client: client,
213
+ Config: &config.Config{SelectedBoard: "board-123"},
214
+ }
215
+
216
+ cmd := notificationListCmd
217
+ cmd.SetContext(testApp.ToContext(context.Background()))
218
+ cmd.Flags().Set("read", "true")
219
+
220
+ if err := handleListNotifications(cmd); err != nil {
221
+ t.Fatalf("handleListNotifications failed: %v", err)
222
+ }
223
+ }
224
+
225
+ func TestNotificationListCommandWithUnreadFilter(t *testing.T) {
226
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
227
+ w.Header().Set("Content-Type", "application/json")
228
+ response := []api.Notification{
229
+ {
230
+ ID: "notif-123",
231
+ Read: false,
232
+ CreatedAt: "2025-01-01T00:00:00Z",
233
+ Title: "Unread notification",
234
+ Body: "This should be shown",
235
+ Creator: api.User{
236
+ ID: "user-123",
237
+ Name: "David Heinemeier Hansson",
238
+ Email: "david@example.com",
239
+ Role: "owner",
240
+ Active: true,
241
+ CreatedAt: "2025-12-05T19:36:35.401Z",
242
+ },
243
+ Card: api.CardReference{
244
+ ID: "card-123",
245
+ Title: "Test card",
246
+ Status: "published",
247
+ },
248
+ },
249
+ {
250
+ ID: "notif-456",
251
+ Read: true,
252
+ ReadAt: "2025-01-02T00:00:00Z",
253
+ CreatedAt: "2025-01-01T12:00:00Z",
254
+ Title: "Read notification",
255
+ Body: "This should be filtered out",
256
+ Creator: api.User{
257
+ ID: "user-456",
258
+ Name: "Jason Fried",
259
+ Email: "jason@example.com",
260
+ Role: "member",
261
+ Active: true,
262
+ CreatedAt: "2025-12-05T19:36:35.419Z",
263
+ },
264
+ Card: api.CardReference{
265
+ ID: "card-456",
266
+ Title: "Another card",
267
+ Status: "in_progress",
268
+ },
269
+ },
270
+ }
271
+ json.NewEncoder(w).Encode(response)
272
+ }))
273
+ defer server.Close()
274
+
275
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
276
+ testApp := &app.App{
277
+ Client: client,
278
+ Config: &config.Config{SelectedBoard: "board-123"},
279
+ }
280
+
281
+ cmd := notificationListCmd
282
+ cmd.SetContext(testApp.ToContext(context.Background()))
283
+ cmd.Flags().Set("unread", "true")
284
+
285
+ if err := handleListNotifications(cmd); err != nil {
286
+ t.Fatalf("handleListNotifications failed: %v", err)
287
+ }
288
+ }