fizzy-cli 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,110 @@
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 TestUserShowCommand(t *testing.T) {
16
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17
+ if r.URL.Path != "/users/user-123" {
18
+ t.Errorf("expected /users/user-123, 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
+ w.Header().Set("Content-Type", "application/json")
30
+ response := api.User{
31
+ ID: "user-123",
32
+ Name: "John Doe",
33
+ Email: "john@example.com",
34
+ Role: "admin",
35
+ Active: true,
36
+ CreatedAt: "2025-01-01T00:00:00Z",
37
+ }
38
+ json.NewEncoder(w).Encode(response)
39
+ }))
40
+ defer server.Close()
41
+
42
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
43
+ testApp := &app.App{Client: client}
44
+
45
+ cmd := userShowCmd
46
+ cmd.SetContext(testApp.ToContext(context.Background()))
47
+
48
+ if err := handleShowUser(cmd, "user-123"); err != nil {
49
+ t.Fatalf("handleShowUser failed: %v", err)
50
+ }
51
+ }
52
+
53
+ func TestUserShowCommandNotFound(t *testing.T) {
54
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
55
+ w.WriteHeader(http.StatusNotFound)
56
+ w.Write([]byte("User not found"))
57
+ }))
58
+ defer server.Close()
59
+
60
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
61
+ testApp := &app.App{Client: client}
62
+
63
+ cmd := userShowCmd
64
+ cmd.SetContext(testApp.ToContext(context.Background()))
65
+
66
+ err := handleShowUser(cmd, "nonexistent-user")
67
+ if err == nil {
68
+ t.Errorf("expected error for user not found")
69
+ }
70
+ if err.Error() != "fetching user: unexpected status code 404: User not found" {
71
+ t.Errorf("expected user not found error, got %v", err)
72
+ }
73
+ }
74
+
75
+ func TestUserShowCommandAPIError(t *testing.T) {
76
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77
+ w.WriteHeader(http.StatusInternalServerError)
78
+ w.Write([]byte("Internal Server Error"))
79
+ }))
80
+ defer server.Close()
81
+
82
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
83
+ testApp := &app.App{Client: client}
84
+
85
+ cmd := userShowCmd
86
+ cmd.SetContext(testApp.ToContext(context.Background()))
87
+
88
+ err := handleShowUser(cmd, "user-123")
89
+ if err == nil {
90
+ t.Errorf("expected error for API failure")
91
+ }
92
+ if err.Error() != "fetching user: unexpected status code 500: Internal Server Error" {
93
+ t.Errorf("expected API error, got %v", err)
94
+ }
95
+ }
96
+
97
+ func TestUserShowCommandNoClient(t *testing.T) {
98
+ testApp := &app.App{}
99
+
100
+ cmd := userShowCmd
101
+ cmd.SetContext(testApp.ToContext(context.Background()))
102
+
103
+ err := handleShowUser(cmd, "user-123")
104
+ if err == nil {
105
+ t.Errorf("expected error when client not available")
106
+ }
107
+ if err.Error() != "API client not available" {
108
+ t.Errorf("expected 'client not available' error, got %v", err)
109
+ }
110
+ }
@@ -0,0 +1,71 @@
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 userUpdateCmd = &cobra.Command{
13
+ Use: "update <user_id>",
14
+ Short: "Update a user",
15
+ Long: `Update user settings such as name and avatar.
16
+
17
+ Avatar must be provided as a URL (e.g., https://example.com/avatar.jpg).
18
+
19
+ Example:
20
+ fizzy user update user-123 --name "John Doe"
21
+ fizzy user update user-123 --avatar https://example.com/avatar.jpg`,
22
+ Args: cobra.ExactArgs(1),
23
+ Run: func(cmd *cobra.Command, args []string) {
24
+ if err := handleUpdateUser(cmd, args[0]); err != nil {
25
+ fmt.Fprintf(cmd.OutOrStderr(), "Error: %v\n", err)
26
+ }
27
+ },
28
+ }
29
+
30
+ func handleUpdateUser(cmd *cobra.Command, userID string) error {
31
+ if !cmd.Flags().Changed("name") && !cmd.Flags().Changed("avatar") {
32
+ return fmt.Errorf("at least one flag must be provided (--name or --avatar)")
33
+ }
34
+
35
+ a := app.FromContext(cmd.Context())
36
+ if a == nil || a.Client == nil {
37
+ return fmt.Errorf("API client not available")
38
+ }
39
+
40
+ payload := api.UpdateUserPayload{}
41
+
42
+ if cmd.Flags().Changed("name") {
43
+ name, err := cmd.Flags().GetString("name")
44
+ if err != nil {
45
+ return fmt.Errorf("invalid name flag: %w", err)
46
+ }
47
+ payload.Name = name
48
+ }
49
+ if cmd.Flags().Changed("avatar") {
50
+ avatar, err := cmd.Flags().GetString("avatar")
51
+ if err != nil {
52
+ return fmt.Errorf("invalid avatar flag: %w", err)
53
+ }
54
+ payload.Avatar = avatar
55
+ }
56
+
57
+ err := a.Client.PutUser(context.Background(), userID, payload)
58
+ if err != nil {
59
+ return fmt.Errorf("updating user: %w", err)
60
+ }
61
+
62
+ fmt.Fprintf(cmd.OutOrStdout(), "✓ User '%s' updated successfully\n", userID)
63
+ return nil
64
+ }
65
+
66
+ func init() {
67
+ userUpdateCmd.Flags().StringP("name", "n", "", "User name")
68
+ userUpdateCmd.Flags().String("avatar", "", "Avatar URL (e.g., https://example.com/avatar.jpg)")
69
+
70
+ userCmd.AddCommand(userUpdateCmd)
71
+ }
@@ -0,0 +1,177 @@
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
+ "github.com/spf13/cobra"
15
+ )
16
+
17
+ func TestUserUpdateCommandSuccess(t *testing.T) {
18
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
19
+ if r.URL.Path != "/users/user-123" {
20
+ t.Errorf("expected /users/user-123, got %s", r.URL.Path)
21
+ }
22
+ if r.Method != http.MethodPut {
23
+ t.Errorf("expected PUT, got %s", r.Method)
24
+ }
25
+
26
+ auth := r.Header.Get("Authorization")
27
+ if auth != "Bearer test-token" {
28
+ t.Errorf("expected Bearer test-token, got %s", auth)
29
+ }
30
+
31
+ body, _ := io.ReadAll(r.Body)
32
+ var payload map[string]api.UpdateUserPayload
33
+ if err := json.Unmarshal(body, &payload); err != nil {
34
+ t.Fatalf("failed to unmarshal request body: %v", err)
35
+ }
36
+
37
+ userPayload := payload["user"]
38
+ if userPayload.Name != "Updated Name" {
39
+ t.Errorf("expected name 'Updated Name', got %s", userPayload.Name)
40
+ }
41
+
42
+ w.WriteHeader(http.StatusNoContent)
43
+ }))
44
+ defer server.Close()
45
+
46
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
47
+ testApp := &app.App{Client: client}
48
+
49
+ cmd := userUpdateCmd
50
+ cmd.SetContext(testApp.ToContext(context.Background()))
51
+ cmd.ParseFlags([]string{"--name", "Updated Name"})
52
+
53
+ if err := handleUpdateUser(cmd, "user-123"); err != nil {
54
+ t.Fatalf("handleUpdateUser failed: %v", err)
55
+ }
56
+ }
57
+
58
+ func TestUserUpdateCommandWithAllFlags(t *testing.T) {
59
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
60
+ if r.Method != http.MethodPut {
61
+ t.Errorf("expected PUT, got %s", r.Method)
62
+ }
63
+
64
+ body, _ := io.ReadAll(r.Body)
65
+ var payload map[string]api.UpdateUserPayload
66
+ json.Unmarshal(body, &payload)
67
+
68
+ userPayload := payload["user"]
69
+ if userPayload.Name != "Updated Name" {
70
+ t.Errorf("expected name 'Updated Name', got %s", userPayload.Name)
71
+ }
72
+ if userPayload.Avatar != "https://example.com/avatar.png" {
73
+ t.Errorf("expected avatar 'https://example.com/avatar.png', got %s", userPayload.Avatar)
74
+ }
75
+
76
+ w.WriteHeader(http.StatusNoContent)
77
+ }))
78
+ defer server.Close()
79
+
80
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
81
+ testApp := &app.App{Client: client}
82
+
83
+ cmd := userUpdateCmd
84
+ cmd.SetContext(testApp.ToContext(context.Background()))
85
+ cmd.ParseFlags([]string{
86
+ "--name", "Updated Name",
87
+ "--avatar", "https://example.com/avatar.png",
88
+ })
89
+
90
+ if err := handleUpdateUser(cmd, "user-123"); err != nil {
91
+ t.Fatalf("handleUpdateUser failed: %v", err)
92
+ }
93
+ }
94
+
95
+ func TestUserUpdateCommandNoFlags(t *testing.T) {
96
+ client := testutil.NewTestClient("http://localhost", "", "", "test-token")
97
+ testApp := &app.App{Client: client}
98
+
99
+ cmd := &cobra.Command{
100
+ Use: "update <user_id>",
101
+ Args: cobra.ExactArgs(1),
102
+ }
103
+ cmd.Flags().String("name", "", "User name")
104
+ cmd.Flags().String("avatar", "", "Avatar URL or file path")
105
+
106
+ cmd.SetContext(testApp.ToContext(context.Background()))
107
+
108
+ err := handleUpdateUser(cmd, "user-123")
109
+ if err == nil {
110
+ t.Errorf("expected error when no flags provided")
111
+ }
112
+ if err.Error() != "at least one flag must be provided (--name or --avatar)" {
113
+ t.Errorf("expected flag requirement error, got %v", err)
114
+ }
115
+ }
116
+
117
+ func TestUserUpdateCommandNotFound(t *testing.T) {
118
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
119
+ w.WriteHeader(http.StatusNotFound)
120
+ w.Write([]byte("User not found"))
121
+ }))
122
+ defer server.Close()
123
+
124
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
125
+ testApp := &app.App{Client: client}
126
+
127
+ cmd := userUpdateCmd
128
+ cmd.SetContext(testApp.ToContext(context.Background()))
129
+ cmd.ParseFlags([]string{"--name", "Updated Name"})
130
+
131
+ err := handleUpdateUser(cmd, "nonexistent-user")
132
+ if err == nil {
133
+ t.Errorf("expected error for user not found")
134
+ }
135
+ if err.Error() != "updating user: unexpected status code 404: User not found" {
136
+ t.Errorf("expected user not found error, got %v", err)
137
+ }
138
+ }
139
+
140
+ func TestUserUpdateCommandAPIError(t *testing.T) {
141
+ server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
142
+ w.WriteHeader(http.StatusInternalServerError)
143
+ w.Write([]byte("Internal Server Error"))
144
+ }))
145
+ defer server.Close()
146
+
147
+ client := testutil.NewTestClient(server.URL, "", "", "test-token")
148
+ testApp := &app.App{Client: client}
149
+
150
+ cmd := userUpdateCmd
151
+ cmd.SetContext(testApp.ToContext(context.Background()))
152
+ cmd.ParseFlags([]string{"--name", "Updated Name"})
153
+
154
+ err := handleUpdateUser(cmd, "user-123")
155
+ if err == nil {
156
+ t.Errorf("expected error for API failure")
157
+ }
158
+ if err.Error() != "updating user: unexpected status code 500: Internal Server Error" {
159
+ t.Errorf("expected API error, got %v", err)
160
+ }
161
+ }
162
+
163
+ func TestUserUpdateCommandNoClient(t *testing.T) {
164
+ testApp := &app.App{}
165
+
166
+ cmd := userUpdateCmd
167
+ cmd.SetContext(testApp.ToContext(context.Background()))
168
+ cmd.ParseFlags([]string{"--name", "Updated Name"})
169
+
170
+ err := handleUpdateUser(cmd, "user-123")
171
+ if err == nil {
172
+ t.Errorf("expected error when client not available")
173
+ }
174
+ if err.Error() != "API client not available" {
175
+ t.Errorf("expected 'client not available' error, got %v", err)
176
+ }
177
+ }
package/docs/API.md CHANGED
@@ -553,7 +553,8 @@ __Response:__
553
553
  "created_at": "2025-12-05T19:36:35.401Z",
554
554
  "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
555
555
  },
556
- "comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments"
556
+ "comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
557
+ "reactions_url": "http://fizzy.localhost:3006/897362094/cards/4/reactions"
557
558
  },
558
559
  ]
559
560
  ```
@@ -614,6 +615,7 @@ __Response:__
614
615
  "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
615
616
  },
616
617
  "comments_url": "http://fizzy.localhost:3006/897362094/cards/4/comments",
618
+ "reactions_url": "http://fizzy.localhost:3006/897362094/cards/4/reactions",
617
619
  "steps": [
618
620
  {
619
621
  "id": "03f8huu0sog76g3s975963b5e",
@@ -928,7 +930,66 @@ __Response:__
928
930
 
929
931
  Returns `204 No Content` on success.
930
932
 
931
- ## Reactions
933
+ ## Card Reactions (Boosts)
934
+
935
+ Card reactions (also called "boosts") let users add short responses directly to cards. These are limited to 16 characters.
936
+
937
+ ### `GET /:account_slug/cards/:card_number/reactions`
938
+
939
+ Returns a list of reactions on a card.
940
+
941
+ __Response:__
942
+
943
+ ```json
944
+ [
945
+ {
946
+ "id": "03f5v9zo9qlcwwpyc0ascnikz",
947
+ "content": "👍",
948
+ "reacter": {
949
+ "id": "03f5v9zjw7pz8717a4no1h8a7",
950
+ "name": "David Heinemeier Hansson",
951
+ "role": "owner",
952
+ "active": true,
953
+ "email_address": "david@example.com",
954
+ "created_at": "2025-12-05T19:36:35.401Z",
955
+ "url": "http://fizzy.localhost:3006/897362094/users/03f5v9zjw7pz8717a4no1h8a7"
956
+ },
957
+ "url": "http://fizzy.localhost:3006/897362094/cards/3/reactions/03f5v9zo9qlcwwpyc0ascnikz"
958
+ }
959
+ ]
960
+ ```
961
+
962
+ ### `POST /:account_slug/cards/:card_number/reactions`
963
+
964
+ Adds a reaction (boost) to a card.
965
+
966
+ | Parameter | Type | Required | Description |
967
+ |-----------|------|----------|-------------|
968
+ | `content` | string | Yes | The reaction text (max 16 characters) |
969
+
970
+ __Request:__
971
+
972
+ ```json
973
+ {
974
+ "reaction": {
975
+ "content": "Great 👍"
976
+ }
977
+ }
978
+ ```
979
+
980
+ __Response:__
981
+
982
+ Returns `201 Created` on success.
983
+
984
+ ### `DELETE /:account_slug/cards/:card_number/reactions/:reaction_id`
985
+
986
+ Removes your reaction from a card. Only the reaction creator can remove their own reactions.
987
+
988
+ __Response:__
989
+
990
+ Returns `204 No Content` on success.
991
+
992
+ ## Comment Reactions
932
993
 
933
994
  Reactions are short (16-character max) responses to comments.
934
995
 
@@ -14,14 +14,48 @@ func (c *Client) GetCards(ctx context.Context, filters CardFilters) ([]Card, err
14
14
  return nil, fmt.Errorf("failed to create get cards request: %w", err)
15
15
  }
16
16
 
17
- if len(filters.BoardIDs) > 0 {
18
- q := req.URL.Query()
19
- for _, boardID := range filters.BoardIDs {
20
- q.Add("board_ids[]", boardID)
21
- }
22
- req.URL.RawQuery = q.Encode()
17
+ q := req.URL.Query()
18
+
19
+ for _, boardID := range filters.BoardIDs {
20
+ q.Add("board_ids[]", boardID)
21
+ }
22
+ for _, tagID := range filters.TagIDs {
23
+ q.Add("tag_ids[]", tagID)
24
+ }
25
+ for _, assigneeID := range filters.AssigneeIDs {
26
+ q.Add("assignee_ids[]", assigneeID)
27
+ }
28
+ for _, creatorID := range filters.CreatorIDs {
29
+ q.Add("creator_ids[]", creatorID)
30
+ }
31
+ for _, closerID := range filters.CloserIDs {
32
+ q.Add("closer_ids[]", closerID)
33
+ }
34
+ for _, cardID := range filters.CardIDs {
35
+ q.Add("card_ids[]", cardID)
36
+ }
37
+ for _, term := range filters.Terms {
38
+ q.Add("terms[]", term)
23
39
  }
24
40
 
41
+ if filters.IndexedBy != "" {
42
+ q.Set("indexed_by", filters.IndexedBy)
43
+ }
44
+ if filters.SortedBy != "" {
45
+ q.Set("sorted_by", filters.SortedBy)
46
+ }
47
+ if filters.AssignmentStatus != "" {
48
+ q.Set("assignment_status", filters.AssignmentStatus)
49
+ }
50
+ if filters.CreationStatus != "" {
51
+ q.Set("creation", filters.CreationStatus)
52
+ }
53
+ if filters.ClosureStatus != "" {
54
+ q.Set("closure", filters.ClosureStatus)
55
+ }
56
+
57
+ req.URL.RawQuery = q.Encode()
58
+
25
59
  var response []Card
26
60
  _, err = c.decodeResponse(req, &response)
27
61
  if err != nil {
@@ -67,3 +67,64 @@ func (c *Client) DeleteCommentReaction(ctx context.Context, cardNumber int, comm
67
67
 
68
68
  return true, nil
69
69
  }
70
+
71
+ func (c *Client) GetCardReactions(ctx context.Context, cardNumber int) ([]Reaction, error) {
72
+ endpointURL := fmt.Sprintf("%s/cards/%d/reactions", c.AccountBaseURL, cardNumber)
73
+
74
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
75
+ if err != nil {
76
+ return nil, fmt.Errorf("failed to create get card reactions request: %w", err)
77
+ }
78
+
79
+ var response []Reaction
80
+ _, err = c.decodeResponse(req, &response)
81
+ if err != nil {
82
+ return nil, err
83
+ }
84
+
85
+ return response, nil
86
+ }
87
+
88
+ func (c *Client) PostCardReaction(ctx context.Context, cardNumber int, content string) (*Reaction, error) {
89
+ endpointURL := fmt.Sprintf("%s/cards/%d/reactions", c.AccountBaseURL, cardNumber)
90
+
91
+ payload := map[string]map[string]string{
92
+ "reaction": {"content": content},
93
+ }
94
+
95
+ req, err := c.newRequest(ctx, http.MethodPost, endpointURL, payload)
96
+ if err != nil {
97
+ return nil, fmt.Errorf("failed to create post card reaction request: %w", err)
98
+ }
99
+
100
+ res, err := c.HTTPClient.Do(req)
101
+ if err != nil {
102
+ return nil, fmt.Errorf("failed to make request: %w", err)
103
+ }
104
+ defer res.Body.Close()
105
+
106
+ if res.StatusCode != http.StatusCreated {
107
+ body, _ := io.ReadAll(res.Body)
108
+ return nil, fmt.Errorf("unexpected status code %d: %s", res.StatusCode, string(body))
109
+ }
110
+
111
+ // API returns 201 Created with Location header, not a response body
112
+ // Return a Reaction object with the content field set for reference
113
+ return &Reaction{Content: content}, nil
114
+ }
115
+
116
+ func (c *Client) DeleteCardReaction(ctx context.Context, cardNumber int, reactionID string) (bool, error) {
117
+ endpointURL := fmt.Sprintf("%s/cards/%d/reactions/%s", c.AccountBaseURL, cardNumber, reactionID)
118
+
119
+ req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
120
+ if err != nil {
121
+ return false, fmt.Errorf("failed to create delete card reaction request: %w", err)
122
+ }
123
+
124
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
125
+ if err != nil {
126
+ return false, err
127
+ }
128
+
129
+ return true, nil
130
+ }
@@ -121,6 +121,11 @@ type User struct {
121
121
  URL string `json:"url"`
122
122
  }
123
123
 
124
+ type UpdateUserPayload struct {
125
+ Name string `json:"name,omitempty"`
126
+ Avatar string `json:"avatar,omitempty"`
127
+ }
128
+
124
129
  type Notification struct {
125
130
  ID string `json:"id"`
126
131
  Read bool `json:"read"`
@@ -0,0 +1,75 @@
1
+ package api
2
+
3
+ import (
4
+ "context"
5
+ "fmt"
6
+ "net/http"
7
+ )
8
+
9
+ func (c *Client) GetUsers(ctx context.Context) ([]User, error) {
10
+ endpointURL := c.AccountBaseURL + "/users"
11
+
12
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
13
+ if err != nil {
14
+ return nil, fmt.Errorf("failed to create request: %w", err)
15
+ }
16
+
17
+ var response []User
18
+ _, err = c.decodeResponse(req, &response)
19
+ if err != nil {
20
+ return nil, err
21
+ }
22
+
23
+ return response, nil
24
+ }
25
+
26
+ func (c *Client) GetUser(ctx context.Context, userID string) (*User, error) {
27
+ endpointURL := c.AccountBaseURL + "/users/" + userID
28
+
29
+ req, err := c.newRequest(ctx, http.MethodGet, endpointURL, nil)
30
+ if err != nil {
31
+ return nil, fmt.Errorf("failed to create request: %w", err)
32
+ }
33
+
34
+ var response User
35
+ _, err = c.decodeResponse(req, &response)
36
+ if err != nil {
37
+ return nil, err
38
+ }
39
+
40
+ return &response, nil
41
+ }
42
+
43
+ func (c *Client) PutUser(ctx context.Context, userID string, payload UpdateUserPayload) error {
44
+ endpointURL := c.AccountBaseURL + "/users/" + userID
45
+
46
+ body := map[string]UpdateUserPayload{"user": payload}
47
+
48
+ req, err := c.newRequest(ctx, http.MethodPut, endpointURL, body)
49
+ if err != nil {
50
+ return fmt.Errorf("failed to create update user request: %w", err)
51
+ }
52
+
53
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
54
+ if err != nil {
55
+ return err
56
+ }
57
+
58
+ return nil
59
+ }
60
+
61
+ func (c *Client) DeleteUser(ctx context.Context, userID string) error {
62
+ endpointURL := c.AccountBaseURL + "/users/" + userID
63
+
64
+ req, err := c.newRequest(ctx, http.MethodDelete, endpointURL, nil)
65
+ if err != nil {
66
+ return fmt.Errorf("failed to create delete user request: %w", err)
67
+ }
68
+
69
+ _, err = c.decodeResponse(req, nil, http.StatusNoContent)
70
+ if err != nil {
71
+ return err
72
+ }
73
+
74
+ return nil
75
+ }
@@ -0,0 +1,19 @@
1
+ package ui
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+
7
+ "github.com/rogeriopvl/fizzy/internal/api"
8
+ )
9
+
10
+ func DisplayUsers(w io.Writer, users []api.User) error {
11
+ for _, user := range users {
12
+ status := "active"
13
+ if !user.Active {
14
+ status = "inactive"
15
+ }
16
+ fmt.Fprintf(w, "%s (%s) - %s [%s]\n", user.Name, user.Email, DisplayID(user.ID), status)
17
+ }
18
+ return nil
19
+ }
@@ -0,0 +1,23 @@
1
+ package ui
2
+
3
+ import (
4
+ "fmt"
5
+ "io"
6
+
7
+ "github.com/rogeriopvl/fizzy/internal/api"
8
+ )
9
+
10
+ func DisplayUser(w io.Writer, user *api.User) error {
11
+ status := "active"
12
+ if !user.Active {
13
+ status = "inactive"
14
+ }
15
+
16
+ fmt.Fprintf(w, "User: %s\n", user.Name)
17
+ fmt.Fprintf(w, "ID: %s\n", user.ID)
18
+ fmt.Fprintf(w, "Email: %s\n", user.Email)
19
+ fmt.Fprintf(w, "Role: %s\n", user.Role)
20
+ fmt.Fprintf(w, "Status: %s\n", status)
21
+ fmt.Fprintf(w, "Created At: %s\n", FormatTime(user.CreatedAt))
22
+ return nil
23
+ }