Add template-as-code sync (campaigns template NAME PATH)

Answers "can the unsubscribe template be added via git+API too": commit
listmonk's own stock campaign template (already unsubscribe-capable) as
email-templates/campaign.html, add a small find-or-create-or-update
Template client (confirmed against knadh/listmonk's actual model/handlers),
a new `template` subcommand, and a manual-dispatch-only workflow to push it
in as the default campaign template. Unlike campaign sync there's no
draft/live status to protect, so this is always a safe overwrite.
This commit is contained in:
2026-07-10 08:54:31 -04:00
parent bcf8df5579
commit a630a8f8ab
9 changed files with 476 additions and 6 deletions
+77
View File
@@ -333,3 +333,80 @@ func TestErrorResponsesAreWrappedWithStatusAndBody(t *testing.T) {
t.Errorf("expected error to surface response body, got %v", err)
}
}
func TestFindTemplateByName(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"data":[{"id":4,"name":"eec-campaigns default","type":"campaign","is_default":true}]}`))
}))
defer srv.Close()
c := New(srv.URL, "u", "t")
tpl, err := c.FindTemplateByName("eec-campaigns default")
if err != nil {
t.Fatalf("FindTemplateByName: %v", err)
}
if tpl == nil || tpl.ID != 4 || !tpl.IsDefault {
t.Fatalf("unexpected template: %+v", tpl)
}
}
func TestFindTemplateByName_NilOnMiss(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"data":[]}`))
}))
defer srv.Close()
c := New(srv.URL, "u", "t")
tpl, err := c.FindTemplateByName("missing")
if err != nil {
t.Fatalf("FindTemplateByName: %v", err)
}
if tpl != nil {
t.Errorf("expected nil for a miss, got %+v", tpl)
}
}
func TestCreateTemplate_SendsCampaignTypeAndIsDefault(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":9}}`))
}))
defer srv.Close()
c := New(srv.URL, "u", "t")
tpl, err := c.CreateTemplate("eec-campaigns default", "<html></html>", true)
if err != nil {
t.Fatalf("CreateTemplate: %v", err)
}
if tpl.ID != 9 {
t.Errorf("expected id 9, got %d", tpl.ID)
}
if gotBody["type"] != "campaign" || gotBody["is_default"] != true {
t.Errorf("expected type=campaign, is_default=true, got %+v", gotBody)
}
}
func TestUpdateTemplate_SendsExpectedPayload(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.UpdateTemplate(9, "eec-campaigns default", "<html>v2</html>", true); err != nil {
t.Fatalf("UpdateTemplate: %v", err)
}
if gotPath != "/api/templates/9" {
t.Errorf("expected /api/templates/9, got %q", gotPath)
}
if gotBody["body"] != "<html>v2</html>" {
t.Errorf("expected updated body to be sent, got %+v", gotBody)
}
}