fizzy-cli 0.7.0 → 0.8.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/.env ADDED
@@ -0,0 +1 @@
1
+ FIZZY_ACCESS_TOKEN=S7FiVcT1K3WgwXc7uzBdQRim
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.8.0 - 2026-02-11
4
+
5
+ ### Features
6
+
7
+ #### Card Management
8
+
9
+ - `fizzy card list` now supports filtering by board, tags, assignees, creators, and closure status
10
+ - Enhanced card listing with additional query parameters for better filtering capabilities
11
+
12
+ #### Reaction Management
13
+
14
+ - `fizzy reaction list <card_number>` - List all reactions on a card
15
+ - `fizzy reaction create <card_number> <emoji>` - Add a reaction to a card
16
+ - `fizzy reaction delete <card_number> <reaction_id>` - Remove a reaction from a card
17
+
18
+ #### User Management
19
+
20
+ - `fizzy user list` - List all users in the account
21
+ - `fizzy user show <user_id>` - Display details for a specific user
22
+ - `fizzy user update <user_id>` - Update user properties (name, avatar)
23
+ - `fizzy user deactivate <user_id>` - Deactivate a user account
24
+
25
+ ### Improvements
26
+
27
+ - Updated API specification to latest version
28
+
3
29
  ## 0.7.0 - 2026-01-26
4
30
 
5
31
  ### Features
package/bin/fizzy CHANGED
Binary file
package/cmd/card_list.go CHANGED
@@ -13,7 +13,20 @@ import (
13
13
  var cardListCmd = &cobra.Command{
14
14
  Use: "list",
15
15
  Short: "List all cards",
16
- Long: `Retrieve and display all cards from Fizzy`,
16
+ Long: `Retrieve and display cards from Fizzy with optional filters.
17
+
18
+ Filter options:
19
+ --tag <id> Filter by tag ID (can be used multiple times)
20
+ --assignee <id> Filter by assignee user ID (can be used multiple times)
21
+ --creator <id> Filter by creator user ID (can be used multiple times)
22
+ --closer <id> Filter by user who closed the card (can be used multiple times)
23
+ --card <id> Filter to specific card ID (can be used multiple times)
24
+ --indexed-by Filter by status: all, closed, not_now, stalled, postponing_soon, golden
25
+ --sorted-by Sort order: latest, newest, oldest
26
+ --unassigned Show only unassigned cards
27
+ --created-in Filter by creation date: today, yesterday, thisweek, lastweek, thismonth, lastmonth, thisyear, lastyear
28
+ --closed-in Filter by closure date: today, yesterday, thisweek, lastweek, thismonth, lastmonth, thisyear, lastyear
29
+ --search Search terms (can be used multiple times)`,
17
30
  Run: func(cmd *cobra.Command, args []string) {
18
31
  if err := handleListCards(cmd); err != nil {
19
32
  fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
@@ -35,6 +48,42 @@ func handleListCards(cmd *cobra.Command) error {
35
48
  BoardIDs: []string{a.Config.SelectedBoard},
36
49
  }
37
50
 
51
+ if tags, _ := cmd.Flags().GetStringSlice("tag"); len(tags) > 0 {
52
+ filters.TagIDs = tags
53
+ }
54
+ if assignees, _ := cmd.Flags().GetStringSlice("assignee"); len(assignees) > 0 {
55
+ filters.AssigneeIDs = assignees
56
+ }
57
+ if creators, _ := cmd.Flags().GetStringSlice("creator"); len(creators) > 0 {
58
+ filters.CreatorIDs = creators
59
+ }
60
+ if closers, _ := cmd.Flags().GetStringSlice("closer"); len(closers) > 0 {
61
+ filters.CloserIDs = closers
62
+ }
63
+ if cardIDs, _ := cmd.Flags().GetStringSlice("card"); len(cardIDs) > 0 {
64
+ filters.CardIDs = cardIDs
65
+ }
66
+ if searches, _ := cmd.Flags().GetStringSlice("search"); len(searches) > 0 {
67
+ filters.Terms = searches
68
+ }
69
+
70
+ if indexedBy, _ := cmd.Flags().GetString("indexed-by"); indexedBy != "" {
71
+ filters.IndexedBy = indexedBy
72
+ }
73
+ if sortedBy, _ := cmd.Flags().GetString("sorted-by"); sortedBy != "" {
74
+ filters.SortedBy = sortedBy
75
+ }
76
+ if createdIn, _ := cmd.Flags().GetString("created-in"); createdIn != "" {
77
+ filters.CreationStatus = createdIn
78
+ }
79
+ if closedIn, _ := cmd.Flags().GetString("closed-in"); closedIn != "" {
80
+ filters.ClosureStatus = closedIn
81
+ }
82
+
83
+ if unassigned, _ := cmd.Flags().GetBool("unassigned"); unassigned {
84
+ filters.AssignmentStatus = "unassigned"
85
+ }
86
+
38
87
  cards, err := a.Client.GetCards(context.Background(), filters)
39
88
  if err != nil {
40
89
  return fmt.Errorf("fetching cards: %w", err)
@@ -49,5 +98,17 @@ func handleListCards(cmd *cobra.Command) error {
49
98
  }
50
99
 
51
100
  func init() {
101
+ cardListCmd.Flags().StringSliceP("tag", "t", []string{}, "Filter by tag ID (can be used multiple times)")
102
+ cardListCmd.Flags().StringSliceP("assignee", "a", []string{}, "Filter by assignee user ID (can be used multiple times)")
103
+ cardListCmd.Flags().StringSlice("creator", []string{}, "Filter by creator user ID (can be used multiple times)")
104
+ cardListCmd.Flags().StringSlice("closer", []string{}, "Filter by closer user ID (can be used multiple times)")
105
+ cardListCmd.Flags().StringSlice("card", []string{}, "Filter to specific card ID (can be used multiple times)")
106
+ cardListCmd.Flags().String("indexed-by", "", "Filter by status: all, closed, not_now, stalled, postponing_soon, golden")
107
+ cardListCmd.Flags().String("sorted-by", "", "Sort order: latest, newest, oldest")
108
+ cardListCmd.Flags().BoolP("unassigned", "u", false, "Show only unassigned cards")
109
+ cardListCmd.Flags().String("created-in", "", "Filter by creation date")
110
+ cardListCmd.Flags().String("closed-in", "", "Filter by closure date")
111
+ cardListCmd.Flags().StringSliceP("search", "s", []string{}, "Search terms (can be used multiple times)")
112
+
52
113
  cardCmd.AddCommand(cardListCmd)
53
114
  }
@@ -11,8 +11,25 @@ import (
11
11
  "github.com/rogeriopvl/fizzy/internal/app"
12
12
  "github.com/rogeriopvl/fizzy/internal/config"
13
13
  "github.com/rogeriopvl/fizzy/internal/testutil"
14
+ "github.com/spf13/cobra"
14
15
  )
15
16
 
17
+ func newCardListCmd() *cobra.Command {
18
+ cmd := &cobra.Command{}
19
+ cmd.Flags().StringSliceP("tag", "t", []string{}, "Filter by tag ID (can be used multiple times)")
20
+ cmd.Flags().StringSliceP("assignee", "a", []string{}, "Filter by assignee user ID (can be used multiple times)")
21
+ cmd.Flags().StringSlice("creator", []string{}, "Filter by creator user ID (can be used multiple times)")
22
+ cmd.Flags().StringSlice("closer", []string{}, "Filter by closer user ID (can be used multiple times)")
23
+ cmd.Flags().StringSlice("card", []string{}, "Filter to specific card ID (can be used multiple times)")
24
+ cmd.Flags().String("indexed-by", "", "Filter by status: all, closed, not_now, stalled, postponing_soon, golden")
25
+ cmd.Flags().String("sorted-by", "", "Sort order: latest, newest, oldest")
26
+ cmd.Flags().BoolP("unassigned", "u", false, "Show only unassigned cards")
27
+ cmd.Flags().String("created-in", "", "Filter by creation date")
28
+ cmd.Flags().String("closed-in", "", "Filter by closure date")
29
+ cmd.Flags().StringSliceP("search", "s", []string{}, "Search terms (can be used multiple times)")
30
+ return cmd
31
+ }
32
+
16
33
  func TestCardListCommand(t *testing.T) {
17
34
  server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
35
  if r.URL.Path != "/cards" {
@@ -146,3 +163,211 @@ func TestCardListCommandNoClient(t *testing.T) {
146
163
  t.Errorf("expected 'client not available' error, got %v", err)
147
164
  }
148
165
  }
166
+
167
+ func TestCardListCommandWithTagFilter(t *testing.T) {
168
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
169
+ tagIDs := r.URL.Query()["tag_ids[]"]
170
+ if len(tagIDs) != 2 || tagIDs[0] != "tag-123" || tagIDs[1] != "tag-456" {
171
+ t.Errorf("expected tag_ids[]=tag-123&tag_ids[]=tag-456, got %v", tagIDs)
172
+ }
173
+
174
+ w.Header().Set("Content-Type", "application/json")
175
+ json.NewEncoder(w).Encode([]api.Card{})
176
+ }))
177
+ defer server.Close()
178
+
179
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
180
+ testApp := &app.App{
181
+ Client: client,
182
+ Config: &config.Config{SelectedBoard: "board-123"},
183
+ }
184
+
185
+ cmd := newCardListCmd()
186
+ cmd.SetContext(testApp.ToContext(context.Background()))
187
+ cmd.ParseFlags([]string{"--tag", "tag-123", "--tag", "tag-456"})
188
+
189
+ if err := handleListCards(cmd); err != nil {
190
+ t.Fatalf("handleListCards failed: %v", err)
191
+ }
192
+ }
193
+
194
+ func TestCardListCommandWithAssigneeFilter(t *testing.T) {
195
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
196
+ assigneeIDs := r.URL.Query()["assignee_ids[]"]
197
+ if len(assigneeIDs) != 1 || assigneeIDs[0] != "user-123" {
198
+ t.Errorf("expected assignee_ids[]=user-123, got %v", assigneeIDs)
199
+ }
200
+
201
+ w.Header().Set("Content-Type", "application/json")
202
+ json.NewEncoder(w).Encode([]api.Card{})
203
+ }))
204
+ defer server.Close()
205
+
206
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
207
+ testApp := &app.App{
208
+ Client: client,
209
+ Config: &config.Config{SelectedBoard: "board-123"},
210
+ }
211
+
212
+ cmd := newCardListCmd()
213
+ cmd.SetContext(testApp.ToContext(context.Background()))
214
+ cmd.ParseFlags([]string{"--assignee", "user-123"})
215
+
216
+ if err := handleListCards(cmd); err != nil {
217
+ t.Fatalf("handleListCards failed: %v", err)
218
+ }
219
+ }
220
+
221
+ func TestCardListCommandWithIndexedByFilter(t *testing.T) {
222
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
223
+ indexedBy := r.URL.Query().Get("indexed_by")
224
+ if indexedBy != "closed" {
225
+ t.Errorf("expected indexed_by=closed, got %s", indexedBy)
226
+ }
227
+
228
+ w.Header().Set("Content-Type", "application/json")
229
+ json.NewEncoder(w).Encode([]api.Card{})
230
+ }))
231
+ defer server.Close()
232
+
233
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
234
+ testApp := &app.App{
235
+ Client: client,
236
+ Config: &config.Config{SelectedBoard: "board-123"},
237
+ }
238
+
239
+ cmd := newCardListCmd()
240
+ cmd.SetContext(testApp.ToContext(context.Background()))
241
+ cmd.ParseFlags([]string{"--indexed-by", "closed"})
242
+
243
+ if err := handleListCards(cmd); err != nil {
244
+ t.Fatalf("handleListCards failed: %v", err)
245
+ }
246
+ }
247
+
248
+ func TestCardListCommandWithSortedByFilter(t *testing.T) {
249
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
250
+ sortedBy := r.URL.Query().Get("sorted_by")
251
+ if sortedBy != "newest" {
252
+ t.Errorf("expected sorted_by=newest, got %s", sortedBy)
253
+ }
254
+
255
+ w.Header().Set("Content-Type", "application/json")
256
+ json.NewEncoder(w).Encode([]api.Card{})
257
+ }))
258
+ defer server.Close()
259
+
260
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
261
+ testApp := &app.App{
262
+ Client: client,
263
+ Config: &config.Config{SelectedBoard: "board-123"},
264
+ }
265
+
266
+ cmd := newCardListCmd()
267
+ cmd.SetContext(testApp.ToContext(context.Background()))
268
+ cmd.ParseFlags([]string{"--sorted-by", "newest"})
269
+
270
+ if err := handleListCards(cmd); err != nil {
271
+ t.Fatalf("handleListCards failed: %v", err)
272
+ }
273
+ }
274
+
275
+ func TestCardListCommandWithUnassignedFilter(t *testing.T) {
276
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
277
+ assignmentStatus := r.URL.Query().Get("assignment_status")
278
+ if assignmentStatus != "unassigned" {
279
+ t.Errorf("expected assignment_status=unassigned, got %s", assignmentStatus)
280
+ }
281
+
282
+ w.Header().Set("Content-Type", "application/json")
283
+ json.NewEncoder(w).Encode([]api.Card{})
284
+ }))
285
+ defer server.Close()
286
+
287
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
288
+ testApp := &app.App{
289
+ Client: client,
290
+ Config: &config.Config{SelectedBoard: "board-123"},
291
+ }
292
+
293
+ cmd := newCardListCmd()
294
+ cmd.SetContext(testApp.ToContext(context.Background()))
295
+ cmd.ParseFlags([]string{"--unassigned"})
296
+
297
+ if err := handleListCards(cmd); err != nil {
298
+ t.Fatalf("handleListCards failed: %v", err)
299
+ }
300
+ }
301
+
302
+ func TestCardListCommandWithSearchFilter(t *testing.T) {
303
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
304
+ terms := r.URL.Query()["terms[]"]
305
+ if len(terms) != 2 || terms[0] != "bug" || terms[1] != "critical" {
306
+ t.Errorf("expected terms[]=bug&terms[]=critical, got %v", terms)
307
+ }
308
+
309
+ w.Header().Set("Content-Type", "application/json")
310
+ json.NewEncoder(w).Encode([]api.Card{})
311
+ }))
312
+ defer server.Close()
313
+
314
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
315
+ testApp := &app.App{
316
+ Client: client,
317
+ Config: &config.Config{SelectedBoard: "board-123"},
318
+ }
319
+
320
+ cmd := newCardListCmd()
321
+ cmd.SetContext(testApp.ToContext(context.Background()))
322
+ cmd.ParseFlags([]string{"--search", "bug", "--search", "critical"})
323
+
324
+ if err := handleListCards(cmd); err != nil {
325
+ t.Fatalf("handleListCards failed: %v", err)
326
+ }
327
+ }
328
+
329
+ func TestCardListCommandWithMultipleFilters(t *testing.T) {
330
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
331
+ boardIDs := r.URL.Query()["board_ids[]"]
332
+ if len(boardIDs) == 0 || boardIDs[0] != "board-123" {
333
+ t.Errorf("expected board_ids[]=board-123, got %v", boardIDs)
334
+ }
335
+
336
+ tagIDs := r.URL.Query()["tag_ids[]"]
337
+ if len(tagIDs) != 1 || tagIDs[0] != "tag-123" {
338
+ t.Errorf("expected tag_ids[]=tag-123, got %v", tagIDs)
339
+ }
340
+
341
+ assigneeIDs := r.URL.Query()["assignee_ids[]"]
342
+ if len(assigneeIDs) != 1 || assigneeIDs[0] != "user-456" {
343
+ t.Errorf("expected assignee_ids[]=user-456, got %v", assigneeIDs)
344
+ }
345
+
346
+ sortedBy := r.URL.Query().Get("sorted_by")
347
+ if sortedBy != "latest" {
348
+ t.Errorf("expected sorted_by=latest, got %s", sortedBy)
349
+ }
350
+
351
+ w.Header().Set("Content-Type", "application/json")
352
+ json.NewEncoder(w).Encode([]api.Card{})
353
+ }))
354
+ defer server.Close()
355
+
356
+ client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
357
+ testApp := &app.App{
358
+ Client: client,
359
+ Config: &config.Config{SelectedBoard: "board-123"},
360
+ }
361
+
362
+ cmd := newCardListCmd()
363
+ cmd.SetContext(testApp.ToContext(context.Background()))
364
+ cmd.ParseFlags([]string{
365
+ "--tag", "tag-123",
366
+ "--assignee", "user-456",
367
+ "--sorted-by", "latest",
368
+ })
369
+
370
+ if err := handleListCards(cmd); err != nil {
371
+ t.Fatalf("handleListCards failed: %v", err)
372
+ }
373
+ }
@@ -0,0 +1,13 @@
1
+ package cmd
2
+
3
+ import "github.com/spf13/cobra"
4
+
5
+ var cardReactionCmd = &cobra.Command{
6
+ Use: "reaction",
7
+ Short: "Manage card reactions",
8
+ Long: `Manage reactions (boosts) on cards in Fizzy`,
9
+ }
10
+
11
+ func init() {
12
+ cardCmd.AddCommand(cardReactionCmd)
13
+ }
@@ -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 cardReactionCreateCmd = &cobra.Command{
13
+ Use: "create <card_number> <emoji>",
14
+ Short: "Create a reaction on a card",
15
+ Long: `Create an emoji reaction (boost) on a card`,
16
+ Args: cobra.ExactArgs(2),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleCreateCardReaction(cmd, args[0], args[1]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleCreateCardReaction(cmd *cobra.Command, cardNumber, emoji 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.PostCardReaction(context.Background(), cardNum, emoji)
36
+ if err != nil {
37
+ return fmt.Errorf("creating reaction: %w", err)
38
+ }
39
+
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Reaction %s created successfully\n", emoji)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardReactionCmd.AddCommand(cardReactionCreateCmd)
46
+ }
@@ -0,0 +1,148 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "io"
7
+ "net/http"
8
+ "net/http/httptest"
9
+ "testing"
10
+
11
+ "github.com/rogeriopvl/fizzy/internal/api"
12
+ "github.com/rogeriopvl/fizzy/internal/app"
13
+ "github.com/rogeriopvl/fizzy/internal/testutil"
14
+ )
15
+
16
+ func TestCardReactionCreateCommandSuccess(t *testing.T) {
17
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
+ if r.URL.Path != "/cards/123/reactions" {
19
+ t.Errorf("expected /cards/123/reactions, got %s", r.URL.Path)
20
+ }
21
+ if r.Method != http.MethodPost {
22
+ t.Errorf("expected POST, 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
+ body, _ := io.ReadAll(r.Body)
31
+ var payload map[string]map[string]string
32
+ if err := json.Unmarshal(body, &payload); err != nil {
33
+ t.Fatalf("failed to unmarshal request body: %v", err)
34
+ }
35
+
36
+ reactionPayload := payload["reaction"]
37
+ if reactionPayload["content"] != "👍" {
38
+ t.Errorf("expected content '👍', got %s", reactionPayload["content"])
39
+ }
40
+
41
+ w.Header().Set("Content-Type", "application/json")
42
+ w.WriteHeader(http.StatusCreated)
43
+ response := api.Reaction{
44
+ ID: "reaction-123",
45
+ Content: "👍",
46
+ Reacter: api.User{ID: "user-1", Name: "John Doe"},
47
+ }
48
+ json.NewEncoder(w).Encode(response)
49
+ }))
50
+ defer server.Close()
51
+
52
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
53
+ testApp := &app.App{Client: client}
54
+
55
+ cmd := cardReactionCreateCmd
56
+ cmd.SetContext(testApp.ToContext(context.Background()))
57
+
58
+ if err := handleCreateCardReaction(cmd, "123", "👍"); err != nil {
59
+ t.Fatalf("handleCreateCardReaction failed: %v", err)
60
+ }
61
+ }
62
+
63
+ func TestCardReactionCreateCommandInvalidCardNumber(t *testing.T) {
64
+ testApp := &app.App{}
65
+
66
+ cmd := cardReactionCreateCmd
67
+ cmd.SetContext(testApp.ToContext(context.Background()))
68
+
69
+ err := handleCreateCardReaction(cmd, "not-a-number", "👍")
70
+ if err == nil {
71
+ t.Errorf("expected error for invalid card number")
72
+ }
73
+ if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
74
+ t.Errorf("expected invalid card number error, got %v", err)
75
+ }
76
+ }
77
+
78
+ func TestCardReactionCreateCommandNoClient(t *testing.T) {
79
+ testApp := &app.App{}
80
+
81
+ cmd := cardReactionCreateCmd
82
+ cmd.SetContext(testApp.ToContext(context.Background()))
83
+
84
+ err := handleCreateCardReaction(cmd, "123", "👍")
85
+ if err == nil {
86
+ t.Errorf("expected error when client not available")
87
+ }
88
+ if err.Error() != "API client not available" {
89
+ t.Errorf("expected 'client not available' error, got %v", err)
90
+ }
91
+ }
92
+
93
+ func TestCardReactionCreateCommandAPIError(t *testing.T) {
94
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
95
+ w.WriteHeader(http.StatusNotFound)
96
+ w.Write([]byte("Card not found"))
97
+ }))
98
+ defer server.Close()
99
+
100
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
101
+ testApp := &app.App{Client: client}
102
+
103
+ cmd := cardReactionCreateCmd
104
+ cmd.SetContext(testApp.ToContext(context.Background()))
105
+
106
+ err := handleCreateCardReaction(cmd, "123", "👍")
107
+ if err == nil {
108
+ t.Errorf("expected error for API failure")
109
+ }
110
+ if err.Error() != "creating reaction: unexpected status code 404: Card not found" {
111
+ t.Errorf("expected API error, got %v", err)
112
+ }
113
+ }
114
+
115
+ func TestCardReactionCreateCommandDifferentEmoji(t *testing.T) {
116
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
117
+ body, _ := io.ReadAll(r.Body)
118
+ var payload map[string]map[string]string
119
+ if err := json.Unmarshal(body, &payload); err != nil {
120
+ t.Fatalf("failed to unmarshal request body: %v", err)
121
+ }
122
+
123
+ reactionPayload := payload["reaction"]
124
+ if reactionPayload["content"] != "🎉" {
125
+ t.Errorf("expected content '🎉', got %s", reactionPayload["content"])
126
+ }
127
+
128
+ w.Header().Set("Content-Type", "application/json")
129
+ w.WriteHeader(http.StatusCreated)
130
+ response := api.Reaction{
131
+ ID: "reaction-456",
132
+ Content: "🎉",
133
+ Reacter: api.User{ID: "user-2", Name: "Jane Doe"},
134
+ }
135
+ json.NewEncoder(w).Encode(response)
136
+ }))
137
+ defer server.Close()
138
+
139
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
140
+ testApp := &app.App{Client: client}
141
+
142
+ cmd := cardReactionCreateCmd
143
+ cmd.SetContext(testApp.ToContext(context.Background()))
144
+
145
+ if err := handleCreateCardReaction(cmd, "123", "🎉"); err != nil {
146
+ t.Fatalf("handleCreateCardReaction failed: %v", err)
147
+ }
148
+ }
@@ -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 cardReactionDeleteCmd = &cobra.Command{
13
+ Use: "delete <card_number> <reaction_id>",
14
+ Short: "Delete a reaction from a card",
15
+ Long: `Remove your reaction (boost) from a card`,
16
+ Args: cobra.ExactArgs(2),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleDeleteCardReaction(cmd, args[0], args[1]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleDeleteCardReaction(cmd *cobra.Command, cardNumber, reactionID 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.DeleteCardReaction(context.Background(), cardNum, reactionID)
36
+ if err != nil {
37
+ return fmt.Errorf("deleting reaction: %w", err)
38
+ }
39
+
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Reaction deleted successfully\n")
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardReactionCmd.AddCommand(cardReactionDeleteCmd)
46
+ }