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.
- package/CHANGELOG.md +46 -0
- package/bin/fizzy +0 -0
- package/cmd/board.go +1 -1
- package/cmd/board_create.go +1 -1
- package/cmd/board_delete.go +40 -0
- package/cmd/board_delete_test.go +121 -0
- package/cmd/board_show.go +40 -0
- package/cmd/board_show_test.go +113 -0
- package/cmd/board_update.go +72 -0
- package/cmd/board_update_test.go +233 -0
- package/cmd/card_assign.go +1 -1
- package/cmd/card_close.go +1 -1
- package/cmd/card_create.go +1 -1
- package/cmd/card_delete.go +1 -1
- package/cmd/card_golden.go +1 -1
- package/cmd/card_list.go +62 -1
- package/cmd/card_list_test.go +225 -0
- package/cmd/card_not_now.go +1 -1
- package/cmd/card_reaction.go +13 -0
- package/cmd/card_reaction_create.go +46 -0
- package/cmd/card_reaction_create_test.go +148 -0
- package/cmd/card_reaction_delete.go +46 -0
- package/cmd/card_reaction_delete_test.go +112 -0
- package/cmd/card_reaction_list.go +51 -0
- package/cmd/card_reaction_list_test.go +127 -0
- package/cmd/card_reopen.go +1 -1
- package/cmd/card_tag.go +1 -1
- package/cmd/card_triage.go +1 -1
- package/cmd/card_ungolden.go +1 -1
- package/cmd/card_untriage.go +1 -1
- package/cmd/card_unwatch.go +1 -1
- package/cmd/card_update.go +1 -1
- package/cmd/card_watch.go +1 -1
- package/cmd/column_create.go +1 -1
- package/cmd/column_delete.go +40 -0
- package/cmd/column_delete_test.go +121 -0
- package/cmd/column_show.go +40 -0
- package/cmd/column_show_test.go +111 -0
- package/cmd/column_update.go +67 -0
- package/cmd/column_update_test.go +198 -0
- package/cmd/comment_create.go +1 -1
- package/cmd/comment_delete.go +1 -1
- package/cmd/comment_update.go +1 -1
- package/cmd/login.go +12 -12
- package/cmd/notification_unread.go +1 -1
- package/cmd/reaction.go +2 -2
- package/cmd/reaction_create.go +1 -1
- package/cmd/reaction_delete.go +1 -1
- package/cmd/step_create.go +1 -1
- package/cmd/step_delete.go +1 -1
- package/cmd/step_update.go +1 -1
- package/cmd/user.go +22 -0
- package/cmd/user_deactivate.go +40 -0
- package/cmd/user_deactivate_test.go +121 -0
- package/cmd/user_list.go +44 -0
- package/cmd/user_list_test.go +126 -0
- package/cmd/user_show.go +40 -0
- package/cmd/user_show_test.go +110 -0
- package/cmd/user_update.go +71 -0
- package/cmd/user_update_test.go +177 -0
- package/docs/API.md +63 -2
- package/internal/api/boards.go +34 -0
- package/internal/api/cards.go +40 -6
- package/internal/api/columns.go +63 -0
- package/internal/api/reactions.go +61 -0
- package/internal/api/types.go +17 -0
- package/internal/api/users.go +75 -0
- package/internal/ui/board_show.go +17 -0
- package/internal/ui/column_show.go +16 -0
- package/internal/ui/format.go +14 -1
- package/internal/ui/user_list.go +19 -0
- package/internal/ui/user_show.go +23 -0
- package/package.json +1 -1
package/cmd/card_assign.go
CHANGED
|
@@ -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.
|
|
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.
|
|
40
|
+
fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d closed successfully\n", cardNum)
|
|
41
41
|
return nil
|
|
42
42
|
}
|
|
43
43
|
|
package/cmd/card_create.go
CHANGED
|
@@ -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.
|
|
57
|
+
fmt.Fprintf(cmd.OutOrStdout(), "✓ Card '%s' created successfully\n", title)
|
|
58
58
|
return nil
|
|
59
59
|
}
|
|
60
60
|
|
package/cmd/card_delete.go
CHANGED
|
@@ -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.
|
|
40
|
+
fmt.Fprintf(cmd.OutOrStdout(), "✓ Card #%d deleted successfully\n", cardNum)
|
|
41
41
|
return nil
|
|
42
42
|
}
|
|
43
43
|
|
package/cmd/card_golden.go
CHANGED
|
@@ -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.
|
|
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:
|
|
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
|
}
|
package/cmd/card_list_test.go
CHANGED
|
@@ -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
|
+
}
|
package/cmd/card_not_now.go
CHANGED
|
@@ -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.
|
|
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
|
+
}
|