Skip to content
quarry Artifact [19ffbca8cf]
Login

Artifact [19ffbca8cf]

Artifact 19ffbca8cf3a2bc46fd4bbc1604fbe44a1ac6317d8b5375c45b972d6c1c701bb:


package markup

import (
	"regexp"
	"strings"
	"testing"
)

const emailSample = "> quoted *line*\n\n" +
	"| a | b |\n|---|--:|\n| 1 | 2 |\n\n" +
	"```go\nfmt.Println(1)\n```\n\n" +
	"![shot](https://cdn.example/x.png) and [rel](/r/alpha/wiki/Home) and [top](#top)\n\n" +
	"> [!WARNING]\n> careful\n"

func mailOf(t *testing.T, src string) string {
	t.Helper()
	got, ok := EmailHTML([]byte(src), nil, func(p string) string { return "https://forge.example" + p })
	if !ok {
		t.Fatal("EmailHTML fell back")
	}
	return string(got)
}

func TestEmailHTMLIsSelfContained(t *testing.T) {
	got := mailOf(t, emailSample)
	for _, banned := range []string{"<style", "<img", "<svg", "<script", "md-"} {
		if strings.Contains(got, banned) {
			t.Errorf("html carries %q", banned)
		}
	}
	for _, m := range regexp.MustCompile(`href="([^"]*)"`).FindAllStringSubmatch(got, -1) {
		if !strings.HasPrefix(m[1], "https://") {
			t.Errorf("non-absolute href %q", m[1])
		}
	}
	for _, want := range []string{
		`href="https://cdn.example/x.png"`,
		`href="https://forge.example/r/alpha/wiki/Home"`,
	} {
		if !strings.Contains(got, want) {
			t.Errorf("missing %s", want)
		}
	}
	for _, tag := range []string{"<blockquote style=", "<table style=", "<th style=", "<td style=", "<pre style=", "<code style=", "<p style=", "<a style="} {
		if !regexp.MustCompile(strings.Replace(tag, " style=", `[^>]* style=`, 1)).MatchString(got) {
			t.Errorf("no inline style on %s", tag)
		}
	}
}

func TestEmailHTMLFallsBackOverTheCap(t *testing.T) {
	big := strings.Repeat("word ", EmailHTMLMax/4)
	if _, ok := EmailHTML([]byte(big), nil, func(p string) string { return p }); ok {
		t.Fatal("oversized html was kept")
	}
}

func TestEmailHTMLKeepsTypographicPunctuation(t *testing.T) {
	const want = "\u201cseen, empty\u201d \u2018x\u2019 a \u2014 b \u2013 c \u2026 \u2192"
	got := mailOf(t, "record "+want+" now")
	if !strings.Contains(got, want) {
		t.Errorf("punctuation changed: %s", got)
	}
}

type mailRefs struct{ ticketResolver }

func (mailRefs) ResolveToken(t RefToken) (string, bool) {
	if t.Kind == RefLanding && t.ID == "12" {
		return "/r/q/landing/a1b2c3d4e5f6", true
	}
	return "", false
}

func (mailRefs) DescribeLanding(t RefToken) (LandingRef, bool) {
	if t.Kind == RefLanding && t.ID == "12" {
		return LandingRef{Number: 12, State: "accepted"}, true
	}
	return LandingRef{}, false
}

// References in mail resolve like the page's and carry their state as a
// small PNG and its label: no SVG (mail drops it), no hover card, no
// hook attributes, every address absolute.
func TestEmailHTMLRendersReferencesLikeThePage(t *testing.T) {
	src := "landed !12, fixed 54dc6a4148, also 508fd44ab0, see [[Design]], unknown !13"
	got, ok := EmailHTML([]byte(src), mailRefs{}, func(p string) string { return "https://forge.example" + p })
	if !ok {
		t.Fatal("fell back")
	}
	h := string(got)
	for _, want := range []string{
		`href="https://forge.example/r/q/landing/a1b2c3d4e5f6"`,
		`href="https://forge.example/r/q/ticket/54dc6a414800aa"`,
		`href="https://forge.example/r/q/ticket/508fd44ab000bb"`,
		`href="https://forge.example/wiki/Design"`,
		`src="https://forge.example/icons/landing-accepted.png" alt="landing 12, accepted"`,
		`src="https://forge.example/icons/ticket-closed.png" alt="ticket 231, closed"`,
		`src="https://forge.example/icons/ticket-open.png" alt="ticket 232, open"`,
		`>12</a>`, `>231</a>`, `>232</a>`, "unknown !13",
	} {
		if !strings.Contains(h, want) {
			t.Errorf("missing %s in %s", want, h)
		}
	}
	for _, banned := range []string{"<svg", "data-ref", "data-ic", "aria-label", "tref-state", ">!12<"} {
		if strings.Contains(h, banned) {
			t.Errorf("carries %q", banned)
		}
	}
	if regexp.MustCompile(`<a [^>]*title=`).MatchString(h) {
		t.Error("a reference keeps its hover title")
	}
	if strings.Count(h, "<img") != 3 {
		t.Errorf("%d images, want one per resolved icon reference", strings.Count(h, "<img"))
	}
}

type stateRefs struct {
	fakeResolver
	ticket string
	lstate string
}

func (s stateRefs) ResolveToken(t RefToken) (string, bool) {
	return "/r/q/" + t.Kind + "/x", t.Kind == RefLanding || t.Kind == RefTicket
}

func (s stateRefs) DescribeTicket(t RefToken) (TicketRef, bool) {
	return TicketRef{ID: "x", URL: "/r/q/ticket/x", Number: 7, State: s.ticket}, true
}

func (s stateRefs) DescribeLanding(t RefToken) (LandingRef, bool) {
	return LandingRef{Number: 9, State: s.lstate}, true
}

// Every state is its own icon and label, on the page and in the mail,
// and none leaves a state word beside the link.
func TestEveryRefStateHasItsIconOnThePageAndInMail(t *testing.T) {
	type row struct{ src, key, label, symbol, tone string }
	var rows []row
	for state, symbol := range map[string]string{"open": "i-tkt-open", "review": "i-tkt-open", "closed": "i-tkt-done"} {
		rows = append(rows, row{"ticket #7", "ticket-" + state, "ticket 7, " + state, symbol, ""})
	}
	// Landings wear the landing glyph in the state's own tone, so they differ
	// from tickets (circles) and from each other.
	for state := range map[string]string{"open": "", "accepted": "", "rejected": ""} {
		symbol := "i-landing"
		rows = append(rows, row{"!9", "landing-" + state, "landing 9, " + state, symbol, ""})
		tone := map[string]string{"open": "g-open", "accepted": "g-merged", "rejected": "g-rejected"}[state]
		rows[len(rows)-1].tone = tone
	}
	for _, r := range rows {
		res := stateRefs{ticket: strings.TrimPrefix(strings.Split(r.key, "-")[1], ""), lstate: strings.Split(r.key, "-")[1]}
		page, _ := Render([]byte(r.src), res)
		for _, want := range []string{`data-ic="` + r.key + `"`, `aria-label="` + r.label + `"`, `href="#` + r.symbol + `"`} {
			if !strings.Contains(string(page), want) {
				t.Errorf("%s page lacks %s: %s", r.key, want, page)
			}
		}
		mail, _ := EmailHTML([]byte(r.src), res, func(p string) string { return "https://f.example" + p })
		if !strings.Contains(string(mail), `src="https://f.example/icons/`+r.key+`.png" alt="`+r.label+`"`) {
			t.Errorf("%s mail lacks its icon: %s", r.key, mail)
		}
		if r.tone != "" && !strings.Contains(string(page), r.tone) {
			t.Errorf("%s page lacks its tone %s", r.key, r.tone)
		}
		for _, word := range []string{"Open</span>", "Fixed</span>", "tref-state"} {
			if strings.Contains(string(page), word) || strings.Contains(string(mail), word) {
				t.Errorf("%s: state text %q remains", r.key, word)
			}
		}
	}
}