package listmonk import ( "encoding/json" "io" "net/http" "net/http/httptest" "strings" "testing" ) func TestFindListByName_ReturnsIDOnExactMatch(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[{"id":3,"name":"Newsletter"},{"id":4,"name":"Workshop Leads"}]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") id, err := c.FindListByName("Newsletter") if err != nil { t.Fatalf("FindListByName: %v", err) } if id != 3 { t.Errorf("expected id 3, got %d", id) } } func TestFindListByName_ErrorsOnZeroMatches(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[{"id":3,"name":"Newsletter"}]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") if _, err := c.FindListByName("Nonexistent"); err == nil { t.Fatal("expected an error for zero matches, got nil") } } func TestFindListByName_ErrorsOnAmbiguousMatches(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[{"id":3,"name":"Newsletter"},{"id":5,"name":"Newsletter"}]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") if _, err := c.FindListByName("Newsletter"); err == nil { t.Fatal("expected an error for ambiguous matches, got nil") } } func TestFindOrCreateListByName_ReusesExisting(t *testing.T) { var postCount int srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodPost { postCount++ } w.Write([]byte(`{"data":{"results":[{"id":9,"name":"segment:launch"}]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") id, err := c.FindOrCreateListByName("segment:launch") if err != nil { t.Fatalf("FindOrCreateListByName: %v", err) } if id != 9 { t.Errorf("expected existing id 9, got %d", id) } if postCount != 0 { t.Errorf("expected no create call when list already exists, got %d POSTs", postCount) } } func TestFindOrCreateListByName_CreatesWhenMissing(t *testing.T) { var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method == http.MethodGet { w.Write([]byte(`{"data":{"results":[]}}`)) return } b, _ := io.ReadAll(r.Body) json.Unmarshal(b, &gotBody) w.Write([]byte(`{"data":{"id":11}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") id, err := c.FindOrCreateListByName("segment:launch") if err != nil { t.Fatalf("FindOrCreateListByName: %v", err) } if id != 11 { t.Errorf("expected new id 11, got %d", id) } if gotBody["name"] != "segment:launch" { t.Errorf("expected create payload to carry the list name, got %+v", gotBody) } } func TestQueryAddToList_SendsExpectedPayload(t *testing.T) { var gotMethod, gotPath string var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotMethod = r.Method gotPath = r.URL.Path b, _ := io.ReadAll(r.Body) json.Unmarshal(b, &gotBody) w.Write([]byte(`{}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") query := "subscribers.attribs->>'source' = 'workshop'" if err := c.QueryAddToList(query, 9); err != nil { t.Fatalf("QueryAddToList: %v", err) } if gotMethod != http.MethodPut { t.Errorf("expected PUT, got %s", gotMethod) } if gotPath != "/api/subscribers/query/lists" { t.Errorf("expected /api/subscribers/query/lists, got %q", gotPath) } if gotBody["query"] != query { t.Errorf("expected query sent verbatim, got %+v", gotBody) } if gotBody["action"] != "add" { t.Errorf("expected action=add, got %+v", gotBody) } } func TestQueryAddToList_SurfacesListmonkErrors(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"message":"invalid SQL expression"}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") err := c.QueryAddToList("not valid sql", 9) if err == nil || !strings.Contains(err.Error(), "invalid SQL expression") { t.Fatalf("expected listmonk's error message to surface, got %v", err) } } func TestFindMediaByFilename(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":[{"id":5,"filename":"launch-abc123-flyer.pdf"}]}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") id, found, err := c.FindMediaByFilename("launch-abc123-flyer.pdf") if err != nil { t.Fatalf("FindMediaByFilename: %v", err) } if !found || id != 5 { t.Errorf("expected found id 5, got found=%v id=%d", found, id) } _, found, err = c.FindMediaByFilename("nope.pdf") if err != nil { t.Fatalf("FindMediaByFilename: %v", err) } if found { t.Error("expected not found for a filename with no match") } } func TestUploadMedia_SendsMultipartFormFile(t *testing.T) { var gotFilename string var gotContent string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if err := r.ParseMultipartForm(1 << 20); err != nil { t.Fatalf("ParseMultipartForm: %v", err) } file, header, err := r.FormFile("file") if err != nil { t.Fatalf("FormFile: %v", err) } defer file.Close() gotFilename = header.Filename b, _ := io.ReadAll(file) gotContent = string(b) w.Write([]byte(`{"data":{"id":42}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") id, err := c.UploadMedia("flyer.pdf", strings.NewReader("pdf-bytes")) if err != nil { t.Fatalf("UploadMedia: %v", err) } if id != 42 { t.Errorf("expected id 42, got %d", id) } if gotFilename != "flyer.pdf" { t.Errorf("expected filename flyer.pdf, got %q", gotFilename) } if gotContent != "pdf-bytes" { t.Errorf("expected uploaded content to match, got %q", gotContent) } } func TestFindCampaignByName(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[ {"id":1,"name":"launch","status":"draft","subject":"Hi","lists":[{"id":3,"name":"Newsletter"}]} ]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") camp, err := c.FindCampaignByName("launch") if err != nil { t.Fatalf("FindCampaignByName: %v", err) } if camp == nil || camp.ID != 1 || camp.Status != "draft" { t.Fatalf("unexpected campaign: %+v", camp) } if len(camp.ListIDs) != 1 || camp.ListIDs[0] != 3 { t.Errorf("expected list IDs [3], got %v", camp.ListIDs) } } func TestFindCampaignByName_NilOnMiss(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") camp, err := c.FindCampaignByName("missing") if err != nil { t.Fatalf("FindCampaignByName: %v", err) } if camp != nil { t.Errorf("expected nil for a miss, got %+v", camp) } } func TestFindCampaignByName_ErrorsOnAmbiguous(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte(`{"data":{"results":[ {"id":1,"name":"launch"},{"id":2,"name":"launch"} ]}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") if _, err := c.FindCampaignByName("launch"); err == nil { t.Fatal("expected an error for ambiguous campaign names, got nil") } } func TestCreateCampaign_SendsMarkdownContentType(t *testing.T) { var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) json.Unmarshal(b, &gotBody) w.Write([]byte(`{"data":{"id":7,"name":"launch","status":"draft"}}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") camp, err := c.CreateCampaign(CampaignInput{ Name: "launch", Subject: "Hi", FromEmail: "a@b.com", Type: "regular", ListIDs: []int{3}, Body: "# hi", }) if err != nil { t.Fatalf("CreateCampaign: %v", err) } if camp.ID != 7 || camp.Status != "draft" { t.Errorf("unexpected campaign: %+v", camp) } if gotBody["content_type"] != "markdown" { t.Errorf("expected content_type=markdown, got %+v", gotBody) } } func TestSetCampaignStatus_SendsStatusToRunning(t *testing.T) { var gotPath string var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { gotPath = r.URL.Path b, _ := io.ReadAll(r.Body) json.Unmarshal(b, &gotBody) w.Write([]byte(`{}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") if err := c.SetCampaignStatus(7, "running"); err != nil { t.Fatalf("SetCampaignStatus: %v", err) } if gotPath != "/api/campaigns/7/status" { t.Errorf("expected /api/campaigns/7/status, got %q", gotPath) } if gotBody["status"] != "running" { t.Errorf("expected status=running, got %+v", gotBody) } } func TestTestCampaign_SendsSubscribersList(t *testing.T) { var gotBody map[string]any srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { b, _ := io.ReadAll(r.Body) json.Unmarshal(b, &gotBody) w.Write([]byte(`{}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") if err := c.TestCampaign(7, []string{"me@example.com"}); err != nil { t.Fatalf("TestCampaign: %v", err) } subs, ok := gotBody["subscribers"].([]any) if !ok || len(subs) != 1 || subs[0] != "me@example.com" { t.Errorf("expected subscribers=[me@example.com], got %+v", gotBody) } } func TestErrorResponsesAreWrappedWithStatusAndBody(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"message":"boom"}`)) })) defer srv.Close() c := New(srv.URL, "u", "t") _, err := c.CreateCampaign(CampaignInput{Name: "x"}) if err == nil || !strings.Contains(err.Error(), "boom") { t.Errorf("expected error to surface response body, got %v", err) } }