fizzy-cli 0.1.0 → 0.2.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.
Files changed (60) hide show
  1. package/.env +1 -0
  2. package/.github/workflows/release.yml +29 -0
  3. package/.github/workflows/tests.yml +24 -0
  4. package/AGENTS.md +33 -0
  5. package/CHANGELOG.md +69 -0
  6. package/Makefile +20 -9
  7. package/README.md +88 -1
  8. package/bin/fizzy +0 -0
  9. package/cmd/account.go +14 -0
  10. package/cmd/account_list.go +44 -0
  11. package/cmd/account_list_test.go +118 -0
  12. package/cmd/board.go +38 -12
  13. package/cmd/board_create.go +60 -0
  14. package/cmd/board_create_test.go +158 -0
  15. package/cmd/board_list.go +18 -32
  16. package/cmd/board_list_test.go +115 -0
  17. package/cmd/board_test.go +92 -0
  18. package/cmd/card.go +24 -0
  19. package/cmd/card_close.go +46 -0
  20. package/cmd/card_close_test.go +92 -0
  21. package/cmd/card_create.go +73 -0
  22. package/cmd/card_create_test.go +206 -0
  23. package/cmd/card_delete.go +46 -0
  24. package/cmd/card_delete_test.go +92 -0
  25. package/cmd/card_list.go +53 -0
  26. package/cmd/card_list_test.go +148 -0
  27. package/cmd/card_reopen.go +46 -0
  28. package/cmd/card_reopen_test.go +92 -0
  29. package/cmd/card_show.go +46 -0
  30. package/cmd/card_show_test.go +92 -0
  31. package/cmd/card_update.go +74 -0
  32. package/cmd/card_update_test.go +147 -0
  33. package/cmd/column.go +14 -0
  34. package/cmd/column_create.go +80 -0
  35. package/cmd/column_create_test.go +196 -0
  36. package/cmd/column_list.go +44 -0
  37. package/cmd/column_list_test.go +138 -0
  38. package/cmd/login.go +61 -4
  39. package/cmd/login_test.go +98 -0
  40. package/cmd/root.go +15 -4
  41. package/cmd/use.go +85 -0
  42. package/cmd/use_test.go +186 -0
  43. package/docs/API.md +1168 -0
  44. package/go.mod +23 -2
  45. package/go.sum +43 -0
  46. package/internal/api/client.go +463 -0
  47. package/internal/app/app.go +49 -0
  48. package/internal/colors/colors.go +32 -0
  49. package/internal/config/config.go +69 -0
  50. package/internal/testutil/client.go +26 -0
  51. package/internal/ui/account_list.go +14 -0
  52. package/internal/ui/account_selector.go +63 -0
  53. package/internal/ui/board_list.go +14 -0
  54. package/internal/ui/card_list.go +14 -0
  55. package/internal/ui/card_show.go +23 -0
  56. package/internal/ui/column_list.go +28 -0
  57. package/internal/ui/format.go +14 -0
  58. package/main.go +1 -1
  59. package/package.json +1 -1
  60. package/scripts/postinstall.js +5 -1
@@ -0,0 +1,158 @@
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 TestBoardCreateCommandSuccess(t *testing.T) {
17
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
+ if r.URL.Path != "/boards" {
19
+ t.Errorf("expected /boards, 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
+ 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]api.CreateBoardPayload
36
+ if err := json.Unmarshal(body, &payload); err != nil {
37
+ t.Fatalf("failed to unmarshal request body: %v", err)
38
+ }
39
+
40
+ boardPayload := payload["board"]
41
+ if boardPayload.Name != "Test Board" {
42
+ t.Errorf("expected name 'Test Board', got %s", boardPayload.Name)
43
+ }
44
+
45
+ w.WriteHeader(http.StatusCreated)
46
+ w.Header().Set("Location", "/boards/board-123")
47
+ }))
48
+ defer server.Close()
49
+
50
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
51
+ testApp := &app.App{Client: client}
52
+
53
+ cmd := boardCreateCmd
54
+ cmd.SetContext(testApp.ToContext(context.Background()))
55
+ cmd.ParseFlags([]string{"--name", "Test Board"})
56
+
57
+ boardName = "Test Board"
58
+ boardAllAccess = false
59
+ boardAutoPostponePeriod = 0
60
+ boardPublicDescription = ""
61
+
62
+ if err := handleCreateBoard(cmd); err != nil {
63
+ t.Fatalf("handleCreateBoard failed: %v", err)
64
+ }
65
+ }
66
+
67
+ func TestBoardCreateCommandWithAllFlags(t *testing.T) {
68
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69
+ if r.Method != http.MethodPost {
70
+ t.Errorf("expected POST, got %s", r.Method)
71
+ }
72
+
73
+ body, _ := io.ReadAll(r.Body)
74
+ var payload map[string]api.CreateBoardPayload
75
+ json.Unmarshal(body, &payload)
76
+
77
+ boardPayload := payload["board"]
78
+ if !boardPayload.AllAccess {
79
+ t.Error("expected AllAccess to be true")
80
+ }
81
+ if boardPayload.AutoPostponePeriod != 7 {
82
+ t.Errorf("expected AutoPostponePeriod 7, got %d", boardPayload.AutoPostponePeriod)
83
+ }
84
+ if boardPayload.PublicDescription != "Team project" {
85
+ t.Errorf("expected description 'Team project', got %s", boardPayload.PublicDescription)
86
+ }
87
+
88
+ w.WriteHeader(http.StatusCreated)
89
+ }))
90
+ defer server.Close()
91
+
92
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
93
+ testApp := &app.App{Client: client}
94
+
95
+ cmd := boardCreateCmd
96
+ cmd.SetContext(testApp.ToContext(context.Background()))
97
+ cmd.ParseFlags([]string{
98
+ "--name", "Test Board",
99
+ "--all-access",
100
+ "--auto-postpone-period", "7",
101
+ "--description", "Team project",
102
+ })
103
+
104
+ boardName = "Test Board"
105
+ boardAllAccess = true
106
+ boardAutoPostponePeriod = 7
107
+ boardPublicDescription = "Team project"
108
+
109
+ if err := handleCreateBoard(cmd); err != nil {
110
+ t.Fatalf("handleCreateBoard failed: %v", err)
111
+ }
112
+ }
113
+
114
+ func TestBoardCreateCommandAPIError(t *testing.T) {
115
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
116
+ w.WriteHeader(http.StatusInternalServerError)
117
+ w.Write([]byte("Internal Server Error"))
118
+ }))
119
+ defer server.Close()
120
+
121
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
122
+ testApp := &app.App{Client: client}
123
+
124
+ cmd := boardCreateCmd
125
+ cmd.SetContext(testApp.ToContext(context.Background()))
126
+ cmd.ParseFlags([]string{"--name", "Test Board"})
127
+
128
+ boardName = "Test Board"
129
+ boardAllAccess = false
130
+ boardAutoPostponePeriod = 0
131
+ boardPublicDescription = ""
132
+
133
+ err := handleCreateBoard(cmd)
134
+ if err == nil {
135
+ t.Errorf("expected error for API failure")
136
+ }
137
+ if err.Error() != "creating board: unexpected status code 500: Internal Server Error" {
138
+ t.Errorf("expected API error, got %v", err)
139
+ }
140
+ }
141
+
142
+ func TestBoardCreateCommandNoClient(t *testing.T) {
143
+ testApp := &app.App{}
144
+
145
+ cmd := boardCreateCmd
146
+ cmd.SetContext(testApp.ToContext(context.Background()))
147
+ cmd.ParseFlags([]string{"--name", "Test Board"})
148
+
149
+ boardName = "Test Board"
150
+
151
+ err := handleCreateBoard(cmd)
152
+ if err == nil {
153
+ t.Errorf("expected error when client not available")
154
+ }
155
+ if err.Error() != "API client not available" {
156
+ t.Errorf("expected 'client not available' error, got %v", err)
157
+ }
158
+ }
package/cmd/board_list.go CHANGED
@@ -4,6 +4,8 @@ import (
4
4
  "context"
5
5
  "fmt"
6
6
 
7
+ "github.com/rogeriopvl/fizzy/internal/app"
8
+ "github.com/rogeriopvl/fizzy/internal/ui"
7
9
  "github.com/spf13/cobra"
8
10
  )
9
11
 
@@ -11,46 +13,30 @@ var boardListCmd = &cobra.Command{
11
13
  Use: "list",
12
14
  Short: "List all boards",
13
15
  Long: `Retrieve and display all boards from Fizzy`,
14
- RunE: listBoards,
16
+ Run: func(cmd *cobra.Command, args []string) {
17
+ if err := handleListBoards(cmd); err != nil {
18
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
19
+ }
20
+ },
15
21
  }
16
22
 
17
- func listBoards(cmd *cobra.Command, args []string) error {
18
- ctx := context.Background()
19
-
20
- // TODO: Implement API call to fetch boards
21
- boards, err := fetchBoards(ctx)
22
- if err != nil {
23
- return fmt.Errorf("failed to list boards: %w", err)
23
+ func handleListBoards(cmd *cobra.Command) error {
24
+ a := app.FromContext(cmd.Context())
25
+ if a == nil || a.Client == nil {
26
+ return fmt.Errorf("API client not available")
24
27
  }
25
28
 
26
- // TODO: Format and display boards
27
- if err := displayBoards(boards); err != nil {
28
- return fmt.Errorf("failed to display boards: %w", err)
29
+ boards, err := a.Client.GetBoards(context.Background())
30
+ if err != nil {
31
+ return fmt.Errorf("fetching boards: %w", err)
29
32
  }
30
33
 
31
- return nil
32
- }
33
-
34
- // fetchBoards retrieves all boards from the Fizzy API.
35
- func fetchBoards(ctx context.Context) ([]Board, error) {
36
- // TODO: Implement API call
37
- return nil, nil
38
- }
39
-
40
- // displayBoards formats and outputs the boards.
41
- func displayBoards(boards []Board) error {
42
- // TODO: Implement output formatting
43
- for _, board := range boards {
44
- fmt.Printf("Board: %+v\n", board)
34
+ if len(boards) == 0 {
35
+ fmt.Println("No boards found")
36
+ return nil
45
37
  }
46
- return nil
47
- }
48
38
 
49
- // Board represents a Fizzy board.
50
- type Board struct {
51
- ID string
52
- Name string
53
- // TODO: Add additional fields as needed
39
+ return ui.DisplayBoards(boards)
54
40
  }
55
41
 
56
42
  func init() {
@@ -0,0 +1,115 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "testing"
9
+
10
+ "github.com/rogeriopvl/fizzy/internal/api"
11
+ "github.com/rogeriopvl/fizzy/internal/app"
12
+ "github.com/rogeriopvl/fizzy/internal/testutil"
13
+ )
14
+
15
+ func TestBoardListCommand(t *testing.T) {
16
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17
+ if r.URL.Path != "/boards" {
18
+ t.Errorf("expected /boards, got %s", r.URL.Path)
19
+ }
20
+ if r.Method != http.MethodGet {
21
+ t.Errorf("expected GET, got %s", r.Method)
22
+ }
23
+
24
+ auth := r.Header.Get("Authorization")
25
+ if auth == "" {
26
+ t.Error("missing Authorization header")
27
+ }
28
+ if auth != "Bearer test-token" {
29
+ t.Errorf("expected Bearer test-token, got %s", auth)
30
+ }
31
+
32
+ w.Header().Set("Content-Type", "application/json")
33
+ response := []api.Board{
34
+ {
35
+ ID: "board-123",
36
+ Name: "Project Alpha",
37
+ AllAccess: true,
38
+ CreatedAt: "2025-01-01T00:00:00Z",
39
+ },
40
+ {
41
+ ID: "board-456",
42
+ Name: "Project Beta",
43
+ AllAccess: false,
44
+ CreatedAt: "2025-01-02T00:00:00Z",
45
+ },
46
+ }
47
+ json.NewEncoder(w).Encode(response)
48
+ }))
49
+ defer server.Close()
50
+
51
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
52
+ testApp := &app.App{Client: client}
53
+
54
+ cmd := boardListCmd
55
+ cmd.SetContext(testApp.ToContext(context.Background()))
56
+
57
+ if err := handleListBoards(cmd); err != nil {
58
+ t.Fatalf("handleListBoards failed: %v", err)
59
+ }
60
+ }
61
+
62
+ func TestBoardListCommandNoBoards(t *testing.T) {
63
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
64
+ w.Header().Set("Content-Type", "application/json")
65
+ json.NewEncoder(w).Encode([]api.Board{})
66
+ }))
67
+ defer server.Close()
68
+
69
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
70
+ testApp := &app.App{Client: client}
71
+
72
+ cmd := boardListCmd
73
+ cmd.SetContext(testApp.ToContext(context.Background()))
74
+
75
+ if err := handleListBoards(cmd); err != nil {
76
+ t.Fatalf("handleListBoards failed: %v", err)
77
+ }
78
+ }
79
+
80
+ func TestBoardListCommandAPIError(t *testing.T) {
81
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
82
+ w.WriteHeader(http.StatusInternalServerError)
83
+ w.Write([]byte("Internal Server Error"))
84
+ }))
85
+ defer server.Close()
86
+
87
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
88
+ testApp := &app.App{Client: client}
89
+
90
+ cmd := boardListCmd
91
+ cmd.SetContext(testApp.ToContext(context.Background()))
92
+
93
+ err := handleListBoards(cmd)
94
+ if err == nil {
95
+ t.Errorf("expected error for API failure")
96
+ }
97
+ if err.Error() != "fetching boards: unexpected status code 500: Internal Server Error" {
98
+ t.Errorf("expected API error, got %v", err)
99
+ }
100
+ }
101
+
102
+ func TestBoardListCommandNoClient(t *testing.T) {
103
+ testApp := &app.App{}
104
+
105
+ cmd := boardListCmd
106
+ cmd.SetContext(testApp.ToContext(context.Background()))
107
+
108
+ err := handleListBoards(cmd)
109
+ if err == nil {
110
+ t.Errorf("expected error when client not available")
111
+ }
112
+ if err.Error() != "API client not available" {
113
+ t.Errorf("expected 'client not available' error, got %v", err)
114
+ }
115
+ }
@@ -0,0 +1,92 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "encoding/json"
6
+ "net/http"
7
+ "net/http/httptest"
8
+ "testing"
9
+
10
+ "github.com/rogeriopvl/fizzy/internal/api"
11
+ "github.com/rogeriopvl/fizzy/internal/app"
12
+ "github.com/rogeriopvl/fizzy/internal/config"
13
+ "github.com/rogeriopvl/fizzy/internal/testutil"
14
+ )
15
+
16
+ func TestBoardCommand(t *testing.T) {
17
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18
+ if r.URL.Path != "/boards/board-123" {
19
+ t.Errorf("expected /boards/board-123, got %s", r.URL.Path)
20
+ }
21
+
22
+ w.Header().Set("Content-Type", "application/json")
23
+ response := api.Board{
24
+ ID: "board-123",
25
+ Name: "Test Board",
26
+ AllAccess: true,
27
+ CreatedAt: "2025-12-05T19:36:35.534Z",
28
+ }
29
+ json.NewEncoder(w).Encode(response)
30
+ }))
31
+ defer server.Close()
32
+
33
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
34
+ testApp := &app.App{
35
+ Client: client,
36
+ Config: &config.Config{
37
+ SelectedBoard: "board-123",
38
+ },
39
+ }
40
+
41
+ cmd := boardCmd
42
+ cmd.SetContext(testApp.ToContext(context.Background()))
43
+
44
+ if err := handleShowBoard(cmd); err != nil {
45
+ t.Fatalf("handleShowBoard failed: %v", err)
46
+ }
47
+ }
48
+
49
+ func TestBoardCommandNoBoard(t *testing.T) {
50
+ client := testutil.NewTestClient("http://localhost", "", "", "test-token")
51
+ testApp := &app.App{
52
+ Client: client,
53
+ Config: &config.Config{
54
+ SelectedBoard: "",
55
+ },
56
+ }
57
+
58
+ cmd := boardCmd
59
+ cmd.SetContext(testApp.ToContext(context.Background()))
60
+
61
+ err := handleShowBoard(cmd)
62
+ if err == nil {
63
+ t.Errorf("expected error when no board selected")
64
+ }
65
+ if err.Error() != "no board selected" {
66
+ t.Errorf("expected 'no board selected' error, got %v", err)
67
+ }
68
+ }
69
+
70
+ func TestBoardCommandAPIError(t *testing.T) {
71
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
72
+ w.WriteHeader(http.StatusInternalServerError)
73
+ w.Write([]byte("Internal Server Error"))
74
+ }))
75
+ defer server.Close()
76
+
77
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
78
+ testApp := &app.App{
79
+ Client: client,
80
+ Config: &config.Config{
81
+ SelectedBoard: "board-123",
82
+ },
83
+ }
84
+
85
+ cmd := boardCmd
86
+ cmd.SetContext(testApp.ToContext(context.Background()))
87
+
88
+ err := handleShowBoard(cmd)
89
+ if err == nil {
90
+ t.Errorf("expected error for API failure")
91
+ }
92
+ }
package/cmd/card.go ADDED
@@ -0,0 +1,24 @@
1
+ // Package cmd
2
+ package cmd
3
+
4
+ import "github.com/spf13/cobra"
5
+
6
+ var cardCmd = &cobra.Command{
7
+ Use: "card",
8
+ Short: "Manage cards",
9
+ Long: `Manage cards in Fizzy`,
10
+ }
11
+
12
+ func init() {
13
+ rootCmd.AddCommand(cardCmd)
14
+
15
+ // Here you will define your flags and configuration settings.
16
+
17
+ // Cobra supports Persistent Flags which will work for this command
18
+ // and all subcommands, e.g.:
19
+ // cardsCmd.PersistentFlags().String("foo", "", "A help for foo")
20
+
21
+ // Cobra supports local flags which will only run when this command
22
+ // is called directly, e.g.:
23
+ // cardsCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
24
+ }
@@ -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 cardCloseCmd = &cobra.Command{
13
+ Use: "close <card_number>",
14
+ Short: "Close a card",
15
+ Long: `Close an existing card`,
16
+ Args: cobra.ExactArgs(1),
17
+ Run: func(cmd *cobra.Command, args []string) {
18
+ if err := handleCloseCard(cmd, args[0]); err != nil {
19
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
20
+ }
21
+ },
22
+ }
23
+
24
+ func handleCloseCard(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.PostCardsClosure(context.Background(), cardNum)
36
+ if err != nil {
37
+ return fmt.Errorf("closing card: %w", err)
38
+ }
39
+
40
+ fmt.Printf("✓ Card #%d closed successfully\n", cardNum)
41
+ return nil
42
+ }
43
+
44
+ func init() {
45
+ cardCmd.AddCommand(cardCloseCmd)
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 TestCardCloseCommandSuccess(t *testing.T) {
14
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
15
+ if r.URL.Path != "/cards/123/closure" {
16
+ t.Errorf("expected /cards/123/closure, got %s", r.URL.Path)
17
+ }
18
+ if r.Method != http.MethodPost {
19
+ t.Errorf("expected POST, 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 := cardCloseCmd
35
+ cmd.SetContext(testApp.ToContext(context.Background()))
36
+
37
+ if err := handleCloseCard(cmd, "123"); err != nil {
38
+ t.Fatalf("handleCloseCard failed: %v", err)
39
+ }
40
+ }
41
+
42
+ func TestCardCloseCommandInvalidCardNumber(t *testing.T) {
43
+ testApp := &app.App{}
44
+
45
+ cmd := cardCloseCmd
46
+ cmd.SetContext(testApp.ToContext(context.Background()))
47
+
48
+ err := handleCloseCard(cmd, "not-a-number")
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 TestCardCloseCommandNoClient(t *testing.T) {
58
+ testApp := &app.App{}
59
+
60
+ cmd := cardCloseCmd
61
+ cmd.SetContext(testApp.ToContext(context.Background()))
62
+
63
+ err := handleCloseCard(cmd, "123")
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 TestCardCloseCommandAPIError(t *testing.T) {
73
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
74
+ w.WriteHeader(http.StatusInternalServerError)
75
+ w.Write([]byte("Internal Server Error"))
76
+ }))
77
+ defer server.Close()
78
+
79
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
80
+ testApp := &app.App{Client: client}
81
+
82
+ cmd := cardCloseCmd
83
+ cmd.SetContext(testApp.ToContext(context.Background()))
84
+
85
+ err := handleCloseCard(cmd, "123")
86
+ if err == nil {
87
+ t.Errorf("expected error for API failure")
88
+ }
89
+ if err.Error() != "closing card: unexpected status code 500: Internal Server Error" {
90
+ t.Errorf("expected API error, got %v", err)
91
+ }
92
+ }
@@ -0,0 +1,73 @@
1
+ package cmd
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+
7
+ "github.com/rogeriopvl/fizzy/internal/api"
8
+ "github.com/rogeriopvl/fizzy/internal/app"
9
+ "github.com/spf13/cobra"
10
+ )
11
+
12
+ var (
13
+ cardTitle string
14
+ cardDescription string
15
+ cardStatus string
16
+ cardImageURL string
17
+ cardTagIDs []string
18
+ cardCreatedAt string
19
+ cardLastActiveAt string
20
+ )
21
+
22
+ var cardCreateCmd = &cobra.Command{
23
+ Use: "create",
24
+ Short: "Create a new card",
25
+ Long: `Create a new card in the selected board`,
26
+ Run: func(cmd *cobra.Command, args []string) {
27
+ if err := handleCreateCard(cmd); err != nil {
28
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
29
+ }
30
+ },
31
+ }
32
+
33
+ func handleCreateCard(cmd *cobra.Command) error {
34
+ a := app.FromContext(cmd.Context())
35
+ if a == nil || a.Client == nil {
36
+ return fmt.Errorf("API client not available")
37
+ }
38
+
39
+ if a.Config.SelectedBoard == "" {
40
+ return fmt.Errorf("no board selected")
41
+ }
42
+
43
+ payload := api.CreateCardPayload{
44
+ Title: cardTitle,
45
+ Description: cardDescription,
46
+ Status: cardStatus,
47
+ ImageURL: cardImageURL,
48
+ TagIDS: cardTagIDs,
49
+ CreatedAt: cardCreatedAt,
50
+ LastActiveAt: cardLastActiveAt,
51
+ }
52
+
53
+ _, err := a.Client.PostCards(context.Background(), payload)
54
+ if err != nil {
55
+ return fmt.Errorf("creating card: %w", err)
56
+ }
57
+
58
+ fmt.Printf("✓ Card '%s' created successfully\n", cardTitle)
59
+ return nil
60
+ }
61
+
62
+ func init() {
63
+ cardCreateCmd.Flags().StringVarP(&cardTitle, "title", "t", "", "Card title (required)")
64
+ cardCreateCmd.MarkFlagRequired("title")
65
+ cardCreateCmd.Flags().StringVarP(&cardDescription, "description", "d", "", "Card description")
66
+ cardCreateCmd.Flags().StringVar(&cardStatus, "status", "", "Card status")
67
+ cardCreateCmd.Flags().StringVar(&cardImageURL, "image-url", "", "Card image URL")
68
+ cardCreateCmd.Flags().StringSliceVar(&cardTagIDs, "tag-id", []string{}, "Tag ID (can be used multiple times)")
69
+ cardCreateCmd.Flags().StringVar(&cardCreatedAt, "created-at", "", "Creation timestamp")
70
+ cardCreateCmd.Flags().StringVar(&cardLastActiveAt, "last-active-at", "", "Last active timestamp")
71
+
72
+ cardCmd.AddCommand(cardCreateCmd)
73
+ }