fizzy-cli 0.2.1 → 0.4.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 (42) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/IMPLEMENTATION_PLAN.md +338 -0
  3. package/bin/fizzy +0 -0
  4. package/cmd/board.go +1 -1
  5. package/cmd/card_assign.go +55 -0
  6. package/cmd/card_assign_test.go +130 -0
  7. package/cmd/card_golden.go +46 -0
  8. package/cmd/card_golden_test.go +92 -0
  9. package/cmd/card_not_now.go +46 -0
  10. package/cmd/card_not_now_test.go +92 -0
  11. package/cmd/card_tag.go +51 -0
  12. package/cmd/card_tag_test.go +112 -0
  13. package/cmd/card_triage.go +46 -0
  14. package/cmd/card_ungolden.go +46 -0
  15. package/cmd/card_ungolden_test.go +92 -0
  16. package/cmd/card_untriage.go +46 -0
  17. package/cmd/card_untriage_test.go +92 -0
  18. package/cmd/card_unwatch.go +46 -0
  19. package/cmd/card_unwatch_test.go +92 -0
  20. package/cmd/card_update.go +0 -1
  21. package/cmd/card_update_test.go +0 -2
  22. package/cmd/card_watch.go +46 -0
  23. package/cmd/card_watch_test.go +92 -0
  24. package/cmd/login.go +2 -1
  25. package/cmd/notification.go +14 -0
  26. package/cmd/notification_list.go +69 -0
  27. package/cmd/notification_list_test.go +288 -0
  28. package/cmd/notification_read.go +51 -0
  29. package/cmd/notification_read_all.go +38 -0
  30. package/cmd/notification_read_all_test.go +75 -0
  31. package/cmd/notification_read_test.go +138 -0
  32. package/cmd/notification_unread.go +44 -0
  33. package/cmd/notification_unread_test.go +99 -0
  34. package/cmd/tag.go +15 -0
  35. package/cmd/tag_list.go +47 -0
  36. package/cmd/tag_list_test.go +109 -0
  37. package/docs/API.md +180 -1
  38. package/go.mod +1 -1
  39. package/internal/api/client.go +275 -0
  40. package/internal/config/config.go +1 -0
  41. package/internal/ui/notification_list.go +27 -0
  42. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,34 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0 - 2026-01-19
4
+
5
+ ### Features
6
+
7
+ #### Card Management
8
+
9
+ - `fizzy card not-now <card_number>` - Move a card to "Not Now" status
10
+ - `fizzy card untriage <card_number>` - Send a card back to triage
11
+ - `fizzy card watch <card_number>` - Subscribe to card notifications
12
+ - `fizzy card unwatch <card_number>` - Unsubscribe from card notifications
13
+ - `fizzy card golden <card_number>` - Mark a card as golden
14
+ - `fizzy card ungolden <card_number>` - Remove golden status from a card
15
+
16
+ ## 0.3.0 - 2026-01-11
17
+
18
+ ### Features
19
+
20
+ #### Notification Management
21
+
22
+ - `fizzy notification list` - List all notifications with optional filtering
23
+ - `fizzy notification read <notification_id>` - Mark a notification as read and display it
24
+ - `fizzy notification unread <notification_id>` - Mark a notification as unread
25
+ - `fizzy notification read-all` - Mark all unread notifications as read
26
+
27
+ #### Card Management
28
+
29
+ - `fizzy card assign <card_number> <user_id>` - Assign or unassign a user to/from a card
30
+ - `fizzy card triage <card_number> <column_id>` - Move a card from triage into a column
31
+
3
32
  ## [0.2.1] - 2025-12-20
4
33
 
5
34
  ### Fixes
@@ -0,0 +1,338 @@
1
+ # Fizzy CLI Implementation Analysis & Plan
2
+
3
+ ## Current Implementation Status
4
+
5
+ ### ✅ Implemented Commands
6
+
7
+ #### Authentication & Setup
8
+ - `fizzy login` - Authenticate with Fizzy API
9
+ - `fizzy use --account <slug>` - Select account
10
+ - `fizzy use --board <name>` - Select board
11
+
12
+ #### Accounts
13
+ - `fizzy account list` - List available accounts
14
+
15
+ #### Boards
16
+ - `fizzy board list` - GET `/boards`
17
+ - `fizzy board show <id>` - GET `/boards/:id`
18
+ - `fizzy board create <name>` - POST `/boards`
19
+
20
+ #### Cards
21
+ - `fizzy card list` - GET `/cards`
22
+ - `fizzy card show <number>` - GET `/cards/:number`
23
+ - `fizzy card create <title>` - POST `/boards/:id/cards`
24
+ - `fizzy card update <number> <title>` - PUT `/cards/:number`
25
+ - `fizzy card delete <number>` - DELETE `/cards/:number`
26
+ - `fizzy card close <number>` - POST `/cards/:number/closure`
27
+ - `fizzy card reopen <number>` - DELETE `/cards/:number/closure`
28
+ - `fizzy card tag <number> <tag>` - POST `/cards/:number/taggings`
29
+ - `fizzy card assign <number> <user_id>` - POST `/cards/:number/assignments`
30
+ - `fizzy card triage <number> <column_id>` - POST `/cards/:number/triage`
31
+
32
+ #### Columns
33
+ - `fizzy column list` - GET `/boards/:id/columns`
34
+ - `fizzy column create <name>` - POST `/boards/:id/columns`
35
+
36
+ #### Tags
37
+ - `fizzy tag list` - GET `/tags`
38
+
39
+ #### Notifications
40
+ - `fizzy notification list` - GET `/notifications`
41
+ - `fizzy notification read <id>` - POST `/notifications/:id/reading`
42
+ - `fizzy notification unread <id>` - DELETE `/notifications/:id/reading`
43
+ - `fizzy notification read-all` - POST `/notifications/bulk_reading`
44
+
45
+ ---
46
+
47
+ ## 📋 Missing API Endpoints (Not Yet Implemented)
48
+
49
+ ### High Priority (Core Functionality)
50
+
51
+ #### Cards - Additional Operations
52
+ - [x] `POST /cards/:number/not_now` - Move card to "Not Now" status
53
+ - [x] `DELETE /cards/:number/triage` - Send card back to triage
54
+ - [x] `POST /cards/:number/watch` - Subscribe to card notifications
55
+ - [x] `DELETE /cards/:number/watch` - Unsubscribe from card notifications
56
+ - [x] `POST /cards/:number/goldness` - Mark card as golden
57
+ - [x] `DELETE /cards/:number/goldness` - Remove golden status
58
+
59
+ #### Comments
60
+ - [ ] `GET /cards/:number/comments` - List card comments
61
+ - [ ] `GET /cards/:number/comments/:id` - Get specific comment
62
+ - [ ] `POST /cards/:number/comments` - Create comment
63
+ - [ ] `PUT /cards/:number/comments/:id` - Update comment
64
+ - [ ] `DELETE /cards/:number/comments/:id` - Delete comment
65
+
66
+ #### Reactions
67
+ - [ ] `GET /cards/:number/comments/:id/reactions` - List reactions on comment
68
+ - [ ] `POST /cards/:number/comments/:id/reactions` - Add reaction
69
+ - [ ] `DELETE /cards/:number/comments/:id/reactions/:id` - Remove reaction
70
+
71
+ #### Steps (To-do items)
72
+ - [ ] `GET /cards/:number/steps/:id` - Get step details
73
+ - [ ] `POST /cards/:number/steps` - Create step
74
+ - [ ] `PUT /cards/:number/steps/:id` - Update step
75
+ - [ ] `DELETE /cards/:number/steps/:id` - Delete step
76
+
77
+ #### Board Management
78
+ - [ ] `PUT /boards/:id` - Update board settings
79
+ - [ ] `DELETE /boards/:id` - Delete board
80
+ - [ ] Support for board parameters: `all_access`, `auto_postpone_period`, `public_description`, `user_ids`
81
+
82
+ #### Columns
83
+ - [ ] `GET /boards/:id/columns/:id` - Get specific column
84
+ - [ ] `PUT /boards/:id/columns/:id` - Update column
85
+ - [ ] `DELETE /boards/:id/columns/:id` - Delete column
86
+
87
+ #### Users
88
+ - [ ] `GET /users` - List account users
89
+ - [ ] `GET /users/:id` - Get user details
90
+ - [ ] `PUT /users/:id` - Update user
91
+ - [ ] `DELETE /users/:id` - Deactivate user
92
+
93
+ #### Identity
94
+ - [ ] `GET /my/identity` - Get current user's accounts and identity (may already be partially implemented)
95
+
96
+ ### Medium Priority (Enhancement Features)
97
+
98
+ #### Card Filtering & Search
99
+ - Implement all card filter parameters:
100
+ - `board_ids[]` (partial - implemented)
101
+ - `tag_ids[]`
102
+ - `assignee_ids[]`
103
+ - `creator_ids[]`
104
+ - `closer_ids[]`
105
+ - `card_ids[]`
106
+ - `indexed_by` (all, closed, not_now, stalled, postponing_soon, golden)
107
+ - `sorted_by` (latest, newest, oldest)
108
+ - `assignment_status` (unassigned)
109
+ - `creation` (date filters)
110
+ - `closure` (date filters)
111
+ - `terms[]` (search)
112
+
113
+ #### Advanced Features
114
+ - [ ] File uploads for card images
115
+ - [ ] Rich text field support with HTML validation
116
+ - [ ] ActionText direct upload for embedded files
117
+ - [ ] ETag/Cache-Control header support for bandwidth optimization
118
+ - [ ] Pagination support (Link header rel="next")
119
+
120
+ ---
121
+
122
+ ## 🎯 Implementation Priority Roadmap
123
+
124
+ ### Phase 1: Core Card Enhancements (Quick wins)
125
+ These are card-related operations with minimal API client work since the pattern is established.
126
+
127
+ 1. ✅ **card not-now** - Move card to "Not Now" status
128
+ 2. ✅ **card untriage** - Send card back to triage
129
+ 3. ✅ **card watch** / **card unwatch** - Notification subscriptions
130
+ 4. ✅ **card golden** / **card ungolden** - Mark as golden
131
+
132
+ ### Phase 2: Comments & Reactions (Foundational)
133
+ Users often need to collaborate through comments.
134
+
135
+ 1. **comment list** - `GET /cards/:number/comments`
136
+ 2. **comment show** - `GET /cards/:number/comments/:id`
137
+ 3. **comment add** - `POST /cards/:number/comments`
138
+ 4. **comment update** - `PUT /cards/:number/comments/:id`
139
+ 5. **comment delete** - `DELETE /cards/:number/comments/:id`
140
+ 6. **reaction add** - `POST /cards/:number/comments/:id/reactions`
141
+ 7. **reaction list** - `GET /cards/:number/comments/:id/reactions`
142
+ 8. **reaction remove** - `DELETE /cards/:number/comments/:id/reactions/:id`
143
+
144
+ ### Phase 3: Steps (To-do items)
145
+ Essential for breaking down work.
146
+
147
+ 1. **step add** - `POST /cards/:number/steps`
148
+ 2. **step list** - Via card show (embedded)
149
+ 3. **step update** - `PUT /cards/:number/steps/:id`
150
+ 4. **step remove** - `DELETE /cards/:number/steps/:id`
151
+
152
+ ### Phase 4: Board & Column Management
153
+ Administrative operations.
154
+
155
+ 1. **board update** - `PUT /boards/:id`
156
+ 2. **board delete** - `DELETE /boards/:id`
157
+ 3. **column show** - `GET /boards/:id/columns/:id`
158
+ 4. **column update** - `PUT /boards/:id/columns/:id`
159
+ 5. **column delete** - `DELETE /boards/:id/columns/:id`
160
+
161
+ ### Phase 5: Users & Advanced Features
162
+ Less common but important for team management.
163
+
164
+ 1. **user list** - `GET /users`
165
+ 2. **user show** - `GET /users/:id`
166
+ 3. **user update** - `PUT /users/:id`
167
+ 4. **user deactivate** - `DELETE /users/:id`
168
+ 5. Card filtering enhancements
169
+ 6. File uploads & rich text
170
+ 7. Pagination support
171
+ 8. ETag caching
172
+
173
+ ---
174
+
175
+ ## 📊 Command Structure Pattern
176
+
177
+ All commands follow this pattern:
178
+
179
+ ```go
180
+ // File: cmd/resource_action.go
181
+
182
+ var resourceActionCmd = &cobra.Command{
183
+ Use: "action <args>",
184
+ Short: "Brief description",
185
+ Long: `Longer description`,
186
+ Args: cobra.ExactArgs(N), // or other validators
187
+ Run: func(cmd *cobra.Command, args []string) {
188
+ if err := handleResourceAction(cmd, args...); err != nil {
189
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
190
+ }
191
+ },
192
+ }
193
+
194
+ func handleResourceAction(cmd *cobra.Command, args ...string) error {
195
+ a := app.FromContext(cmd.Context())
196
+ if a == nil || a.Client == nil {
197
+ return fmt.Errorf("API client not available")
198
+ }
199
+
200
+ // API call
201
+ result, err := a.Client.MethodName(context.Background(), args...)
202
+ if err != nil {
203
+ return fmt.Errorf("operation failed: %w", err)
204
+ }
205
+
206
+ // Display result
207
+ return ui.DisplayResource(result)
208
+ }
209
+
210
+ func init() {
211
+ parentCmd.AddCommand(resourceActionCmd)
212
+ // Add flags if needed
213
+ }
214
+ ```
215
+
216
+ ---
217
+
218
+ ## 📝 API Client Work Required
219
+
220
+ ### New Methods Needed in `internal/api/client.go`
221
+
222
+ **Card Operations:**
223
+ ```go
224
+ func (c *Client) PostCardNotNow(ctx context.Context, cardNumber int) (bool, error)
225
+ func (c *Client) DeleteCardTriage(ctx context.Context, cardNumber int) (bool, error)
226
+ func (c *Client) PostCardWatch(ctx context.Context, cardNumber int) (bool, error)
227
+ func (c *Client) DeleteCardWatch(ctx context.Context, cardNumber int) (bool, error)
228
+ func (c *Client) PostCardGoldenness(ctx context.Context, cardNumber int) (bool, error)
229
+ func (c *Client) DeleteCardGoldenness(ctx context.Context, cardNumber int) (bool, error)
230
+ ```
231
+
232
+ **Comment Operations:**
233
+ ```go
234
+ func (c *Client) GetCardComments(ctx context.Context, cardNumber int) ([]Comment, error)
235
+ func (c *Client) GetCardComment(ctx context.Context, cardNumber int, commentID string) (*Comment, error)
236
+ func (c *Client) PostCardComment(ctx context.Context, cardNumber int, body string) (bool, error)
237
+ func (c *Client) PutCardComment(ctx context.Context, cardNumber int, commentID string, body string) (bool, error)
238
+ func (c *Client) DeleteCardComment(ctx context.Context, cardNumber int, commentID string) (bool, error)
239
+ ```
240
+
241
+ **Reaction Operations:**
242
+ ```go
243
+ func (c *Client) GetCommentReactions(ctx context.Context, cardNumber int, commentID string) ([]Reaction, error)
244
+ func (c *Client) PostCommentReaction(ctx context.Context, cardNumber int, commentID string, content string) (bool, error)
245
+ func (c *Client) DeleteCommentReaction(ctx context.Context, cardNumber int, commentID string, reactionID string) (bool, error)
246
+ ```
247
+
248
+ **Step Operations:**
249
+ ```go
250
+ func (c *Client) PostCardStep(ctx context.Context, cardNumber int, content string) (bool, error)
251
+ func (c *Client) GetCardStep(ctx context.Context, cardNumber int, stepID string) (*Step, error)
252
+ func (c *Client) PutCardStep(ctx context.Context, cardNumber int, stepID string, payload UpdateStepPayload) (bool, error)
253
+ func (c *Client) DeleteCardStep(ctx context.Context, cardNumber int, stepID string) (bool, error)
254
+ ```
255
+
256
+ **Board Operations:**
257
+ ```go
258
+ func (c *Client) PutBoard(ctx context.Context, boardID string, payload UpdateBoardPayload) (bool, error)
259
+ func (c *Client) DeleteBoard(ctx context.Context, boardID string) (bool, error)
260
+ ```
261
+
262
+ **Column Operations:**
263
+ ```go
264
+ func (c *Client) GetColumn(ctx context.Context, columnID string) (*Column, error)
265
+ func (c *Client) PutColumn(ctx context.Context, columnID string, payload UpdateColumnPayload) (bool, error)
266
+ func (c *Client) DeleteColumn(ctx context.Context, columnID string) (bool, error)
267
+ ```
268
+
269
+ **User Operations:**
270
+ ```go
271
+ func (c *Client) GetUsers(ctx context.Context) ([]User, error)
272
+ func (c *Client) GetUser(ctx context.Context, userID string) (*User, error)
273
+ func (c *Client) PutUser(ctx context.Context, userID string, payload UpdateUserPayload) (bool, error)
274
+ func (c *Client) DeleteUser(ctx context.Context, userID string) (bool, error)
275
+ ```
276
+
277
+ ### New Data Types Needed
278
+
279
+ ```go
280
+ type Comment struct {
281
+ ID string
282
+ CreatedAt string
283
+ UpdatedAt string
284
+ Body map[string]string // {plain_text, html}
285
+ Creator User
286
+ Card CardReference
287
+ ReactionsURL string
288
+ URL string
289
+ }
290
+
291
+ type Reaction struct {
292
+ ID string
293
+ Content string
294
+ Reacter User
295
+ URL string
296
+ }
297
+
298
+ type Step struct {
299
+ ID string
300
+ Content string
301
+ Completed bool
302
+ }
303
+
304
+ type UpdateStepPayload struct {
305
+ Content string `json:"content,omitempty"`
306
+ Completed bool `json:"completed,omitempty"`
307
+ }
308
+
309
+ type UpdateBoardPayload struct {
310
+ Name string `json:"name,omitempty"`
311
+ AllAccess bool `json:"all_access,omitempty"`
312
+ AutoPostponePeriod int `json:"auto_postpone_period,omitempty"`
313
+ PublicDescription string `json:"public_description,omitempty"`
314
+ UserIDs []string `json:"user_ids,omitempty"`
315
+ }
316
+
317
+ type UpdateColumnPayload struct {
318
+ Name string `json:"name,omitempty"`
319
+ Color *Color `json:"color,omitempty"`
320
+ }
321
+
322
+ type UpdateUserPayload struct {
323
+ Name string `json:"name,omitempty"`
324
+ Avatar string `json:"avatar,omitempty"` // file upload
325
+ }
326
+ ```
327
+
328
+ ---
329
+
330
+ ## 🔍 Next Steps Recommendation
331
+
332
+ **Start with Phase 1 (Card Enhancements)** as they:
333
+ - Have proven command patterns already in codebase
334
+ - Require minimal new API client code
335
+ - Deliver immediate user value
336
+ - Build confidence before tackling complex features like comments
337
+
338
+ **Suggested first command:** `card not-now` - single simple API call, no new data types needed.
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 cardGoldenCmd = &cobra.Command{
13
+ Use: "golden <card_number>",
14
+ Short: "Mark a card as golden",
15
+ Long: `Mark an existing card as golden`,
16
+ Args: cobra.ExactArgs(1),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleGoldenCard(cmd, args[0]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleGoldenCard(cmd *cobra.Command, cardNumber 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.PostCardGoldenness(context.Background(), cardNum)
36
+ if err != nil {
37
+ return fmt.Errorf("marking card as golden: %w", err)
38
+ }
39
+
40
+ fmt.Printf("✓ Card #%d marked as golden\n", cardNum)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardCmd.AddCommand(cardGoldenCmd)
46
+ }