Fix TestCampaign sending only subscribers, failing listmonk validation
CI / test (push) Successful in 9s
CI / test (push) Successful in 9s
listmonk's /api/campaigns/:id/test handler binds the request into the
same campReq struct create/update use and validates it in full, so a
request with only {"subscribers": [...]} fails with "Invalid length
for name" before subscribers is ever read. TestCampaign now sends the
campaign's full CampaignInput alongside subscribers.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,5 +39,5 @@ func Test(lm *listmonk.Client, slug string, emails []string) error {
|
|||||||
if camp == nil {
|
if camp == nil {
|
||||||
return fmt.Errorf("no campaign named %q in listmonk — run sync first", slug)
|
return fmt.Errorf("no campaign named %q in listmonk — run sync first", slug)
|
||||||
}
|
}
|
||||||
return lm.TestCampaign(camp.ID, emails)
|
return lm.TestCampaign(camp.ID, camp.AsInput(), emails)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ func syncOne(lm *listmonk.Client, slug, dir, path string, defaultPreviewEmails [
|
|||||||
}
|
}
|
||||||
var previewErr error
|
var previewErr error
|
||||||
if len(recipients) > 0 {
|
if len(recipients) > 0 {
|
||||||
previewErr = lm.TestCampaign(campaignID, recipients)
|
previewErr = lm.TestCampaign(campaignID, input, recipients)
|
||||||
}
|
}
|
||||||
return syncOneResult{Changed: true, PreviewErr: previewErr}, nil
|
return syncOneResult{Changed: true, PreviewErr: previewErr}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -297,6 +297,23 @@ type Campaign struct {
|
|||||||
MediaIDs []int
|
MediaIDs []int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AsInput converts a fetched Campaign back into the CampaignInput shape,
|
||||||
|
// for callers (e.g. Test) that need to re-send a campaign's current fields
|
||||||
|
// against an endpoint that validates the full campaign body.
|
||||||
|
func (camp *Campaign) AsInput() CampaignInput {
|
||||||
|
return CampaignInput{
|
||||||
|
Name: camp.Name,
|
||||||
|
Subject: camp.Subject,
|
||||||
|
FromEmail: camp.FromEmail,
|
||||||
|
TemplateID: camp.TemplateID,
|
||||||
|
Type: camp.Type,
|
||||||
|
Tags: camp.Tags,
|
||||||
|
ListIDs: camp.ListIDs,
|
||||||
|
Body: camp.Body,
|
||||||
|
MediaIDs: camp.MediaIDs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CampaignInput is what sync.go builds from a campaign's frontmatter+body to
|
// CampaignInput is what sync.go builds from a campaign's frontmatter+body to
|
||||||
// create or update a listmonk campaign.
|
// create or update a listmonk campaign.
|
||||||
type CampaignInput struct {
|
type CampaignInput struct {
|
||||||
@@ -482,14 +499,18 @@ func (c *Client) SetCampaignStatus(id int, status string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestCampaign sends a preview of the campaign's current content to the
|
// TestCampaign sends a preview of the campaign's current content to the
|
||||||
// given addresses without touching its status. Request field confirmed
|
// given addresses without touching its status. listmonk's test-send handler
|
||||||
// against knadh/listmonk's actual handler source (cmd/campaigns.go's
|
// binds the request into the same campReq struct create/update use and
|
||||||
// campReq.SubscriberEmails, json tag "subscribers").
|
// validates it in full, so the campaign's other fields (in particular a
|
||||||
func (c *Client) TestCampaign(id int, emails []string) error {
|
// non-empty "name") must be sent alongside "subscribers" — a request with
|
||||||
|
// just {"subscribers": [...]} fails listmonk's own validation with "Invalid
|
||||||
|
// length for name" before the "subscribers" field is ever looked at.
|
||||||
|
func (c *Client) TestCampaign(id int, in CampaignInput, emails []string) error {
|
||||||
if len(emails) == 0 {
|
if len(emails) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
payload := map[string]any{"subscribers": emails}
|
payload := in.payload()
|
||||||
|
payload["subscribers"] = emails
|
||||||
respBody, status, err := c.do(http.MethodPost, fmt.Sprintf("/api/campaigns/%d/test", id), payload)
|
respBody, status, err := c.do(http.MethodPost, fmt.Sprintf("/api/campaigns/%d/test", id), payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("sending test for campaign %d: %w", id, err)
|
return fmt.Errorf("sending test for campaign %d: %w", id, err)
|
||||||
|
|||||||
@@ -311,13 +311,17 @@ func TestTestCampaign_SendsSubscribersList(t *testing.T) {
|
|||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
c := New(srv.URL, "u", "t")
|
c := New(srv.URL, "u", "t")
|
||||||
if err := c.TestCampaign(7, []string{"me@example.com"}); err != nil {
|
in := CampaignInput{Name: "launch", Subject: "Hi", Body: "body", Type: "regular"}
|
||||||
|
if err := c.TestCampaign(7, in, []string{"me@example.com"}); err != nil {
|
||||||
t.Fatalf("TestCampaign: %v", err)
|
t.Fatalf("TestCampaign: %v", err)
|
||||||
}
|
}
|
||||||
subs, ok := gotBody["subscribers"].([]any)
|
subs, ok := gotBody["subscribers"].([]any)
|
||||||
if !ok || len(subs) != 1 || subs[0] != "me@example.com" {
|
if !ok || len(subs) != 1 || subs[0] != "me@example.com" {
|
||||||
t.Errorf("expected subscribers=[me@example.com], got %+v", gotBody)
|
t.Errorf("expected subscribers=[me@example.com], got %+v", gotBody)
|
||||||
}
|
}
|
||||||
|
if gotBody["name"] != "launch" {
|
||||||
|
t.Errorf("expected the full campaign body (name=launch) alongside subscribers, got %+v", gotBody)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestErrorResponsesAreWrappedWithStatusAndBody(t *testing.T) {
|
func TestErrorResponsesAreWrappedWithStatusAndBody(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user