Skip to content

Commit a55ea51

Browse files
committed
campaigns update
1 parent 9fc8dd1 commit a55ea51

4 files changed

Lines changed: 243 additions & 0 deletions

File tree

cmd/campaigns.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,44 @@ var campaignsCreateCmd = &cobra.Command{
101101
},
102102
}
103103

104+
func runCampaignsUpdate(cfg *config.Config, id string, req api.UpdateCampaignRequest) (*api.Campaign, error) {
105+
return newAPIClient(cfg).UpdateCampaign(id, req)
106+
}
107+
108+
var campaignsUpdateCmd = &cobra.Command{
109+
Use: "update <id>",
110+
Short: "Update a draft campaign",
111+
Args: cobra.ExactArgs(1),
112+
RunE: func(cmd *cobra.Command, args []string) error {
113+
name, _ := cmd.Flags().GetString("name")
114+
115+
cfg, err := loadConfig()
116+
if err != nil {
117+
return err
118+
}
119+
120+
c, err := runCampaignsUpdate(cfg, args[0], api.UpdateCampaignRequest{Name: name})
121+
if err != nil {
122+
return err
123+
}
124+
125+
if isJSONOutput() {
126+
return printJSON(cmd.OutOrStdout(), c)
127+
}
128+
129+
fmt.Fprintf(cmd.OutOrStdout(), "Updated. (id: %s)\n\n", c.CampaignID)
130+
131+
t := newStyledTable(cmd.OutOrStdout(), "FIELD", "VALUE")
132+
t.Row("campaignId", c.CampaignID)
133+
t.Row("emailMessageId", deref(c.EmailMessageID))
134+
t.Row("name", c.Name)
135+
t.Row("status", c.Status)
136+
t.Row("createdAt", c.CreatedAt)
137+
t.Row("updatedAt", c.UpdatedAt)
138+
return t.Render()
139+
},
140+
}
141+
104142
var campaignsGetCmd = &cobra.Command{
105143
Use: "get <id>",
106144
Short: "Get a campaign",
@@ -140,5 +178,9 @@ func init() {
140178
campaignsCreateCmd.MarkFlagRequired("name")
141179
campaignsCmd.AddCommand(campaignsCreateCmd)
142180

181+
campaignsUpdateCmd.Flags().StringP("name", "n", "", "Campaign name (required)")
182+
campaignsUpdateCmd.MarkFlagRequired("name")
183+
campaignsCmd.AddCommand(campaignsUpdateCmd)
184+
143185
rootCmd.AddCommand(campaignsCmd)
144186
}

cmd/campaigns_update_test.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
package cmd
2+
3+
import (
4+
"net/http"
5+
"testing"
6+
7+
"github.com/loops-so/cli/internal/api"
8+
)
9+
10+
func TestRunCampaignsUpdate(t *testing.T) {
11+
body := `{
12+
"success": true,
13+
"campaignId": "cmp_abc123",
14+
"emailMessageId": "em_abc123",
15+
"name": "Renamed",
16+
"status": "Draft",
17+
"createdAt": "2026-04-01T10:00:00Z",
18+
"updatedAt": "2026-04-25T10:00:00Z"
19+
}`
20+
21+
t.Run("returns campaign on success", func(t *testing.T) {
22+
serveJSON(t, http.StatusOK, body)
23+
c, err := runCampaignsUpdate(cfg(t), "cmp_abc123", api.UpdateCampaignRequest{Name: "Renamed"})
24+
if err != nil {
25+
t.Fatalf("unexpected error: %v", err)
26+
}
27+
if c.CampaignID != "cmp_abc123" {
28+
t.Errorf("CampaignID = %q, want cmp_abc123", c.CampaignID)
29+
}
30+
if c.Name != "Renamed" {
31+
t.Errorf("Name = %q, want Renamed", c.Name)
32+
}
33+
if deref(c.EmailMessageID) != "em_abc123" {
34+
t.Errorf("EmailMessageID = %q, want em_abc123", deref(c.EmailMessageID))
35+
}
36+
})
37+
38+
t.Run("returns error when not in draft", func(t *testing.T) {
39+
serveJSON(t, http.StatusConflict, `{"success":false,"message":"Campaign is not in draft status"}`)
40+
_, err := runCampaignsUpdate(cfg(t), "cmp_abc123", api.UpdateCampaignRequest{Name: "Renamed"})
41+
if err == nil {
42+
t.Fatal("expected error, got nil")
43+
}
44+
})
45+
}

internal/api/campaigns.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ type CreateCampaignRequest struct {
4747
Name string `json:"name"`
4848
}
4949

50+
type UpdateCampaignRequest struct {
51+
Name string `json:"name"`
52+
}
53+
5054
type CampaignCreateResponse struct {
5155
Campaign
5256
EmailMessageContentRevisionID *string `json:"emailMessageContentRevisionId"`
@@ -81,6 +85,35 @@ func (c *Client) CreateCampaign(req CreateCampaignRequest) (*CampaignCreateRespo
8185
return &result, nil
8286
}
8387

88+
func (c *Client) UpdateCampaign(id string, req UpdateCampaignRequest) (*Campaign, error) {
89+
b, err := json.Marshal(req)
90+
if err != nil {
91+
return nil, fmt.Errorf("failed to encode request: %w", err)
92+
}
93+
94+
httpReq, err := c.newRequest(http.MethodPost, "/campaigns/"+id, bytes.NewReader(b))
95+
if err != nil {
96+
return nil, err
97+
}
98+
99+
resp, err := c.do(httpReq)
100+
if err != nil {
101+
return nil, err
102+
}
103+
defer resp.Body.Close()
104+
105+
if resp.StatusCode != http.StatusOK {
106+
return nil, errorFromResponse(resp)
107+
}
108+
109+
var result Campaign
110+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
111+
return nil, fmt.Errorf("failed to decode response: %w", err)
112+
}
113+
114+
return &result, nil
115+
}
116+
84117
func (c *Client) GetCampaign(id string) (*Campaign, error) {
85118
req, err := c.newRequest(http.MethodGet, "/campaigns/"+id, nil)
86119
if err != nil {

internal/api/campaigns_test.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,129 @@ func TestCreateCampaign_RequestBody(t *testing.T) {
154154
}
155155
}
156156

157+
const updateCampaignResponse = `{
158+
"success": true,
159+
"campaignId": "cmp_abc123",
160+
"emailMessageId": "em_abc123",
161+
"name": "Renamed",
162+
"status": "Draft",
163+
"createdAt": "2026-04-01T10:00:00Z",
164+
"updatedAt": "2026-04-25T10:00:00Z"
165+
}`
166+
167+
func TestUpdateCampaign(t *testing.T) {
168+
tests := []struct {
169+
name string
170+
statusCode int
171+
body string
172+
wantAPIErr *APIError
173+
wantErrMsg string
174+
}{
175+
{
176+
name: "success",
177+
statusCode: http.StatusOK,
178+
body: updateCampaignResponse,
179+
},
180+
{
181+
name: "not found",
182+
statusCode: http.StatusNotFound,
183+
body: `{"success":false,"message":"Campaign not found"}`,
184+
wantAPIErr: &APIError{StatusCode: http.StatusNotFound, Message: "Campaign not found"},
185+
},
186+
{
187+
name: "not in draft",
188+
statusCode: http.StatusConflict,
189+
body: `{"success":false,"message":"Campaign is not in draft status"}`,
190+
wantAPIErr: &APIError{StatusCode: http.StatusConflict, Message: "Campaign is not in draft status"},
191+
},
192+
{
193+
name: "invalid json",
194+
statusCode: http.StatusOK,
195+
body: `not json`,
196+
wantErrMsg: "failed to decode response",
197+
},
198+
}
199+
200+
for _, tt := range tests {
201+
t.Run(tt.name, func(t *testing.T) {
202+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
203+
w.WriteHeader(tt.statusCode)
204+
w.Write([]byte(tt.body))
205+
}))
206+
defer server.Close()
207+
208+
client := NewClient(server.URL, "test-key", false)
209+
result, err := client.UpdateCampaign("cmp_abc123", UpdateCampaignRequest{Name: "Renamed"})
210+
211+
if tt.wantAPIErr != nil {
212+
var apiErr *APIError
213+
if !errors.As(err, &apiErr) {
214+
t.Fatalf("expected *APIError, got %T: %v", err, err)
215+
}
216+
if apiErr.StatusCode != tt.wantAPIErr.StatusCode {
217+
t.Errorf("StatusCode = %d, want %d", apiErr.StatusCode, tt.wantAPIErr.StatusCode)
218+
}
219+
if apiErr.Message != tt.wantAPIErr.Message {
220+
t.Errorf("Message = %q, want %q", apiErr.Message, tt.wantAPIErr.Message)
221+
}
222+
return
223+
}
224+
225+
if tt.wantErrMsg != "" {
226+
if err == nil {
227+
t.Fatalf("expected error containing %q, got nil", tt.wantErrMsg)
228+
}
229+
if !strings.Contains(err.Error(), tt.wantErrMsg) {
230+
t.Errorf("error = %q, want it to contain %q", err.Error(), tt.wantErrMsg)
231+
}
232+
return
233+
}
234+
235+
if err != nil {
236+
t.Fatalf("unexpected error: %v", err)
237+
}
238+
if result.CampaignID != "cmp_abc123" {
239+
t.Errorf("CampaignID = %q, want cmp_abc123", result.CampaignID)
240+
}
241+
if result.Name != "Renamed" {
242+
t.Errorf("Name = %q, want Renamed", result.Name)
243+
}
244+
})
245+
}
246+
}
247+
248+
func TestUpdateCampaign_RequestBodyAndPath(t *testing.T) {
249+
var (
250+
gotPath string
251+
gotMethod string
252+
body map[string]any
253+
)
254+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
255+
gotPath = r.URL.Path
256+
gotMethod = r.Method
257+
b, _ := io.ReadAll(r.Body)
258+
json.Unmarshal(b, &body)
259+
w.WriteHeader(http.StatusOK)
260+
w.Write([]byte(updateCampaignResponse))
261+
}))
262+
defer server.Close()
263+
264+
client := NewClient(server.URL, "test-key", false)
265+
if _, err := client.UpdateCampaign("cmp_abc123", UpdateCampaignRequest{Name: "Renamed"}); err != nil {
266+
t.Fatalf("unexpected error: %v", err)
267+
}
268+
269+
if gotMethod != http.MethodPost {
270+
t.Errorf("method = %q, want POST", gotMethod)
271+
}
272+
if gotPath != "/campaigns/cmp_abc123" {
273+
t.Errorf("path = %q, want /campaigns/cmp_abc123", gotPath)
274+
}
275+
if body["name"] != "Renamed" {
276+
t.Errorf("name = %v, want Renamed", body["name"])
277+
}
278+
}
279+
157280
func TestGetCampaign(t *testing.T) {
158281
body := `{
159282
"success": true,

0 commit comments

Comments
 (0)