fizzy-cli 0.2.0 → 0.3.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/.env +1 -0
- package/CHANGELOG.md +22 -0
- package/bin/fizzy +0 -0
- package/cmd/board.go +1 -1
- package/cmd/card_assign.go +55 -0
- package/cmd/card_assign_test.go +130 -0
- package/cmd/card_triage.go +46 -0
- package/cmd/card_update.go +0 -1
- package/cmd/card_update_test.go +0 -2
- package/cmd/login.go +2 -1
- package/cmd/notification.go +14 -0
- package/cmd/notification_list.go +69 -0
- package/cmd/notification_list_test.go +288 -0
- package/cmd/notification_read.go +51 -0
- package/cmd/notification_read_all.go +38 -0
- package/cmd/notification_read_all_test.go +75 -0
- package/cmd/notification_read_test.go +138 -0
- package/cmd/notification_unread.go +44 -0
- package/cmd/notification_unread_test.go +99 -0
- package/docs/API.md +144 -1
- package/go.mod +1 -1
- package/internal/api/client.go +137 -0
- package/internal/config/config.go +1 -0
- package/internal/ui/notification_list.go +27 -0
- package/package.json +1 -1
- package/scripts/postinstall.js +5 -1
- 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
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
9
|
+
"github.com/rogeriopvl/fizzy/internal/ui"
|
|
10
|
+
"github.com/spf13/cobra"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
var notificationReadCmd = &cobra.Command{
|
|
14
|
+
Use: "read <notification_id>",
|
|
15
|
+
Short: "Mark notification as read and display it",
|
|
16
|
+
Long: `Mark a notification as read and display its content`,
|
|
17
|
+
Args: cobra.ExactArgs(1),
|
|
18
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
19
|
+
if err := handleReadNotification(cmd, args[0]); err != nil {
|
|
20
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func handleReadNotification(cmd *cobra.Command, notificationID string) error {
|
|
26
|
+
a := app.FromContext(cmd.Context())
|
|
27
|
+
if a == nil || a.Client == nil {
|
|
28
|
+
return fmt.Errorf("API client not available")
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if _, err := a.Client.PostNotificationReading(context.Background(), notificationID); err != nil {
|
|
32
|
+
if strings.Contains(err.Error(), "404") {
|
|
33
|
+
return fmt.Errorf("notification not found")
|
|
34
|
+
}
|
|
35
|
+
return fmt.Errorf("marking notification as read: %w", err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
notification, err := a.Client.GetNotification(context.Background(), notificationID)
|
|
39
|
+
if err != nil {
|
|
40
|
+
if strings.Contains(err.Error(), "404") {
|
|
41
|
+
return fmt.Errorf("notification not found")
|
|
42
|
+
}
|
|
43
|
+
return fmt.Errorf("fetching notification: %w", err)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return ui.DisplayNotification(notification)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func init() {
|
|
50
|
+
notificationCmd.AddCommand(notificationReadCmd)
|
|
51
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
|
|
7
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
8
|
+
"github.com/spf13/cobra"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
var notificationReadAllCmd = &cobra.Command{
|
|
12
|
+
Use: "read-all",
|
|
13
|
+
Short: "Mark all unread notifications as read",
|
|
14
|
+
Long: `Mark all unread notifications as read`,
|
|
15
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
16
|
+
if err := handleReadAllNotifications(cmd); err != nil {
|
|
17
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
func handleReadAllNotifications(cmd *cobra.Command) error {
|
|
23
|
+
a := app.FromContext(cmd.Context())
|
|
24
|
+
if a == nil || a.Client == nil {
|
|
25
|
+
return fmt.Errorf("API client not available")
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if _, err := a.Client.PostBulkNotificationsReading(context.Background()); err != nil {
|
|
29
|
+
return fmt.Errorf("marking all notifications as read: %w", err)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fmt.Println("✓ All notifications marked as read successfully")
|
|
33
|
+
return nil
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
func init() {
|
|
37
|
+
notificationCmd.AddCommand(notificationReadAllCmd)
|
|
38
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"net/http"
|
|
6
|
+
"net/http/httptest"
|
|
7
|
+
"testing"
|
|
8
|
+
|
|
9
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
10
|
+
"github.com/rogeriopvl/fizzy/internal/config"
|
|
11
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
func TestNotificationReadAllCommand(t *testing.T) {
|
|
15
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
16
|
+
if r.URL.Path == "/notifications/bulk_reading" && r.Method == http.MethodPost {
|
|
17
|
+
w.WriteHeader(http.StatusNoContent)
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
22
|
+
w.WriteHeader(http.StatusNotFound)
|
|
23
|
+
}))
|
|
24
|
+
defer server.Close()
|
|
25
|
+
|
|
26
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
27
|
+
testApp := &app.App{
|
|
28
|
+
Client: client,
|
|
29
|
+
Config: &config.Config{SelectedBoard: "board-123"},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
cmd := notificationReadAllCmd
|
|
33
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
34
|
+
|
|
35
|
+
if err := handleReadAllNotifications(cmd); err != nil {
|
|
36
|
+
t.Fatalf("handleReadAllNotifications failed: %v", err)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
func TestNotificationReadAllCommandAPIError(t *testing.T) {
|
|
41
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
42
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
43
|
+
w.Write([]byte("Internal Server Error"))
|
|
44
|
+
}))
|
|
45
|
+
defer server.Close()
|
|
46
|
+
|
|
47
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
48
|
+
testApp := &app.App{
|
|
49
|
+
Client: client,
|
|
50
|
+
Config: &config.Config{SelectedBoard: "board-123"},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
cmd := notificationReadAllCmd
|
|
54
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
55
|
+
|
|
56
|
+
err := handleReadAllNotifications(cmd)
|
|
57
|
+
if err == nil {
|
|
58
|
+
t.Errorf("expected error for API failure")
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
func TestNotificationReadAllCommandNoClient(t *testing.T) {
|
|
63
|
+
testApp := &app.App{}
|
|
64
|
+
|
|
65
|
+
cmd := notificationReadAllCmd
|
|
66
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
67
|
+
|
|
68
|
+
err := handleReadAllNotifications(cmd)
|
|
69
|
+
if err == nil {
|
|
70
|
+
t.Errorf("expected error when client not available")
|
|
71
|
+
}
|
|
72
|
+
if err.Error() != "API client not available" {
|
|
73
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -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/config"
|
|
13
|
+
"github.com/rogeriopvl/fizzy/internal/testutil"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func TestNotificationReadCommand(t *testing.T) {
|
|
17
|
+
requestCount := 0
|
|
18
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
19
|
+
requestCount++
|
|
20
|
+
|
|
21
|
+
if r.URL.Path == "/notifications/notif-123/reading" && r.Method == http.MethodPost {
|
|
22
|
+
w.WriteHeader(http.StatusNoContent)
|
|
23
|
+
return
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if r.URL.Path == "/notifications/notif-123" && r.Method == http.MethodGet {
|
|
27
|
+
w.Header().Set("Content-Type", "application/json")
|
|
28
|
+
response := api.Notification{
|
|
29
|
+
ID: "notif-123",
|
|
30
|
+
Read: true,
|
|
31
|
+
ReadAt: "2025-01-01T00:00:00Z",
|
|
32
|
+
CreatedAt: "2025-01-01T00:00:00Z",
|
|
33
|
+
Title: "Test Notification",
|
|
34
|
+
Body: "This is a test notification",
|
|
35
|
+
Creator: api.User{
|
|
36
|
+
ID: "user-123",
|
|
37
|
+
Name: "David Heinemeier Hansson",
|
|
38
|
+
Email: "david@example.com",
|
|
39
|
+
Role: "owner",
|
|
40
|
+
Active: true,
|
|
41
|
+
CreatedAt: "2025-12-05T19:36:35.401Z",
|
|
42
|
+
},
|
|
43
|
+
Card: api.CardReference{
|
|
44
|
+
ID: "card-123",
|
|
45
|
+
Title: "Test Card",
|
|
46
|
+
Status: "published",
|
|
47
|
+
URL: "http://fizzy.localhost:3006/897362094/cards/1",
|
|
48
|
+
},
|
|
49
|
+
URL: "http://fizzy.localhost:3006/897362094/notifications/notif-123",
|
|
50
|
+
}
|
|
51
|
+
json.NewEncoder(w).Encode(response)
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
|
56
|
+
w.WriteHeader(http.StatusNotFound)
|
|
57
|
+
}))
|
|
58
|
+
defer server.Close()
|
|
59
|
+
|
|
60
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
61
|
+
testApp := &app.App{
|
|
62
|
+
Client: client,
|
|
63
|
+
Config: &config.Config{SelectedBoard: "board-123"},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
cmd := notificationReadCmd
|
|
67
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
68
|
+
|
|
69
|
+
if err := handleReadNotification(cmd, "notif-123"); err != nil {
|
|
70
|
+
t.Fatalf("handleReadNotification failed: %v", err)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if requestCount != 2 {
|
|
74
|
+
t.Errorf("expected 2 requests (POST and GET), got %d", requestCount)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
func TestNotificationReadCommandNotFound(t *testing.T) {
|
|
79
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
80
|
+
w.WriteHeader(http.StatusNotFound)
|
|
81
|
+
w.Write([]byte("Notification not found"))
|
|
82
|
+
}))
|
|
83
|
+
defer server.Close()
|
|
84
|
+
|
|
85
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
86
|
+
testApp := &app.App{
|
|
87
|
+
Client: client,
|
|
88
|
+
Config: &config.Config{SelectedBoard: "board-123"},
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
cmd := notificationReadCmd
|
|
92
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
93
|
+
|
|
94
|
+
err := handleReadNotification(cmd, "notif-invalid")
|
|
95
|
+
if err == nil {
|
|
96
|
+
t.Errorf("expected error for invalid notification")
|
|
97
|
+
}
|
|
98
|
+
if err.Error() != "notification not found" {
|
|
99
|
+
t.Errorf("expected 'notification not found' error, got %v", err)
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
func TestNotificationReadCommandAPIError(t *testing.T) {
|
|
104
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
105
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
106
|
+
w.Write([]byte("Internal Server Error"))
|
|
107
|
+
}))
|
|
108
|
+
defer server.Close()
|
|
109
|
+
|
|
110
|
+
client := testutil.NewTestClient(server.URL, "", "board-123", "test-token")
|
|
111
|
+
testApp := &app.App{
|
|
112
|
+
Client: client,
|
|
113
|
+
Config: &config.Config{SelectedBoard: "board-123"},
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
cmd := notificationReadCmd
|
|
117
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
118
|
+
|
|
119
|
+
err := handleReadNotification(cmd, "notif-123")
|
|
120
|
+
if err == nil {
|
|
121
|
+
t.Errorf("expected error for API failure")
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func TestNotificationReadCommandNoClient(t *testing.T) {
|
|
126
|
+
testApp := &app.App{}
|
|
127
|
+
|
|
128
|
+
cmd := notificationReadCmd
|
|
129
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
130
|
+
|
|
131
|
+
err := handleReadNotification(cmd, "notif-123")
|
|
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
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
package cmd
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/rogeriopvl/fizzy/internal/app"
|
|
9
|
+
"github.com/spf13/cobra"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
var notificationUnreadCmd = &cobra.Command{
|
|
13
|
+
Use: "unread <notification_id>",
|
|
14
|
+
Short: "Mark notification as unread",
|
|
15
|
+
Long: `Mark a notification as unread`,
|
|
16
|
+
Args: cobra.ExactArgs(1),
|
|
17
|
+
Run: func(cmd *cobra.Command, args []string) {
|
|
18
|
+
if err := handleUnreadNotification(cmd, args[0]); err != nil {
|
|
19
|
+
fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
func handleUnreadNotification(cmd *cobra.Command, notificationID string) error {
|
|
25
|
+
a := app.FromContext(cmd.Context())
|
|
26
|
+
if a == nil || a.Client == nil {
|
|
27
|
+
return fmt.Errorf("API client not available")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
_, err := a.Client.DeleteNotificationReading(context.Background(), notificationID)
|
|
31
|
+
if err != nil {
|
|
32
|
+
if strings.Contains(err.Error(), "404") {
|
|
33
|
+
return fmt.Errorf("notification not found")
|
|
34
|
+
}
|
|
35
|
+
return fmt.Errorf("marking notification as unread: %w", err)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
fmt.Printf("✓ Notification marked as unread successfully\n")
|
|
39
|
+
return nil
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func init() {
|
|
43
|
+
notificationCmd.AddCommand(notificationUnreadCmd)
|
|
44
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
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 TestNotificationUnreadCommand(t *testing.T) {
|
|
14
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
15
|
+
if r.URL.Path != "/notifications/notif-123/reading" {
|
|
16
|
+
t.Errorf("expected /notifications/notif-123/reading, 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 := notificationUnreadCmd
|
|
35
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
36
|
+
|
|
37
|
+
if err := handleUnreadNotification(cmd, "notif-123"); err != nil {
|
|
38
|
+
t.Fatalf("handleUnreadNotification failed: %v", err)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
func TestNotificationUnreadCommandNotFound(t *testing.T) {
|
|
43
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
44
|
+
w.WriteHeader(http.StatusNotFound)
|
|
45
|
+
w.Write([]byte("Notification not found"))
|
|
46
|
+
}))
|
|
47
|
+
defer server.Close()
|
|
48
|
+
|
|
49
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
50
|
+
testApp := &app.App{Client: client}
|
|
51
|
+
|
|
52
|
+
cmd := notificationUnreadCmd
|
|
53
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
54
|
+
|
|
55
|
+
err := handleUnreadNotification(cmd, "notif-invalid")
|
|
56
|
+
if err == nil {
|
|
57
|
+
t.Errorf("expected error for invalid notification")
|
|
58
|
+
}
|
|
59
|
+
if err.Error() != "notification not found" {
|
|
60
|
+
t.Errorf("expected 'notification not found' error, got %v", err)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
func TestNotificationUnreadCommandAPIError(t *testing.T) {
|
|
65
|
+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
66
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
67
|
+
w.Write([]byte("Internal Server Error"))
|
|
68
|
+
}))
|
|
69
|
+
defer server.Close()
|
|
70
|
+
|
|
71
|
+
client := testutil.NewTestClient(server.URL, "", "", "test-token")
|
|
72
|
+
testApp := &app.App{Client: client}
|
|
73
|
+
|
|
74
|
+
cmd := notificationUnreadCmd
|
|
75
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
76
|
+
|
|
77
|
+
err := handleUnreadNotification(cmd, "notif-123")
|
|
78
|
+
if err == nil {
|
|
79
|
+
t.Errorf("expected error for API failure")
|
|
80
|
+
}
|
|
81
|
+
if err.Error() != "marking notification as unread: unexpected status code 500: Internal Server Error" {
|
|
82
|
+
t.Errorf("expected API error, got %v", err)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
func TestNotificationUnreadCommandNoClient(t *testing.T) {
|
|
87
|
+
testApp := &app.App{}
|
|
88
|
+
|
|
89
|
+
cmd := notificationUnreadCmd
|
|
90
|
+
cmd.SetContext(testApp.ToContext(context.Background()))
|
|
91
|
+
|
|
92
|
+
err := handleUnreadNotification(cmd, "notif-123")
|
|
93
|
+
if err == nil {
|
|
94
|
+
t.Errorf("expected error when client not available")
|
|
95
|
+
}
|
|
96
|
+
if err.Error() != "API client not available" {
|
|
97
|
+
t.Errorf("expected 'client not available' error, got %v", err)
|
|
98
|
+
}
|
|
99
|
+
}
|
package/docs/API.md
CHANGED
|
@@ -5,6 +5,13 @@ a bot to perform various actions for you.
|
|
|
5
5
|
|
|
6
6
|
## Authentication
|
|
7
7
|
|
|
8
|
+
There are two ways to authenticate with the Fizzy API:
|
|
9
|
+
|
|
10
|
+
1. **Personal access tokens** - Long-lived tokens for scripts and integrations
|
|
11
|
+
2. **Magic link authentication** - Session-based authentication for native apps
|
|
12
|
+
|
|
13
|
+
### Personal Access Tokens
|
|
14
|
+
|
|
8
15
|
To use the API you'll need an access token. To get one, go to your profile, then,
|
|
9
16
|
in the API section, click on "Personal access tokens" and then click on
|
|
10
17
|
"Generate new access token".
|
|
@@ -36,6 +43,81 @@ To authenticate a request using your access token, include it in the `Authorizat
|
|
|
36
43
|
curl -H "Authorization: Bearer put-your-access-token-here" -H "Accept: application/json" https://app.fizzy.do/my/identity
|
|
37
44
|
```
|
|
38
45
|
|
|
46
|
+
### Magic Link Authentication
|
|
47
|
+
|
|
48
|
+
For native apps, you can authenticate users via magic links. This is a two-step process:
|
|
49
|
+
|
|
50
|
+
#### 1. Request a magic link
|
|
51
|
+
|
|
52
|
+
Send the user's email address to request a magic link be sent to them:
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
curl -X POST \
|
|
56
|
+
-H "Content-Type: application/json" \
|
|
57
|
+
-H "Accept: application/json" \
|
|
58
|
+
-d '{"email_address": "user@example.com"}' \
|
|
59
|
+
https://app.fizzy.do/session
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
__Response:__
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
HTTP/1.1 201 Created
|
|
66
|
+
Set-Cookie: pending_authentication_token=...; HttpOnly; SameSite=Lax
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"pending_authentication_token": "eyJfcmFpbHMi..."
|
|
72
|
+
}
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The response includes a `pending_authentication_token` both in the JSON body and as a cookie.
|
|
76
|
+
Native apps should store this token and include it as a cookie when submitting the magic link code.
|
|
77
|
+
|
|
78
|
+
__Error responses:__
|
|
79
|
+
|
|
80
|
+
| Status Code | Description |
|
|
81
|
+
|--------|-------------|
|
|
82
|
+
| `422 Unprocessable entity` | Invalid email address, if sign ups are enabled and the value isn't a valid email address |
|
|
83
|
+
| `429 Too Many Requests` | Rate limit exceeded |
|
|
84
|
+
|
|
85
|
+
#### 2. Submit the magic link code
|
|
86
|
+
|
|
87
|
+
Once the user receives the magic link email, they'll have a 6-character code. Submit it to complete authentication:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
curl -X POST \
|
|
91
|
+
-H "Content-Type: application/json" \
|
|
92
|
+
-H "Accept: application/json" \
|
|
93
|
+
-H "Cookie: pending_authentication_token=eyJfcmFpbHMi..." \
|
|
94
|
+
-d '{"code": "ABC123"}' \
|
|
95
|
+
https://app.fizzy.do/session/magic_link
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
__Response:__
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"session_token": "eyJfcmFpbHMi..."
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The `session_token` can be used to authenticate subsequent requests by including it as a cookie:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
curl -H "Cookie: session_token=eyJfcmFpbHMi..." \
|
|
110
|
+
-H "Accept: application/json" \
|
|
111
|
+
https://app.fizzy.do/my/identity
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
__Error responses:__
|
|
115
|
+
|
|
116
|
+
| Status Code | Description |
|
|
117
|
+
|--------|-------------|
|
|
118
|
+
| `401 Unauthorized` | Invalid `pending_authentication_token` or `code` |
|
|
119
|
+
| `429 Too Many Requests` | Rate limit exceeded |
|
|
120
|
+
|
|
39
121
|
## Caching
|
|
40
122
|
|
|
41
123
|
Most endpoints return [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) and [Cache-Control](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control) headers. You can use these to avoid re-downloading unchanged data.
|
|
@@ -482,7 +564,60 @@ Returns a specific card by its number.
|
|
|
482
564
|
|
|
483
565
|
__Response:__
|
|
484
566
|
|
|
485
|
-
|
|
567
|
+
```json
|
|
568
|
+
{
|
|
569
|
+
"id": "03f5vaeq985jlvwv3arl4srq2",
|
|
570
|
+
"number": 1,
|
|
571
|
+
"title": "First!",
|
|
572
|
+
"status": "published",
|
|
573
|
+
"description": "Hello, World!",
|
|
574
|
+
"description_html": "<div class=\"action-text-content\"><p>Hello, World!</p></div>",
|
|
575
|
+
"image_url": null,
|
|
576
|
+
"tags": ["programming"],
|
|
577
|
+
"golden": false,
|
|
578
|
+
"last_active_at": "2025-12-05T19:38:48.553Z",
|
|
579
|
+
"created_at": "2025-12-05T19:38:48.540Z",
|
|
580
|
+
"url": "http://fizzy.localhost:3006/897362094/cards/4",
|
|
581
|
+
"board": {
|
|
582
|
+
"id": "03f5v9zkft4hj9qq0lsn9ohcm",
|
|
583
|
+
"name": "Fizzy",
|
|
584
|
+
"all_access": true,
|
|
585
|
+
"created_at": "2025-12-05T19:36:35.534Z",
|
|
586
|
+
"url": "http://fizzy.localhost:3006/897362094/boards/03f5v9zkft4hj9qq0lsn9ohcm",
|
|
587
|
+
"creator": {
|
|
588
|
+
"id": "03f5v9zjw7pz8717a4no1h8a7",
|
|
589
|
+
"name": "David Heinemeier Hansson",
|
|
590
|
+
"role": "owner",
|
|
591
|
+
"active": true,
|
|
592
|
+
"email_address": "david@example.com",
|
|
593
|
+
"created_at": "2025-12-05T19:36:35.401Z",
|
|
594
|
+
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
595
|
+
}
|
|
596
|
+
},
|
|
597
|
+
"creator": {
|
|
598
|
+
"id": "03f5v9zjw7pz8717a4no1h8a7",
|
|
599
|
+
"name": "David Heinemeier Hansson",
|
|
600
|
+
"role": "owner",
|
|
601
|
+
"active": true,
|
|
602
|
+
"email_address": "david@example.com",
|
|
603
|
+
"created_at": "2025-12-05T19:36:35.401Z",
|
|
604
|
+
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
605
|
+
},
|
|
606
|
+
"comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
|
|
607
|
+
"steps": [
|
|
608
|
+
{
|
|
609
|
+
"id": "03f8huu0sog76g3s975963b5e",
|
|
610
|
+
"content": "This is the first step",
|
|
611
|
+
"completed": false
|
|
612
|
+
},
|
|
613
|
+
{
|
|
614
|
+
"id": "03f8huu0sog76g3s975969734",
|
|
615
|
+
"content": "This is the second step",
|
|
616
|
+
"completed": false
|
|
617
|
+
}
|
|
618
|
+
]
|
|
619
|
+
}
|
|
620
|
+
```
|
|
486
621
|
|
|
487
622
|
### `POST /:account_slug/boards/:board_id/cards`
|
|
488
623
|
|
|
@@ -661,6 +796,10 @@ __Response:__
|
|
|
661
796
|
"created_at": "2025-12-05T19:36:35.401Z",
|
|
662
797
|
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
663
798
|
},
|
|
799
|
+
"card": {
|
|
800
|
+
"id": "03f5v9zo9qlcwwpyc0ascnikz",
|
|
801
|
+
"url": "http://fizzy.localhost:3006/897362094/cards/03f5v9zo9qlcwwpyc0ascnikz"
|
|
802
|
+
},
|
|
664
803
|
"reactions_url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz/reactions",
|
|
665
804
|
"url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz"
|
|
666
805
|
}
|
|
@@ -691,6 +830,10 @@ __Response:__
|
|
|
691
830
|
"created_at": "2025-12-05T19:36:35.401Z",
|
|
692
831
|
"url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
|
|
693
832
|
},
|
|
833
|
+
"card": {
|
|
834
|
+
"id": "03f5v9zo9qlcwwpyc0ascnikz",
|
|
835
|
+
"url": "http://fizzy.localhost:3006/897362094/cards/03f5v9zo9qlcwwpyc0ascnikz"
|
|
836
|
+
},
|
|
694
837
|
"reactions_url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz/reactions",
|
|
695
838
|
"url": "http://fizzy.localhost:3006/897362094/cards/3/comments/03f5v9zo9qlcwwpyc0ascnikz"
|
|
696
839
|
}
|
package/go.mod
CHANGED
|
@@ -4,13 +4,13 @@ go 1.25.1
|
|
|
4
4
|
|
|
5
5
|
require (
|
|
6
6
|
github.com/charmbracelet/bubbletea v1.3.10
|
|
7
|
+
github.com/charmbracelet/lipgloss v1.1.0
|
|
7
8
|
github.com/spf13/cobra v1.10.2
|
|
8
9
|
)
|
|
9
10
|
|
|
10
11
|
require (
|
|
11
12
|
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
12
13
|
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
13
|
-
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
14
14
|
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
|
15
15
|
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
16
16
|
github.com/charmbracelet/x/term v0.2.1 // indirect
|