b16d2b6c5f
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.
154 lines
3.8 KiB
Go
154 lines
3.8 KiB
Go
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")
|
|
}
|
|
}
|