fizzy-cli 0.6.1 → 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.
Files changed (73) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/bin/fizzy +0 -0
  3. package/cmd/board.go +1 -1
  4. package/cmd/board_create.go +1 -1
  5. package/cmd/board_delete.go +40 -0
  6. package/cmd/board_delete_test.go +121 -0
  7. package/cmd/board_show.go +40 -0
  8. package/cmd/board_show_test.go +113 -0
  9. package/cmd/board_update.go +72 -0
  10. package/cmd/board_update_test.go +233 -0
  11. package/cmd/card_assign.go +1 -1
  12. package/cmd/card_close.go +1 -1
  13. package/cmd/card_create.go +1 -1
  14. package/cmd/card_delete.go +1 -1
  15. package/cmd/card_golden.go +1 -1
  16. package/cmd/card_list.go +62 -1
  17. package/cmd/card_list_test.go +225 -0
  18. package/cmd/card_not_now.go +1 -1
  19. package/cmd/card_reaction.go +13 -0
  20. package/cmd/card_reaction_create.go +46 -0
  21. package/cmd/card_reaction_create_test.go +148 -0
  22. package/cmd/card_reaction_delete.go +46 -0
  23. package/cmd/card_reaction_delete_test.go +112 -0
  24. package/cmd/card_reaction_list.go +51 -0
  25. package/cmd/card_reaction_list_test.go +127 -0
  26. package/cmd/card_reopen.go +1 -1
  27. package/cmd/card_tag.go +1 -1
  28. package/cmd/card_triage.go +1 -1
  29. package/cmd/card_ungolden.go +1 -1
  30. package/cmd/card_untriage.go +1 -1
  31. package/cmd/card_unwatch.go +1 -1
  32. package/cmd/card_update.go +1 -1
  33. package/cmd/card_watch.go +1 -1
  34. package/cmd/column_create.go +1 -1
  35. package/cmd/column_delete.go +40 -0
  36. package/cmd/column_delete_test.go +121 -0
  37. package/cmd/column_show.go +40 -0
  38. package/cmd/column_show_test.go +111 -0
  39. package/cmd/column_update.go +67 -0
  40. package/cmd/column_update_test.go +198 -0
  41. package/cmd/comment_create.go +1 -1
  42. package/cmd/comment_delete.go +1 -1
  43. package/cmd/comment_update.go +1 -1
  44. package/cmd/login.go +12 -12
  45. package/cmd/notification_unread.go +1 -1
  46. package/cmd/reaction.go +2 -2
  47. package/cmd/reaction_create.go +1 -1
  48. package/cmd/reaction_delete.go +1 -1
  49. package/cmd/step_create.go +1 -1
  50. package/cmd/step_delete.go +1 -1
  51. package/cmd/step_update.go +1 -1
  52. package/cmd/user.go +22 -0
  53. package/cmd/user_deactivate.go +40 -0
  54. package/cmd/user_deactivate_test.go +121 -0
  55. package/cmd/user_list.go +44 -0
  56. package/cmd/user_list_test.go +126 -0
  57. package/cmd/user_show.go +40 -0
  58. package/cmd/user_show_test.go +110 -0
  59. package/cmd/user_update.go +71 -0
  60. package/cmd/user_update_test.go +177 -0
  61. package/docs/API.md +63 -2
  62. package/internal/api/boards.go +34 -0
  63. package/internal/api/cards.go +40 -6
  64. package/internal/api/columns.go +63 -0
  65. package/internal/api/reactions.go +61 -0
  66. package/internal/api/types.go +17 -0
  67. package/internal/api/users.go +75 -0
  68. package/internal/ui/board_show.go +17 -0
  69. package/internal/ui/column_show.go +16 -0
  70. package/internal/ui/format.go +14 -1
  71. package/internal/ui/user_list.go +19 -0
  72. package/internal/ui/user_show.go +23 -0
  73. package/package.json +1 -1
@@ -46,7 +46,7 @@ func handleAssignCard(cmd *cobra.Command, cardNumber, userID string) error {
46
46
  return fmt.Errorf("assigning card: %w", err)
47
47
  }
48
48
 
49
- fmt.Printf("✓ Card #%d assignment toggled for user %s\n", cardNum, userID)
49
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d assignment toggled for user %s\n", cardNum, userID)
50
50
  return nil
51
51
  }
52
52
 
package/cmd/card_close.go CHANGED
@@ -37,7 +37,7 @@ func handleCloseCard(cmd *cobra.Command, cardNumber string) error {
37
37
  return fmt.Errorf("closing card: %w", err)
38
38
  }
39
39
 
40
- fmt.Printf("✓ Card #%d closed successfully\n", cardNum)
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d closed successfully\n", cardNum)
41
41
  return nil
42
42
  }
43
43
 
@@ -54,7 +54,7 @@ func handleCreateCard(cmd *cobra.Command) error {
54
54
  return fmt.Errorf("creating card: %w", err)
55
55
  }
56
56
 
57
- fmt.Printf("✓ Card '%s' created successfully\n", title)
57
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card '%s' created successfully\n", title)
58
58
  return nil
59
59
  }
60
60
 
@@ -37,7 +37,7 @@ func handleDeleteCard(cmd *cobra.Command, cardNumber string) error {
37
37
  return fmt.Errorf("deleting card: %w", err)
38
38
  }
39
39
 
40
- fmt.Printf("✓ Card #%d deleted successfully\n", cardNum)
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d deleted successfully\n", cardNum)
41
41
  return nil
42
42
  }
43
43
 
@@ -37,7 +37,7 @@ func handleGoldenCard(cmd *cobra.Command, cardNumber string) error {
37
37
  return fmt.Errorf("marking card as golden: %w", err)
38
38
  }
39
39
 
40
- fmt.Printf("✓ Card #%d marked as golden\n", cardNum)
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d marked as golden\n", cardNum)
41
41
  return nil
42
42
  }
43
43
 
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
+ }
@@ -37,7 +37,7 @@ func handleNotNowCard(cmd *cobra.Command, cardNumber string) error {
37
37
  return fmt.Errorf("moving card to not now: %w", err)
38
38
  }
39
39
 
40
- fmt.Printf("✓ Card #%d moved to Not Now successfully\n", cardNum)
40
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d moved to Not Now successfully\n", cardNum)
41
41
  return nil
42
42
  }
43
43
 
@@ -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
+ }