Files
warmbly/internal/tasks/tracking_links_test.go
T

195 lines
8.6 KiB
Go

package tasks
import (
"strings"
"testing"
"github.com/google/uuid"
"github.com/warmbly/warmbly/internal/models"
)
func TestAddOpenTrackingPixelUsesTheConfiguredHost(t *testing.T) {
html := "<html><body>hi</body></html>"
out := AddOpenTrackingPixel(html, uuid.New(), "t.acme.com")
if !strings.Contains(out, `src="https://t.acme.com/t/o/`) {
t.Fatalf("pixel did not use the host: %s", out)
}
}
// A local tracking service does not serve TLS, so an https pixel there is one
// no mail client can load.
func TestAddOpenTrackingPixelUsesHTTPForLocalHosts(t *testing.T) {
out := AddOpenTrackingPixel("<html><body>hi</body></html>", uuid.New(), "localhost:3000")
if !strings.Contains(out, `src="http://localhost:3000/t/o/`) {
t.Fatalf("expected an http pixel, got: %s", out)
}
}
// Without a tracking host there is nowhere to record an open. The pixel used to
// fall back to a hardcoded warmbly.com name, which on any other install meant
// mailing recipients a beacon to a third party that recorded nothing locally.
func TestAddOpenTrackingPixelSkippedWithoutAHost(t *testing.T) {
html := "<html><body>hi</body></html>"
if out := AddOpenTrackingPixel(html, uuid.New(), ""); out != html {
t.Fatalf("expected the body untouched, got: %s", out)
}
}
func TestWrapLinksForTrackingMintsTickets(t *testing.T) {
html := `<a href="https://example.com/pricing">pricing</a>`
out, links := WrapLinksForTracking(html, uuid.New(), uuid.New(), "t.acme.com")
if len(links) != 1 {
t.Fatalf("expected one ticket, got %d", len(links))
}
if links[0].Destination != "https://example.com/pricing" {
t.Fatalf("destination not preserved: %q", links[0].Destination)
}
if !strings.Contains(out, "https://t.acme.com/c/"+links[0].ID.String()) {
t.Fatalf("link not rewritten to the ticket: %s", out)
}
}
// The worst outcome of a missing tracking host: every link in a real campaign
// email rewritten to a host that does not exist. Ship the originals instead.
func TestWrapLinksForTrackingLeavesLinksAloneWithoutAHost(t *testing.T) {
html := `<a href="https://example.com/pricing">pricing</a>`
out, links := WrapLinksForTracking(html, uuid.New(), uuid.New(), "")
if out != html || links != nil {
t.Fatalf("expected the original links, got %s / %v", out, links)
}
}
func TestWrapLinksForTrackingSkipsItsOwnHostAndNonHTTP(t *testing.T) {
html := `<a href="https://t.acme.com/c/abc">x</a><a href="mailto:a@b.com">m</a><a href="#top">t</a>`
out, links := WrapLinksForTracking(html, uuid.New(), uuid.New(), "t.acme.com")
if len(links) != 0 {
t.Fatalf("expected no tickets, got %d", len(links))
}
if out != html {
t.Fatalf("body should be unchanged: %s", out)
}
}
func TestTrackLinksReadsTheAnchorTextAsTheLabel(t *testing.T) {
html := `<p>See <a href="https://example.com/pricing"><b>our</b> Pricing &amp; plans</a> or <a href="https://example.com/x"><img src="cid:logo" alt="Logo"></a></p>`
_, links := TrackLinks(html, LinkTracking{TaskID: uuid.New(), CampaignID: uuid.New(), TrackingDomain: "t.acme.com", Wrap: true})
if len(links) != 2 {
t.Fatalf("expected two tickets, got %d", len(links))
}
if links[0].Label != "our Pricing & plans" {
t.Fatalf("label not read from the anchor text: %q", links[0].Label)
}
if links[1].Label != "Logo" {
t.Fatalf("image link should fall back to alt text: %q", links[1].Label)
}
}
func TestTrackLinksTagsUTMAndStoresTheTaggedDestination(t *testing.T) {
html := `<a href="https://example.com/pricing?ref=1#top">Pricing page</a>`
utm := &UTMParams{Source: "warmbly", Medium: "email", Campaign: "q3_outbound"}
out, links := TrackLinks(html, LinkTracking{TaskID: uuid.New(), CampaignID: uuid.New(), TrackingDomain: "t.acme.com", Wrap: true, UTM: utm})
if len(links) != 1 {
t.Fatalf("expected one ticket, got %d", len(links))
}
want := "https://example.com/pricing?ref=1&utm_source=warmbly&utm_medium=email&utm_campaign=q3_outbound&utm_content=pricing_page#top"
if links[0].Destination != want {
t.Fatalf("destination = %q, want %q", links[0].Destination, want)
}
if strings.Contains(out, "utm_") {
t.Fatalf("the email must carry only the ticket, got: %s", out)
}
}
func TestTrackLinksTagsUTMWithoutWrappingWhenLinkTrackingIsOff(t *testing.T) {
html := `<a href='https://example.com/a?x=1&amp;y=2'>Go</a>`
out, links := TrackLinks(html, LinkTracking{UTM: &UTMParams{Source: "s", Medium: "m", Campaign: "c"}})
if links != nil {
t.Fatalf("no tickets expected, got %v", links)
}
want := `<a href="https://example.com/a?x=1&amp;y=2&amp;utm_source=s&amp;utm_medium=m&amp;utm_campaign=c&amp;utm_content=go">Go</a>`
if out != want {
t.Fatalf("got %s\nwant %s", out, want)
}
}
func TestTrackLinksKeepsHandWrittenUTMValues(t *testing.T) {
html := `<a href="https://example.com/?utm_source=newsletter&utm_content=hero">Hero</a>`
_, links := TrackLinks(html, LinkTracking{TaskID: uuid.New(), CampaignID: uuid.New(), TrackingDomain: "t.acme.com", Wrap: true, UTM: &UTMParams{Source: "warmbly", Medium: "email", Campaign: "c"}})
got := links[0].Destination
if strings.Count(got, "utm_source=") != 1 || !strings.Contains(got, "utm_source=newsletter") || !strings.Contains(got, "utm_content=hero") {
t.Fatalf("hand-written values must win: %s", got)
}
if !strings.Contains(got, "utm_medium=email") || !strings.Contains(got, "utm_campaign=c") {
t.Fatalf("missing values must still be added: %s", got)
}
}
func TestTrackLinksNumbersLinksWithoutText(t *testing.T) {
html := `<a href="https://example.com/1"><img src="x"></a><a href="https://example.com/2"></a>`
_, links := TrackLinks(html, LinkTracking{TaskID: uuid.New(), CampaignID: uuid.New(), TrackingDomain: "t.acme.com", Wrap: true, UTM: &UTMParams{Source: "s", Medium: "m", Campaign: "c"}})
if !strings.HasSuffix(links[0].Destination, "utm_content=link_1") || !strings.HasSuffix(links[1].Destination, "utm_content=link_2") {
t.Fatalf("unlabelled links should be numbered: %s / %s", links[0].Destination, links[1].Destination)
}
}
// A stylesheet or preload href in the head is not a link anyone clicks;
// redirecting it through a ticket would break it.
func TestTrackLinksOnlyTouchesAnchors(t *testing.T) {
html := `<link rel="stylesheet" href="https://example.com/a.css"><a href="https://example.com/p">p</a>`
out, links := WrapLinksForTracking(html, uuid.New(), uuid.New(), "t.acme.com")
if len(links) != 1 || links[0].Destination != "https://example.com/p" {
t.Fatalf("expected only the anchor wrapped: %v", links)
}
if !strings.Contains(out, `href="https://example.com/a.css"`) {
t.Fatalf("stylesheet href must be untouched: %s", out)
}
}
func TestCampaignUTMDefaults(t *testing.T) {
c := &models.Campaign{Name: "Q3 Outbound: Fintech!", UTMTracking: true}
p := CampaignUTM(c)
if p == nil || p.Source != "warmbly" || p.Medium != "email" || p.Campaign != "q3_outbound_fintech" {
t.Fatalf("unexpected defaults: %+v", p)
}
c.UTMSource, c.UTMCampaign = " acme ", "launch"
p = CampaignUTM(c)
if p.Source != "acme" || p.Campaign != "launch" {
t.Fatalf("overrides not honoured: %+v", p)
}
if CampaignUTM(&models.Campaign{Name: "x"}) != nil {
t.Fatal("utm tagging off must yield nil")
}
}
func TestTrackLinksReadsBareHrefAndIgnoresDataHref(t *testing.T) {
html := `<a data-href="https://tracker.example/x" href=https://example.com/bare target="_blank">Bare</a>`
out, links := WrapLinksForTracking(html, uuid.New(), uuid.New(), "t.acme.com")
if len(links) != 1 || links[0].Destination != "https://example.com/bare" || links[0].Label != "Bare" {
t.Fatalf("expected the bare href wrapped: %+v", links)
}
if !strings.Contains(out, `data-href="https://tracker.example/x"`) || !strings.Contains(out, `target="_blank"`) {
t.Fatalf("other attributes must survive: %s", out)
}
}
func TestTrackableURLComparesTheHost(t *testing.T) {
if trackableURL("https://t.acme.com/c/abc", "t.acme.com") {
t.Fatal("a link already on the tracking host must be left alone")
}
if !trackableURL("https://example.com/?next=t.acme.com", "t.acme.com") {
t.Fatal("a URL merely mentioning the host is a normal link")
}
if !trackableURL("https://not-t.acme.com/", "t.acme.com") {
t.Fatal("a different host that ends with the tracking host is a normal link")
}
}
func TestTagPlainTextLinks(t *testing.T) {
body := "See https://example.com/pricing. Docs: https://example.com/docs?x=1\nSkip https://t.acme.com/c/abc"
out := TagPlainTextLinks(body, &UTMParams{Source: "s", Medium: "m", Campaign: "c"}, "t.acme.com")
want := "See https://example.com/pricing?utm_source=s&utm_medium=m&utm_campaign=c&utm_content=link_1. Docs: https://example.com/docs?x=1&utm_source=s&utm_medium=m&utm_campaign=c&utm_content=link_2\nSkip https://t.acme.com/c/abc"
if out != want {
t.Fatalf("got %s\nwant %s", out, want)
}
}