Initial implementation of the eec-campaigns tool

Drives listmonk's real Campaign API from git-authored Markdown+frontmatter,
so broadcast/segment emails get listmonk's mature unsubscribe/bulk-send/
attachment handling instead of reimplementing it. sync only ever creates or
updates a draft (idempotent, diff-based, refuses to touch a non-draft
campaign); a pushed send/<slug> tag or manual workflow run is the only way
to actually trigger a send. Includes list-name resolution, segment_query
materialization into managed lists, content-hash-deduped attachment
uploads, and an automatic post-sync preview email.
This commit is contained in:
2026-07-10 07:30:45 -04:00
commit b16d2b6c5f
16 changed files with 2461 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
package campaign
import (
"os"
"path/filepath"
"strings"
"testing"
)
func writeFile(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
t.Fatal(err)
}
}
func TestParseFile_ParsesFrontmatterAndBody(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
subject: "Big Announcement"
lists: ["Newsletter"]
from_email: hello@reground.org
tags: ["announcement"]
attachments:
- assets/one-pager.pdf
---
Hello **world**.
`)
fm, body, err := ParseFile(path)
if err != nil {
t.Fatalf("ParseFile: %v", err)
}
if fm.Subject != "Big Announcement" {
t.Errorf("unexpected subject: %q", fm.Subject)
}
if len(fm.Lists) != 1 || fm.Lists[0] != "Newsletter" {
t.Errorf("unexpected lists: %v", fm.Lists)
}
if fm.Type != "regular" {
t.Errorf("expected type to default to regular, got %q", fm.Type)
}
if !strings.Contains(body, "Hello **world**.") {
t.Errorf("unexpected body: %q", body)
}
}
func TestParseFile_MissingFrontmatterDelimiter(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, "no frontmatter here\n")
if _, _, err := ParseFile(path); err == nil {
t.Fatal("expected an error for a file with no frontmatter block")
}
}
func TestParseFile_UnterminatedFrontmatter(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, "---\nsubject: hi\nbody with no closing delimiter")
if _, _, err := ParseFile(path); err == nil {
t.Fatal("expected an error for unterminated frontmatter")
}
}
func TestParseFile_MissingSubject(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
lists: ["Newsletter"]
from_email: hello@reground.org
---
body
`)
if _, _, err := ParseFile(path); err == nil || !strings.Contains(err.Error(), "subject") {
t.Fatalf("expected a 'missing subject' error, got %v", err)
}
}
func TestParseFile_MissingFromEmail(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
subject: hi
lists: ["Newsletter"]
---
body
`)
if _, _, err := ParseFile(path); err == nil || !strings.Contains(err.Error(), "from_email") {
t.Fatalf("expected a 'missing from_email' error, got %v", err)
}
}
func TestParseFile_RequiresListsOrSegmentQuery(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
subject: hi
from_email: hello@reground.org
---
body
`)
if _, _, err := ParseFile(path); err == nil || !strings.Contains(err.Error(), "target audience") {
t.Fatalf("expected a 'needs a target audience' error, got %v", err)
}
}
func TestParseFile_SegmentQueryAloneSatisfiesTargetAudience(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
subject: hi
from_email: hello@reground.org
segment_query: "subscribers.attribs->>'source' = 'workshop'"
---
body
`)
if _, _, err := ParseFile(path); err != nil {
t.Fatalf("expected segment_query alone to be sufficient, got %v", err)
}
}
func TestParseFile_RejectsOptin(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, `---
subject: hi
lists: ["Newsletter"]
from_email: hello@reground.org
type: optin
---
body
`)
if _, _, err := ParseFile(path); err == nil || !strings.Contains(err.Error(), "optin") {
t.Fatalf("expected an 'optin not supported' error, got %v", err)
}
}
func TestParseFile_MalformedYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "campaign.md")
writeFile(t, path, "---\nsubject: [unterminated\n---\nbody\n")
if _, _, err := ParseFile(path); err == nil {
t.Fatal("expected an error for malformed YAML frontmatter")
}
}