Files
warmbly/internal/tasks/preview_test.go
T
Matthew MeszarosandDevin AI d807aa2852 feat: expose typed sender mailbox details in campaign templates, previews and email editor variables
Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-10-05 15:15:12 +00:00

116 lines
5.1 KiB
Go

package tasks
import (
"context"
"strings"
"testing"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
)
func TestPreviewEmailUsesSelectedSender(t *testing.T) {
service := &tasksService{}
for _, account := range []*models.Email{
{Name: "Tareque M.", Email: "tareque@example.com", SendAsEmail: "hello@example.com"},
{Name: "John S.", Email: "john@example.com"},
} {
preview := service.PreviewEmail(context.Background(), uuid.Nil, EmailPreviewInput{
Subject: "From {{.Sender.Name}}", BodyHTML: "<p>{{.FirstName}}: {{.Sender.Email}}</p>",
BodyPlain: "{{.Sender.Provider}}|{{.Sender.Email}}", Contact: models.Contact{FirstName: "Alex"}, Account: account,
})
if preview.From == nil || preview.From.Email != account.SendFrom() || preview.Subject != "From "+account.Name || preview.BodyHTML != "<p>Alex: "+account.SendFrom()+"</p>" || preview.BodyPlain != "|"+account.SendFrom() {
t.Fatalf("preview does not match selected sender: %+v", preview)
}
if len(preview.Errors) != 0 || len(preview.Unresolved) != 0 {
t.Fatalf("sender variables left errors: %+v", preview)
}
}
preview := service.PreviewEmail(context.Background(), uuid.Nil, EmailPreviewInput{Subject: "{{.Sender.Email}}"})
if preview.Subject != "" || preview.From != nil {
t.Fatalf("preview without a mailbox invented a sender: %+v", preview)
}
}
// finishBody is what the preview and the test send share with the campaign
// send: the parts land in send order (body, signature, opt-out) and a
// plain-text campaign loses its HTML part before the signature is added.
func TestFinishBodyMatchesSendOrder(t *testing.T) {
account := &models.Email{SignatureSync: true, SignatureHTML: "<p>Ana</p>", SignaturePlain: "Ana"}
optOut := &models.UnsubscribeSettings{Mode: models.UnsubscribeModeText, Text: "Reply stop to opt out."}
h, p := finishBody("<p>Hi</p>", "", false, account, optOut, "")
sig, foot := strings.Index(h, "<p>Ana</p>"), strings.Index(h, "Reply stop to opt out.")
if sig < 0 || foot < 0 || sig > foot {
t.Fatalf("html parts out of send order: %q", h)
}
if !strings.HasPrefix(p, "Hi") || strings.Index(p, "Ana") > strings.Index(p, "Reply stop") {
t.Fatalf("plain part not derived from html or out of order: %q", p)
}
h, p = finishBody("<p>Hi</p>", "", true, account, optOut, "")
if h != "" {
t.Fatalf("plain-text campaign kept an html part: %q", h)
}
if !strings.Contains(p, "Ana") || !strings.Contains(p, "Reply stop") {
t.Fatalf("plain-text campaign lost signature or footer: %q", p)
}
// No mailbox and no campaign: templates only, nothing appended.
h, p = finishBody("<p>Hi</p>", "Hi", false, nil, nil, "")
if h != "<p>Hi</p>" || p != "Hi" {
t.Fatalf("bare preview was decorated: %q / %q", h, p)
}
// Signature sync off leaves the body alone even with a signature stored.
off := &models.Email{SignatureSync: false, SignatureHTML: "<p>Ana</p>"}
if h, _ = finishBody("<p>Hi</p>", "Hi", false, off, nil, ""); h != "<p>Hi</p>" {
t.Fatalf("signature added while sync is off: %q", h)
}
}
func TestPreviewTemplatesWithUsesTheGivenLink(t *testing.T) {
p := previewTemplatesWith("s", "<a href=\"{{.UnsubscribeLink}}\">x</a>", "", models.Contact{}, TemplateContext{UnsubscribeLink: "https://api.example.com/unsubscribe/tok"})
if !strings.Contains(p.BodyHTML, "https://api.example.com/unsubscribe/tok") {
t.Fatalf("link variable did not resolve to the given link: %q", p.BodyHTML)
}
if q := PreviewTemplates("s", "{{.UnsubscribeLink}}", "", models.Contact{}); q.BodyHTML != PreviewUnsubscribeLink {
t.Fatalf("default preview link changed: %q", q.BodyHTML)
}
}
// A step created through the API or an agent tool carries the composer's empty
// <div></div> under body_html. It is not an empty string, so it shipped as the
// text/html alternative and the recipient opened a blank message with the real
// copy only in the fallback part.
func TestFinishBodyDropsATextlessHTMLPart(t *testing.T) {
h, p := finishBody("<div></div>", "Hi Ana, quick question.", false, nil, nil, "")
if h != "" {
t.Errorf("HTML part = %q, want it dropped so the plain part is what ships", h)
}
if p != "Hi Ana, quick question." {
t.Errorf("plain part = %q, want the step's own text", p)
}
}
// The guard must not touch a body that renders something, including one whose
// only content is an image.
func TestFinishBodyKeepsABodyThatRenders(t *testing.T) {
if h, _ := finishBody("<div>Hi</div>", "Hi", false, nil, nil, ""); h != "<div>Hi</div>" {
t.Errorf("HTML part = %q, want it kept", h)
}
img := `<div><img src="https://example.com/a.png"></div>`
if h, _ := finishBody(img, "see the image", false, nil, nil, ""); h == "" {
t.Error("an image-only body was dropped; it renders for the recipient")
}
}
// With no plain part either there is nothing to fall back to, so dropping the
// HTML would send an empty message. The start-time guard is what stops that
// campaign existing; here the body is left alone.
func TestFinishBodyKeepsAnEmptyHTMLPartWhenThereIsNoPlainText(t *testing.T) {
if h, _ := finishBody("<div></div>", "", false, nil, nil, ""); h == "" {
t.Error("dropped the only part the message had")
}
}