fizzy-cli 0.5.0 → 0.6.1
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 +35 -0
- package/cmd/card_update.go +24 -14
- package/cmd/comment_create.go +2 -2
- package/cmd/comment_delete.go +46 -0
- package/cmd/comment_delete_test.go +92 -0
- package/cmd/comment_update.go +51 -0
- package/cmd/comment_update_test.go +130 -0
- package/cmd/reaction.go +13 -0
- package/cmd/reaction_create.go +46 -0
- package/cmd/reaction_create_test.go +113 -0
- package/cmd/reaction_delete.go +46 -0
- package/cmd/reaction_delete_test.go +92 -0
- package/cmd/reaction_list.go +51 -0
- package/cmd/reaction_list_test.go +125 -0
- package/cmd/step.go +14 -0
- package/cmd/step_create.go +53 -0
- package/cmd/step_create_test.go +171 -0
- package/cmd/step_delete.go +46 -0
- package/cmd/step_delete_test.go +92 -0
- package/cmd/step_update.go +66 -0
- package/cmd/step_update_test.go +190 -0
- package/internal/api/boards.go +59 -0
- package/internal/api/cards.go +288 -0
- package/internal/api/client.go +1 -712
- package/internal/api/columns.go +50 -0
- package/internal/api/comments.go +108 -0
- package/internal/api/identity.go +24 -0
- package/internal/api/notifications.go +89 -0
- package/internal/api/reactions.go +69 -0
- package/internal/api/steps.go +101 -0
- package/internal/api/tags.go +24 -0
- package/internal/api/types.go +178 -0
- package/internal/ui/reaction_list.go +14 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1 - 2026-01-26
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- Fixed API client to not parse empty responses from POST endpoints, this was
|
|
8
|
+
causing the create sub-command of comment, reaction and step to show an error
|
|
9
|
+
message, even though the resource was actually created on the API.
|
|
10
|
+
|
|
11
|
+
## 0.6.0 - 2026-01-25
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
#### Reaction Management
|
|
16
|
+
|
|
17
|
+
- `fizzy reaction list <card_number> <comment_id>` - List all reactions on a comment
|
|
18
|
+
- `fizzy reaction add <card_number> <comment_id> <emoji>` - Add a reaction to a comment
|
|
19
|
+
- `fizzy reaction remove <card_number> <comment_id> <reaction_id>` - Remove a reaction from a comment
|
|
20
|
+
|
|
21
|
+
#### Step Management
|
|
22
|
+
|
|
23
|
+
- `fizzy step create <card_number> --content <text> [--completed]` - Create a new step on a card
|
|
24
|
+
- `fizzy step update <card_number> <step_id> [--content <text>] [--completed]` - Update a step
|
|
25
|
+
- `fizzy step delete <card_number> <step_id>` - Delete a step from a card
|
|
26
|
+
|
|
27
|
+
#### Comment Management
|
|
28
|
+
|
|
29
|
+
- `fizzy comment delete <card_number> <comment_id>` - Delete a comment from a card
|
|
30
|
+
- `fizzy comment update <card_number> <comment_id> --body <text>` - Update a comment on a card
|
|
31
|
+
|
|
32
|
+
### Improvements
|
|
33
|
+
|
|
34
|
+
- Refactored API client logic into separate files for better organization
|
|
35
|
+
- Enhanced card update command to use `Flags().Changed()` for better flag handling
|
|
36
|
+
- Comprehensive test coverage for all new commands (168 tests total)
|
|
37
|
+
|
|
3
38
|
## 0.5.0 - 2026-01-25
|
|
4
39
|
|
|
5
40
|
### Features
|
package/cmd/card_update.go
CHANGED
|
@@ -33,23 +33,33 @@ func handleUpdateCard(cmd *cobra.Command, cardNumber string) error {
|
|
|
33
33
|
return fmt.Errorf("API client not available")
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
//
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
status, _ := cmd.Flags().GetString("status")
|
|
40
|
-
tagIDs, _ := cmd.Flags().GetStringSlice("tag-id")
|
|
41
|
-
lastActiveAt, _ := cmd.Flags().GetString("last-active-at")
|
|
36
|
+
// Build payload only with flags that were explicitly set
|
|
37
|
+
var payload api.UpdateCardPayload
|
|
38
|
+
hasChanges := false
|
|
42
39
|
|
|
43
|
-
if
|
|
44
|
-
|
|
40
|
+
if cmd.Flags().Changed("title") {
|
|
41
|
+
payload.Title, _ = cmd.Flags().GetString("title")
|
|
42
|
+
hasChanges = true
|
|
43
|
+
}
|
|
44
|
+
if cmd.Flags().Changed("description") {
|
|
45
|
+
payload.Description, _ = cmd.Flags().GetString("description")
|
|
46
|
+
hasChanges = true
|
|
47
|
+
}
|
|
48
|
+
if cmd.Flags().Changed("status") {
|
|
49
|
+
payload.Status, _ = cmd.Flags().GetString("status")
|
|
50
|
+
hasChanges = true
|
|
51
|
+
}
|
|
52
|
+
if cmd.Flags().Changed("tag-id") {
|
|
53
|
+
payload.TagIDS, _ = cmd.Flags().GetStringSlice("tag-id")
|
|
54
|
+
hasChanges = true
|
|
55
|
+
}
|
|
56
|
+
if cmd.Flags().Changed("last-active-at") {
|
|
57
|
+
payload.LastActiveAt, _ = cmd.Flags().GetString("last-active-at")
|
|
58
|
+
hasChanges = true
|
|
45
59
|
}
|
|
46
60
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Description: description,
|
|
50
|
-
Status: status,
|
|
51
|
-
TagIDS: tagIDs,
|
|
52
|
-
LastActiveAt: lastActiveAt,
|
|
61
|
+
if !hasChanges {
|
|
62
|
+
return fmt.Errorf("must provide at least one flag to update (--title, --description, --status, --tag-id, or --last-active-at)")
|
|
53
63
|
}
|
|
54
64
|
|
|
55
65
|
card, err := a.Client.PutCard(context.Background(), cardNum, payload)
|
package/cmd/comment_create.go
CHANGED
|
@@ -34,12 +34,12 @@ func handleCreateComment(cmd *cobra.Command, cardNumber string) error {
|
|
|
34
34
|
|
|
35
35
|
body, _ := cmd.Flags().GetString("body")
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
_, err = a.Client.PostCardComment(context.Background(), cardNum, body)
|
|
38
38
|
if err != nil {
|
|
39
39
|
return fmt.Errorf("creating comment: %w", err)
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
fmt.Printf("✓ Comment created successfully
|
|
42
|
+
fmt.Printf("✓ Comment created successfully\n")
|
|
43
43
|
return nil
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -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 commentDeleteCmd = &cobra.Command{
|
|
13
|
+
Use: "delete <card_number> <comment_id>",
|
|
14
|
+
Short: "Delete a comment",
|
|
15
|
+
Long: `Delete a comment from a card`,
|
|
16
|
+
Args: cobra.ExactArgs(2),
|
|
17
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
18
|
+
if err := handleDeleteComment(cmd, args[0], args[1]); err != nil {
|
|
19
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func handleDeleteComment(cmd *cobra.Command, cardNumber, commentID 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.DeleteCardComment(context.Background(), cardNum, commentID)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("deleting comment: %w", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fmt.Printf("✓ Comment deleted successfully\n")
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func init() {
|
|
45
|
+
commentCmd.AddCommand(commentDeleteCmd)
|
|
46
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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/testutil"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
func TestCommentDeleteCommandSuccess(t *testing.T) {
|
|
14
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
15
|
+
if r.URL.Path != "/cards/123/comments/comment-456" {
|
|
16
|
+
t.Errorf("expected /cards/123/comments/comment-456, got %s", r.URL.Path)
|
|
17
|
+
}
|
|
18
|
+
if r.Method != http.MethodDelete {
|
|
19
|
+
t.Errorf("expected DELETE, got %s", r.Method)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
auth := r.Header.Get("Authorization")
|
|
23
|
+
if auth != "Bearer test-token" {
|
|
24
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
w.WriteHeader(http.StatusNoContent)
|
|
28
|
+
}))
|
|
29
|
+
defer server.Close()
|
|
30
|
+
|
|
31
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
32
|
+
testApp := &app.App{Client: client}
|
|
33
|
+
|
|
34
|
+
cmd := commentDeleteCmd
|
|
35
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
36
|
+
|
|
37
|
+
if err := handleDeleteComment(cmd, "123", "comment-456"); err != nil {
|
|
38
|
+
t.Fatalf("handleDeleteComment failed: %v", err)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestCommentDeleteCommandInvalidCardNumber(t *testing.T) {
|
|
43
|
+
testApp := &app.App{}
|
|
44
|
+
|
|
45
|
+
cmd := commentDeleteCmd
|
|
46
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
47
|
+
|
|
48
|
+
err := handleDeleteComment(cmd, "not-a-number", "comment-456")
|
|
49
|
+
if err == nil {
|
|
50
|
+
t.Errorf("expected error for invalid card number")
|
|
51
|
+
}
|
|
52
|
+
if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
|
|
53
|
+
t.Errorf("expected invalid card number error, got %v", err)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func TestCommentDeleteCommandNoClient(t *testing.T) {
|
|
58
|
+
testApp := &app.App{}
|
|
59
|
+
|
|
60
|
+
cmd := commentDeleteCmd
|
|
61
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
62
|
+
|
|
63
|
+
err := handleDeleteComment(cmd, "123", "comment-456")
|
|
64
|
+
if err == nil {
|
|
65
|
+
t.Errorf("expected error when client not available")
|
|
66
|
+
}
|
|
67
|
+
if err.Error() != "API client not available" {
|
|
68
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func TestCommentDeleteCommandAPIError(t *testing.T) {
|
|
73
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
74
|
+
w.WriteHeader(http.StatusNotFound)
|
|
75
|
+
w.Write([]byte("Comment not found"))
|
|
76
|
+
}))
|
|
77
|
+
defer server.Close()
|
|
78
|
+
|
|
79
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
80
|
+
testApp := &app.App{Client: client}
|
|
81
|
+
|
|
82
|
+
cmd := commentDeleteCmd
|
|
83
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
84
|
+
|
|
85
|
+
err := handleDeleteComment(cmd, "123", "comment-456")
|
|
86
|
+
if err == nil {
|
|
87
|
+
t.Errorf("expected error for API failure")
|
|
88
|
+
}
|
|
89
|
+
if err.Error() != "deleting comment: unexpected status code 404: Comment not found" {
|
|
90
|
+
t.Errorf("expected API error, got %v", err)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
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 commentUpdateCmd = &cobra.Command{
|
|
13
|
+
Use: "update <card_number> <comment_id>",
|
|
14
|
+
Short: "Update an existing comment",
|
|
15
|
+
Long: `Update the body of an existing comment on a card`,
|
|
16
|
+
Args: cobra.ExactArgs(2),
|
|
17
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
18
|
+
if err := handleUpdateComment(cmd, args[0], args[1]); err != nil {
|
|
19
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func handleUpdateComment(cmd *cobra.Command, cardNumber, commentID 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
|
+
body, _ := cmd.Flags().GetString("body")
|
|
36
|
+
|
|
37
|
+
comment, err := a.Client.PutCardComment(context.Background(), cardNum, commentID, body)
|
|
38
|
+
if err != nil {
|
|
39
|
+
return fmt.Errorf("updating comment: %w", err)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
fmt.Printf("✓ Comment updated successfully (id: %s)\n", comment.ID)
|
|
43
|
+
return nil
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
func init() {
|
|
47
|
+
commentUpdateCmd.Flags().StringP("body", "b", "", "New comment body (required)")
|
|
48
|
+
commentUpdateCmd.MarkFlagRequired("body")
|
|
49
|
+
|
|
50
|
+
commentCmd.AddCommand(commentUpdateCmd)
|
|
51
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
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 TestCommentUpdateCommandSuccess(t *testing.T) {
|
|
17
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
18
|
+
if r.URL.Path != "/cards/123/comments/comment-456" {
|
|
19
|
+
t.Errorf("expected /cards/123/comments/comment-456, got %s", r.URL.Path)
|
|
20
|
+
}
|
|
21
|
+
if r.Method != http.MethodPut {
|
|
22
|
+
t.Errorf("expected PUT, 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
|
+
if r.Header.Get("Content-Type") != "application/json" {
|
|
31
|
+
t.Errorf("expected Content-Type: application/json, got %s", r.Header.Get("Content-Type"))
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
body, _ := io.ReadAll(r.Body)
|
|
35
|
+
var payload map[string]map[string]string
|
|
36
|
+
if err := json.Unmarshal(body, &payload); err != nil {
|
|
37
|
+
t.Fatalf("failed to unmarshal request body: %v", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
commentPayload := payload["comment"]
|
|
41
|
+
if commentPayload["body"] != "Updated comment text" {
|
|
42
|
+
t.Errorf("expected body 'Updated comment text', got %s", commentPayload["body"])
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
w.Header().Set("Content-Type", "application/json")
|
|
46
|
+
response := api.Comment{
|
|
47
|
+
ID: "comment-456",
|
|
48
|
+
CreatedAt: "2025-01-01T00:00:00Z",
|
|
49
|
+
UpdatedAt: "2025-01-02T00:00:00Z",
|
|
50
|
+
Creator: api.User{ID: "user-1", Name: "John Doe"},
|
|
51
|
+
}
|
|
52
|
+
response.Body.PlainText = "Updated comment text"
|
|
53
|
+
json.NewEncoder(w).Encode(response)
|
|
54
|
+
}))
|
|
55
|
+
defer server.Close()
|
|
56
|
+
|
|
57
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
58
|
+
testApp := &app.App{Client: client}
|
|
59
|
+
|
|
60
|
+
cmd := commentUpdateCmd
|
|
61
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
62
|
+
cmd.ParseFlags([]string{
|
|
63
|
+
"--body", "Updated comment text",
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
if err := handleUpdateComment(cmd, "123", "comment-456"); err != nil {
|
|
67
|
+
t.Fatalf("handleUpdateComment failed: %v", err)
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
func TestCommentUpdateCommandInvalidCardNumber(t *testing.T) {
|
|
72
|
+
testApp := &app.App{}
|
|
73
|
+
|
|
74
|
+
cmd := commentUpdateCmd
|
|
75
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
76
|
+
cmd.ParseFlags([]string{
|
|
77
|
+
"--body", "Updated text",
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
err := handleUpdateComment(cmd, "not-a-number", "comment-456")
|
|
81
|
+
if err == nil {
|
|
82
|
+
t.Errorf("expected error for invalid card number")
|
|
83
|
+
}
|
|
84
|
+
if err.Error() != "invalid card number: strconv.Atoi: parsing \"not-a-number\": invalid syntax" {
|
|
85
|
+
t.Errorf("expected invalid card number error, got %v", err)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func TestCommentUpdateCommandNoClient(t *testing.T) {
|
|
90
|
+
testApp := &app.App{}
|
|
91
|
+
|
|
92
|
+
cmd := commentUpdateCmd
|
|
93
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
94
|
+
cmd.ParseFlags([]string{
|
|
95
|
+
"--body", "Updated text",
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
err := handleUpdateComment(cmd, "123", "comment-456")
|
|
99
|
+
if err == nil {
|
|
100
|
+
t.Errorf("expected error when client not available")
|
|
101
|
+
}
|
|
102
|
+
if err.Error() != "API client not available" {
|
|
103
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
func TestCommentUpdateCommandAPIError(t *testing.T) {
|
|
108
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
109
|
+
w.WriteHeader(http.StatusNotFound)
|
|
110
|
+
w.Write([]byte("Comment not found"))
|
|
111
|
+
}))
|
|
112
|
+
defer server.Close()
|
|
113
|
+
|
|
114
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
115
|
+
testApp := &app.App{Client: client}
|
|
116
|
+
|
|
117
|
+
cmd := commentUpdateCmd
|
|
118
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
119
|
+
cmd.ParseFlags([]string{
|
|
120
|
+
"--body", "Updated text",
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
err := handleUpdateComment(cmd, "123", "comment-456")
|
|
124
|
+
if err == nil {
|
|
125
|
+
t.Errorf("expected error for API failure")
|
|
126
|
+
}
|
|
127
|
+
if err.Error() != "updating comment: unexpected status code 404: Comment not found" {
|
|
128
|
+
t.Errorf("expected API error, got %v", err)
|
|
129
|
+
}
|
|
130
|
+
}
|
package/cmd/reaction.go
ADDED
|
@@ -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 reactionCreateCmd = &cobra.Command{
|
|
13
|
+
Use: "create <card_number> <comment_id> <emoji>",
|
|
14
|
+
Short: "Create a reaction on a comment",
|
|
15
|
+
Long: `Create an emoji reaction on a comment`,
|
|
16
|
+
Args: cobra.ExactArgs(3),
|
|
17
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
18
|
+
if err := handleCreateReaction(cmd, args[0], args[1], args[2]); err != nil {
|
|
19
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func handleCreateReaction(cmd *cobra.Command, cardNumber, commentID, 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.PostCommentReaction(context.Background(), cardNum, commentID, emoji)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("creating reaction: %w", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fmt.Printf("✓ Reaction %s created successfully\n", emoji)
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func init() {
|
|
45
|
+
reactionCmd.AddCommand(reactionCreateCmd)
|
|
46
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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 TestReactionCreateCommandSuccess(t *testing.T) {
|
|
17
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
18
|
+
if r.URL.Path != "/cards/123/comments/comment-456/reactions" {
|
|
19
|
+
t.Errorf("expected /cards/123/comments/comment-456/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-789",
|
|
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 := reactionCreateCmd
|
|
56
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
57
|
+
|
|
58
|
+
if err := handleCreateReaction(cmd, "123", "comment-456", "👍"); err != nil {
|
|
59
|
+
t.Fatalf("handleCreateReaction failed: %v", err)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
func TestReactionCreateCommandInvalidCardNumber(t *testing.T) {
|
|
64
|
+
testApp := &app.App{}
|
|
65
|
+
|
|
66
|
+
cmd := reactionCreateCmd
|
|
67
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
68
|
+
|
|
69
|
+
err := handleCreateReaction(cmd, "not-a-number", "comment-456", "👍")
|
|
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 TestReactionCreateCommandNoClient(t *testing.T) {
|
|
79
|
+
testApp := &app.App{}
|
|
80
|
+
|
|
81
|
+
cmd := reactionCreateCmd
|
|
82
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
83
|
+
|
|
84
|
+
err := handleCreateReaction(cmd, "123", "comment-456", "👍")
|
|
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 TestReactionCreateCommandAPIError(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("Comment 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 := reactionCreateCmd
|
|
104
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
105
|
+
|
|
106
|
+
err := handleCreateReaction(cmd, "123", "comment-456", "👍")
|
|
107
|
+
if err == nil {
|
|
108
|
+
t.Errorf("expected error for API failure")
|
|
109
|
+
}
|
|
110
|
+
if err.Error() != "creating reaction: unexpected status code 404: Comment not found" {
|
|
111
|
+
t.Errorf("expected API error, got %v", err)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -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 reactionDeleteCmd = &cobra.Command{
|
|
13
|
+
Use: "delete <card_number> <comment_id> <reaction_id>",
|
|
14
|
+
Short: "Delete a reaction from a comment",
|
|
15
|
+
Long: `Delete an emoji reaction from a comment`,
|
|
16
|
+
Args: cobra.ExactArgs(3),
|
|
17
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
18
|
+
if err := handleDeleteReaction(cmd, args[0], args[1], args[2]); err != nil {
|
|
19
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func handleDeleteReaction(cmd *cobra.Command, cardNumber, commentID, 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.DeleteCommentReaction(context.Background(), cardNum, commentID, reactionID)
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("deleting reaction: %w", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
fmt.Printf("✓ Reaction deleted successfully\n")
|
|
41
|
+
return nil
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func init() {
|
|
45
|
+
reactionCmd.AddCommand(reactionDeleteCmd)
|
|
46
|
+
}
|