fizzy-cli 0.1.0 → 0.2.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/.github/workflows/release.yml +29 -0
- package/.github/workflows/tests.yml +24 -0
- package/AGENTS.md +33 -0
- package/CHANGELOG.md +63 -0
- package/Makefile +20 -9
- package/README.md +88 -1
- package/bin/fizzy +0 -0
- package/bin/fizzy-darwin-amd64 +0 -0
- package/bin/fizzy-darwin-arm64 +0 -0
- package/bin/fizzy-linux-amd64 +0 -0
- package/bin/fizzy-linux-arm64 +0 -0
- package/bin/fizzy-windows-amd64.exe +0 -0
- package/cmd/account.go +14 -0
- package/cmd/account_list.go +44 -0
- package/cmd/account_list_test.go +118 -0
- package/cmd/board.go +38 -12
- package/cmd/board_create.go +60 -0
- package/cmd/board_create_test.go +158 -0
- package/cmd/board_list.go +18 -32
- package/cmd/board_list_test.go +115 -0
- package/cmd/board_test.go +92 -0
- package/cmd/card.go +24 -0
- package/cmd/card_close.go +46 -0
- package/cmd/card_close_test.go +92 -0
- package/cmd/card_create.go +73 -0
- package/cmd/card_create_test.go +206 -0
- package/cmd/card_delete.go +46 -0
- package/cmd/card_delete_test.go +92 -0
- package/cmd/card_list.go +53 -0
- package/cmd/card_list_test.go +148 -0
- package/cmd/card_reopen.go +46 -0
- package/cmd/card_reopen_test.go +92 -0
- package/cmd/card_show.go +46 -0
- package/cmd/card_show_test.go +92 -0
- package/cmd/card_update.go +74 -0
- package/cmd/card_update_test.go +147 -0
- package/cmd/column.go +14 -0
- package/cmd/column_create.go +80 -0
- package/cmd/column_create_test.go +196 -0
- package/cmd/column_list.go +44 -0
- package/cmd/column_list_test.go +138 -0
- package/cmd/login.go +61 -4
- package/cmd/login_test.go +98 -0
- package/cmd/root.go +15 -4
- package/cmd/use.go +85 -0
- package/cmd/use_test.go +186 -0
- package/docs/API.md +1168 -0
- package/go.mod +23 -2
- package/go.sum +43 -0
- package/internal/api/client.go +463 -0
- package/internal/app/app.go +49 -0
- package/internal/colors/colors.go +32 -0
- package/internal/config/config.go +69 -0
- package/internal/testutil/client.go +26 -0
- package/internal/ui/account_list.go +14 -0
- package/internal/ui/account_selector.go +63 -0
- package/internal/ui/board_list.go +14 -0
- package/internal/ui/card_list.go +14 -0
- package/internal/ui/card_show.go +23 -0
- package/internal/ui/column_list.go +28 -0
- package/internal/ui/format.go +14 -0
- package/main.go +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,138 @@
|
|
|
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 TestColumnListCommand(t *testing.T) {
|
|
16
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
17
|
+
if r.URL.Path != "/boards/board-123/columns" {
|
|
18
|
+
t.Errorf("expected /boards/board-123/columns, 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 != "Bearer test-token" {
|
|
26
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if r.Header.Get("Accept") != "application/json" {
|
|
30
|
+
t.Errorf("expected Accept: application/json, got %s", r.Header.Get("Accept"))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
w.Header().Set("Content-Type", "application/json")
|
|
34
|
+
response := []api.Column{
|
|
35
|
+
{
|
|
36
|
+
ID: "col-123",
|
|
37
|
+
Name: "Todo",
|
|
38
|
+
Color: api.ColorObject{
|
|
39
|
+
Name: "Blue",
|
|
40
|
+
Value: "var(--color-card-default)",
|
|
41
|
+
},
|
|
42
|
+
CreatedAt: "2025-01-01T00:00:00Z",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
ID: "col-456",
|
|
46
|
+
Name: "In Progress",
|
|
47
|
+
Color: api.ColorObject{
|
|
48
|
+
Name: "Lime",
|
|
49
|
+
Value: "var(--color-card-4)",
|
|
50
|
+
},
|
|
51
|
+
CreatedAt: "2025-01-02T00:00:00Z",
|
|
52
|
+
},
|
|
53
|
+
}
|
|
54
|
+
json.NewEncoder(w).Encode(response)
|
|
55
|
+
}))
|
|
56
|
+
defer server.Close()
|
|
57
|
+
|
|
58
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
59
|
+
testApp := &app.App{Client: client}
|
|
60
|
+
|
|
61
|
+
cmd := columnListCmd
|
|
62
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
63
|
+
|
|
64
|
+
if err := handleListColumns(cmd); err != nil {
|
|
65
|
+
t.Fatalf("handleListColumns failed: %v", err)
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func TestColumnListCommandNoColumns(t *testing.T) {
|
|
70
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
71
|
+
w.Header().Set("Content-Type", "application/json")
|
|
72
|
+
json.NewEncoder(w).Encode([]api.Column{})
|
|
73
|
+
}))
|
|
74
|
+
defer server.Close()
|
|
75
|
+
|
|
76
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
77
|
+
testApp := &app.App{Client: client}
|
|
78
|
+
|
|
79
|
+
cmd := columnListCmd
|
|
80
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
81
|
+
|
|
82
|
+
if err := handleListColumns(cmd); err != nil {
|
|
83
|
+
t.Fatalf("handleListColumns failed: %v", err)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
func TestColumnListCommandAPIError(t *testing.T) {
|
|
88
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
89
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
90
|
+
w.Write([]byte("Internal Server Error"))
|
|
91
|
+
}))
|
|
92
|
+
defer server.Close()
|
|
93
|
+
|
|
94
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
95
|
+
testApp := &app.App{Client: client}
|
|
96
|
+
|
|
97
|
+
cmd := columnListCmd
|
|
98
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
99
|
+
|
|
100
|
+
err := handleListColumns(cmd)
|
|
101
|
+
if err == nil {
|
|
102
|
+
t.Errorf("expected error for API failure")
|
|
103
|
+
}
|
|
104
|
+
if err.Error() != "fetching columns: unexpected status code 500: Internal Server Error" {
|
|
105
|
+
t.Errorf("expected API error, got %v", err)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
func TestColumnListCommandNoBoard(t *testing.T) {
|
|
110
|
+
client := testutil.NewTestClient("http://localhost", "", "", "test-token")
|
|
111
|
+
testApp := &app.App{Client: client}
|
|
112
|
+
|
|
113
|
+
cmd := columnListCmd
|
|
114
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
115
|
+
|
|
116
|
+
err := handleListColumns(cmd)
|
|
117
|
+
if err == nil {
|
|
118
|
+
t.Errorf("expected error when board not selected")
|
|
119
|
+
}
|
|
120
|
+
if err.Error() != "fetching columns: please select a board first with 'fizzy use --board <board_name>'" {
|
|
121
|
+
t.Errorf("expected 'board not selected' error, got %v", err)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func TestColumnListCommandNoClient(t *testing.T) {
|
|
126
|
+
testApp := &app.App{}
|
|
127
|
+
|
|
128
|
+
cmd := columnListCmd
|
|
129
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
130
|
+
|
|
131
|
+
err := handleListColumns(cmd)
|
|
132
|
+
if err == nil {
|
|
133
|
+
t.Errorf("expected error when client not available")
|
|
134
|
+
}
|
|
135
|
+
if err.Error() != "API client not available" {
|
|
136
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
137
|
+
}
|
|
138
|
+
}
|
package/cmd/login.go
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
package cmd
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"context"
|
|
4
5
|
"fmt"
|
|
6
|
+
"os"
|
|
5
7
|
|
|
8
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
9
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
10
|
+
"github.com/rogeriopvl/fizzy/internal/ui"
|
|
6
11
|
"github.com/spf13/cobra"
|
|
7
12
|
)
|
|
8
13
|
|
|
@@ -11,13 +16,65 @@ var loginCmd = &cobra.Command{
|
|
|
11
16
|
Short: "Prints instructions on how to authenticate with the Fizzy API",
|
|
12
17
|
Long: `Prints intructions on how to authenticate with the Fizzy API`,
|
|
13
18
|
Run: func(cmd *cobra.Command, args []string) {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
fmt.Println("\nThen export it as an environment variable in your shell, with the name FIZZY_ACCESS_TOKEN")
|
|
19
|
+
if err := handleLogin(cmd); err != nil {
|
|
20
|
+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
|
21
|
+
}
|
|
18
22
|
},
|
|
19
23
|
}
|
|
20
24
|
|
|
25
|
+
func handleLogin(cmd *cobra.Command) error {
|
|
26
|
+
token, isSet := os.LookupEnv("FIZZY_ACCESS_TOKEN")
|
|
27
|
+
if !isSet || token == "" {
|
|
28
|
+
return printAuthInstructions()
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
fmt.Printf("✓ Authenticated with access token: %s\n", token[:6]+"...")
|
|
32
|
+
|
|
33
|
+
a := app.FromContext(cmd.Context())
|
|
34
|
+
if a == nil || a.Client == nil {
|
|
35
|
+
return fmt.Errorf("API client not available")
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
identity, err := a.Client.GetMyIdentity(context.Background())
|
|
39
|
+
if err != nil {
|
|
40
|
+
return fmt.Errorf("fetching identity: %w", err)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
selected, err := chooseAccount(identity.Accounts)
|
|
44
|
+
if err != nil {
|
|
45
|
+
return err
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fmt.Printf("\nSelected account: %s (%s)\n", selected.Name, selected.Slug)
|
|
49
|
+
|
|
50
|
+
// Save the selected account to config
|
|
51
|
+
a.Config.SelectedAccount = selected.Slug
|
|
52
|
+
if err := a.Config.Save(); err != nil {
|
|
53
|
+
return fmt.Errorf("saving config: %w", err)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
func chooseAccount(accounts []api.Account) (api.Account, error) {
|
|
60
|
+
if len(accounts) == 1 {
|
|
61
|
+
selected := accounts[0]
|
|
62
|
+
return selected, nil
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fmt.Println("\nAvailable accounts:")
|
|
66
|
+
return ui.SelectAccount(accounts)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
func printAuthInstructions() error {
|
|
70
|
+
fmt.Println("To authenticate with Fizzy's API you need an access token.")
|
|
71
|
+
fmt.Printf("\nGo to https://app.fizzy.do/<account_slug>/my/access_tokens and follow the instructions...\n")
|
|
72
|
+
fmt.Println("(Replace <account_slug> with your account slug)")
|
|
73
|
+
fmt.Printf("\nThen export it as an environment variable in your shell, with the name FIZZY_ACCESS_TOKEN\n")
|
|
74
|
+
fmt.Println("And re-run this command.")
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
21
78
|
func init() {
|
|
22
79
|
rootCmd.AddCommand(loginCmd)
|
|
23
80
|
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"net/http"
|
|
7
|
+
"net/http/httptest"
|
|
8
|
+
"os"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/api"
|
|
12
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
13
|
+
"github.com/rogeriopvl/fizzy/internal/config"
|
|
14
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
func TestLoginCommand(t *testing.T) {
|
|
18
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
19
|
+
if r.URL.Path != "/my/identity" {
|
|
20
|
+
t.Errorf("expected /my/identity, got %s", r.URL.Path)
|
|
21
|
+
}
|
|
22
|
+
if r.Method != http.MethodGet {
|
|
23
|
+
t.Errorf("expected GET, got %s", r.Method)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
auth := r.Header.Get("Authorization")
|
|
27
|
+
if auth == "" {
|
|
28
|
+
t.Error("missing Authorization header")
|
|
29
|
+
w.WriteHeader(http.StatusUnauthorized)
|
|
30
|
+
return
|
|
31
|
+
}
|
|
32
|
+
if auth != "Bearer test-token" {
|
|
33
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if r.Header.Get("Accept") != "application/json" {
|
|
37
|
+
t.Errorf("expected Accept: application/json, got %s", r.Header.Get("Accept"))
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
w.Header().Set("Content-Type", "application/json")
|
|
41
|
+
response := api.GetMyIdentityResponse{
|
|
42
|
+
Accounts: []api.Account{
|
|
43
|
+
{
|
|
44
|
+
ID: "acc-123",
|
|
45
|
+
Name: "Test Account",
|
|
46
|
+
Slug: "test-account",
|
|
47
|
+
User: api.User{
|
|
48
|
+
ID: "user-123",
|
|
49
|
+
Email: "test@example.com",
|
|
50
|
+
Name: "Test User",
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
json.NewEncoder(w).Encode(response)
|
|
56
|
+
}))
|
|
57
|
+
defer server.Close()
|
|
58
|
+
|
|
59
|
+
tmpDir := t.TempDir()
|
|
60
|
+
configPath := tmpDir + "/.config/fizzy-cli/config.json"
|
|
61
|
+
|
|
62
|
+
t.Setenv("FIZZY_ACCESS_TOKEN", "test-token")
|
|
63
|
+
t.Setenv("HOME", tmpDir)
|
|
64
|
+
|
|
65
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
66
|
+
|
|
67
|
+
cfg := &config.Config{}
|
|
68
|
+
testApp := &app.App{
|
|
69
|
+
Client: client,
|
|
70
|
+
Config: cfg,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
cmd := loginCmd
|
|
74
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
75
|
+
|
|
76
|
+
if err := handleLogin(cmd); err != nil {
|
|
77
|
+
t.Fatalf("handleLogin failed: %v", err)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if cfg.SelectedAccount != "test-account" {
|
|
81
|
+
t.Errorf("expected SelectedAccount=test-account, got %s", cfg.SelectedAccount)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if _, err := os.Stat(configPath); err != nil {
|
|
85
|
+
t.Errorf("config file not created at %s: %v", configPath, err)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
func TestLoginCommandWithoutToken(t *testing.T) {
|
|
90
|
+
t.Setenv("FIZZY_ACCESS_TOKEN", "")
|
|
91
|
+
|
|
92
|
+
cmd := loginCmd
|
|
93
|
+
err := handleLogin(cmd)
|
|
94
|
+
|
|
95
|
+
if err != nil {
|
|
96
|
+
t.Errorf("expected no error when token is missing, got %v", err)
|
|
97
|
+
}
|
|
98
|
+
}
|
package/cmd/root.go
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
package cmd
|
|
2
2
|
|
|
3
3
|
import (
|
|
4
|
+
"fmt"
|
|
4
5
|
"os"
|
|
5
6
|
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
6
8
|
"github.com/spf13/cobra"
|
|
7
9
|
)
|
|
8
10
|
|
|
11
|
+
var Version = "dev"
|
|
12
|
+
|
|
9
13
|
var rootCmd = &cobra.Command{
|
|
10
|
-
Use:
|
|
11
|
-
Short:
|
|
12
|
-
Long:
|
|
13
|
-
|
|
14
|
+
Use: "fizzy-cli",
|
|
15
|
+
Short: "Fizzy CLI",
|
|
16
|
+
Long: `Fizzy CLI`,
|
|
17
|
+
Version: Version,
|
|
18
|
+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
|
19
|
+
a, _ := app.New()
|
|
20
|
+
if a != nil {
|
|
21
|
+
cmd.SetContext(a.ToContext(cmd.Context()))
|
|
22
|
+
}
|
|
23
|
+
},
|
|
14
24
|
}
|
|
15
25
|
|
|
16
26
|
func Execute() {
|
|
@@ -24,4 +34,5 @@ func init() {
|
|
|
24
34
|
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.fizzy-cli.yaml)")
|
|
25
35
|
|
|
26
36
|
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
|
37
|
+
rootCmd.SetVersionTemplate(fmt.Sprintf("fizzy-cli v%s\n", Version))
|
|
27
38
|
}
|
package/cmd/use.go
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
8
|
+
"github.com/rogeriopvl/fizzy/internal/config"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var useCmd = &cobra.Command{
|
|
13
|
+
Use: "use",
|
|
14
|
+
Short: "Set the active board or account",
|
|
15
|
+
Long: `Set the active board or account to use for subsequent commands`,
|
|
16
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
17
|
+
if err := handleUse(cmd); err != nil {
|
|
18
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
func handleUse(cmd *cobra.Command) error {
|
|
24
|
+
board, _ := cmd.Flags().GetString("board")
|
|
25
|
+
account, _ := cmd.Flags().GetString("account")
|
|
26
|
+
|
|
27
|
+
if board == "" && account == "" {
|
|
28
|
+
return fmt.Errorf("must specify either --board or --account")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if board != "" && account != "" {
|
|
32
|
+
return fmt.Errorf("cannot specify both --board and --account")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cfg, err := config.Load()
|
|
36
|
+
if err != nil {
|
|
37
|
+
return fmt.Errorf("loading config: %w", err)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if board != "" {
|
|
41
|
+
a := app.FromContext(cmd.Context())
|
|
42
|
+
if a == nil || a.Client == nil {
|
|
43
|
+
return fmt.Errorf("API client not available")
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
boards, err := a.Client.GetBoards(context.Background())
|
|
47
|
+
if err != nil {
|
|
48
|
+
return fmt.Errorf("fetching boards: %w", err)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
var boardID string
|
|
52
|
+
for _, b := range boards {
|
|
53
|
+
if b.Name == board {
|
|
54
|
+
boardID = b.ID
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if boardID == "" {
|
|
60
|
+
return fmt.Errorf("board '%s' not found", board)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
cfg.SelectedBoard = boardID
|
|
64
|
+
if err := cfg.Save(); err != nil {
|
|
65
|
+
return fmt.Errorf("saving config: %w", err)
|
|
66
|
+
}
|
|
67
|
+
fmt.Printf("Selected board: %s\n", board)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if account != "" {
|
|
71
|
+
cfg.SelectedAccount = account
|
|
72
|
+
if err := cfg.Save(); err != nil {
|
|
73
|
+
return fmt.Errorf("saving config: %w", err)
|
|
74
|
+
}
|
|
75
|
+
fmt.Printf("Selected account: %s\n", account)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return nil
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func init() {
|
|
82
|
+
rootCmd.AddCommand(useCmd)
|
|
83
|
+
useCmd.Flags().String("board", "", "Board name to use")
|
|
84
|
+
useCmd.Flags().String("account", "", "Account slug to use")
|
|
85
|
+
}
|
package/cmd/use_test.go
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
"fmt"
|
|
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/config"
|
|
14
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
15
|
+
"github.com/spf13/cobra"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
func newUseCmd() *cobra.Command {
|
|
19
|
+
cmd := &cobra.Command{
|
|
20
|
+
Use: "use",
|
|
21
|
+
Short: "Set the active board or account",
|
|
22
|
+
Long: `Set the active board or account to use for subsequent commands`,
|
|
23
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
24
|
+
if err := handleUse(cmd); err != nil {
|
|
25
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
}
|
|
29
|
+
cmd.Flags().String("board", "", "Board name to use")
|
|
30
|
+
cmd.Flags().String("account", "", "Account slug to use")
|
|
31
|
+
return cmd
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func TestUseCommandSetBoard(t *testing.T) {
|
|
35
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
36
|
+
if r.URL.Path != "/boards" {
|
|
37
|
+
t.Errorf("expected /boards, got %s", r.URL.Path)
|
|
38
|
+
}
|
|
39
|
+
if r.Method != http.MethodGet {
|
|
40
|
+
t.Errorf("expected GET, got %s", r.Method)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
auth := r.Header.Get("Authorization")
|
|
44
|
+
if auth == "" {
|
|
45
|
+
t.Error("missing Authorization header")
|
|
46
|
+
}
|
|
47
|
+
if auth != "Bearer test-token" {
|
|
48
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
w.Header().Set("Content-Type", "application/json")
|
|
52
|
+
response := []api.Board{
|
|
53
|
+
{
|
|
54
|
+
ID: "board-123",
|
|
55
|
+
Name: "My Project",
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
ID: "board-456",
|
|
59
|
+
Name: "Other Board",
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
json.NewEncoder(w).Encode(response)
|
|
63
|
+
}))
|
|
64
|
+
defer server.Close()
|
|
65
|
+
|
|
66
|
+
tmpDir := t.TempDir()
|
|
67
|
+
t.Setenv("HOME", tmpDir)
|
|
68
|
+
|
|
69
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
70
|
+
cfg := &config.Config{}
|
|
71
|
+
testApp := &app.App{
|
|
72
|
+
Client: client,
|
|
73
|
+
Config: cfg,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
cmd := newUseCmd()
|
|
77
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
78
|
+
cmd.ParseFlags([]string{"--board", "My Project"})
|
|
79
|
+
|
|
80
|
+
if err := handleUse(cmd); err != nil {
|
|
81
|
+
t.Fatalf("handleUse failed: %v", err)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
savedCfg, err := config.Load()
|
|
85
|
+
if err != nil {
|
|
86
|
+
t.Fatalf("failed to load config: %v", err)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if savedCfg.SelectedBoard != "board-123" {
|
|
90
|
+
t.Errorf("expected SelectedBoard=board-123, got %s", savedCfg.SelectedBoard)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func TestUseCommandSetAccount(t *testing.T) {
|
|
95
|
+
tmpDir := t.TempDir()
|
|
96
|
+
t.Setenv("HOME", tmpDir)
|
|
97
|
+
|
|
98
|
+
cmd := newUseCmd()
|
|
99
|
+
cmd.ParseFlags([]string{"--account", "my-company"})
|
|
100
|
+
|
|
101
|
+
cfg := &config.Config{}
|
|
102
|
+
testApp := &app.App{Config: cfg}
|
|
103
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
104
|
+
|
|
105
|
+
if err := handleUse(cmd); err != nil {
|
|
106
|
+
t.Fatalf("handleUse failed: %v", err)
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
savedCfg, err := config.Load()
|
|
110
|
+
if err != nil {
|
|
111
|
+
t.Fatalf("failed to load config: %v", err)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if savedCfg.SelectedAccount != "my-company" {
|
|
115
|
+
t.Errorf("expected SelectedAccount=my-company, got %s", savedCfg.SelectedAccount)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
func TestUseCommandBoardNotFound(t *testing.T) {
|
|
120
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
121
|
+
if r.Method != http.MethodGet {
|
|
122
|
+
t.Errorf("expected GET, got %s", r.Method)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
auth := r.Header.Get("Authorization")
|
|
126
|
+
if auth != "Bearer test-token" {
|
|
127
|
+
t.Errorf("expected Bearer test-token, got %s", auth)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
w.Header().Set("Content-Type", "application/json")
|
|
131
|
+
response := []api.Board{
|
|
132
|
+
{ID: "board-123", Name: "Existing Board"},
|
|
133
|
+
}
|
|
134
|
+
json.NewEncoder(w).Encode(response)
|
|
135
|
+
}))
|
|
136
|
+
defer server.Close()
|
|
137
|
+
|
|
138
|
+
tmpDir := t.TempDir()
|
|
139
|
+
t.Setenv("HOME", tmpDir)
|
|
140
|
+
|
|
141
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
142
|
+
testApp := &app.App{
|
|
143
|
+
Client: client,
|
|
144
|
+
Config: &config.Config{},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
cmd := newUseCmd()
|
|
148
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
149
|
+
cmd.ParseFlags([]string{"--board", "Nonexistent Board"})
|
|
150
|
+
|
|
151
|
+
err := handleUse(cmd)
|
|
152
|
+
if err == nil {
|
|
153
|
+
t.Errorf("expected error for nonexistent board")
|
|
154
|
+
}
|
|
155
|
+
if err.Error() != "board 'Nonexistent Board' not found" {
|
|
156
|
+
t.Errorf("expected 'not found' error, got %v", err)
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
func TestUseCommandBothFlagsError(t *testing.T) {
|
|
161
|
+
cmd := newUseCmd()
|
|
162
|
+
cmd.ParseFlags([]string{"--board", "Board", "--account", "Account"})
|
|
163
|
+
cmd.SetContext(context.Background())
|
|
164
|
+
|
|
165
|
+
err := handleUse(cmd)
|
|
166
|
+
if err == nil {
|
|
167
|
+
t.Errorf("expected error when both flags provided")
|
|
168
|
+
}
|
|
169
|
+
if err.Error() != "cannot specify both --board and --account" {
|
|
170
|
+
t.Errorf("expected 'both flags' error, got %v", err)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
func TestUseCommandNoFlagsError(t *testing.T) {
|
|
175
|
+
cmd := newUseCmd()
|
|
176
|
+
cmd.SetContext(context.Background())
|
|
177
|
+
cmd.ParseFlags([]string{})
|
|
178
|
+
|
|
179
|
+
err := handleUse(cmd)
|
|
180
|
+
if err == nil {
|
|
181
|
+
t.Errorf("expected error when no flags provided")
|
|
182
|
+
}
|
|
183
|
+
if err.Error() != "must specify either --board or --account" {
|
|
184
|
+
t.Errorf("expected 'no flags' error, got %v", err)
|
|
185
|
+
}
|
|
186
|
+
}
|