Skip to content
quarry Artifact [d485379533]
Login

Artifact [d485379533]

Artifact d48537953351bcd9d0853125edca13687bb5a9e6c38c577b9c2e7b0f45faa997:


package web

import (
	"html/template"
	"os"
	"path/filepath"
	"strings"
	"testing"
)

// With no uploaded image — the ordinary case, and the only case on a
// forge that has bound no storage — everything keeps the identicon
// derived from its name.
// "zit" and not "quarry": quarry's repository carries a built-in mark
// (see TestQuarrysRepositoryWearsTheLogo), so it is the one name that
// does NOT answer this question.
func TestWithoutAnUploadEverythingKeepsItsIdenticon(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })

	avatarTag = nil
	if got := repoIcon("zit"); got != identicon("zit") {
		t.Errorf("FAIL: no lookup at all did not fall back")
	}
	avatarTag = func(kind, name string) (string, bool) { return "", false }
	if got := repoIcon("zit"); got != identicon("zit") {
		t.Errorf("FAIL: a lookup that says no did not fall back")
	}
	// An empty validator is not an image either: it would produce a
	// URL that can never be invalidated.
	avatarTag = func(kind, name string) (string, bool) { return "", true }
	if got := repoIcon("zit"); got != identicon("zit") {
		t.Errorf("FAIL: an empty validator was drawn as an image")
	}
}

// The validator is in the URL, not only in a header. The image is
// served immutable, so a new upload has to arrive at a NEW address or
// a reader who already has the old one never asks again.
func TestAnUploadedMarkCarriesItsValidatorInTheURL(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })
	avatarTag = func(kind, name string) (string, bool) {
		return strings.Repeat("a", 64), true
	}
	got := string(repoIcon("quarry"))
	for _, want := range []string{
		`<img`, `src="/r/quarry/avatar?v=aaaaaaaaaaaa"`, `alt=""`, `loading="lazy"`,
	} {
		if !strings.Contains(got, want) {
			t.Errorf("FAIL: the mark is missing %q: %s", want, got)
		}
	}
	// A second upload is a second address.
	avatarTag = func(kind, name string) (string, bool) {
		return strings.Repeat("b", 64), true
	}
	if second := string(repoIcon("quarry")); second == got {
		t.Errorf("FAIL: a new image reuses the old address")
	}
}

// A team and a repository of one name are two marks at two addresses:
// the kind is half the key, and the template says which it means.
func TestATeamAndARepositoryDrawFromTheirOwnKind(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })
	avatarTag = func(kind, name string) (string, bool) {
		return kind + "-" + strings.Repeat("f", 20), true
	}
	repo, team := string(repoIcon("quarry")), string(teamIcon("quarry"))
	if !strings.Contains(repo, `src="/r/quarry/avatar`) {
		t.Errorf("FAIL: the repository's mark points at %s", repo)
	}
	if !strings.Contains(team, `src="/t/quarry/avatar`) {
		t.Errorf("FAIL: the team's mark points at %s", team)
	}
	if repo == team {
		t.Errorf("FAIL: one name gave one mark for two kinds")
	}
}

// With no storage bound — which is every forge until an operator binds
// one — the settings page says WHY there is no control rather than
// offering one that refuses.
func TestWithNoStorageTheSettingsPageExplainsItself(t *testing.T) {
	td := teamDetail{Team: teamRow{Name: "core", Title: "Core"}, CanManage: true, Sect: "settings"}
	render := func(off, has bool) string {
		var buf strings.Builder
		if err := teamSettingsPage.Execute(&buf, pageData{Title: "Core · Settings",
			OwnH1: true, Team: &td, TeamBar: td.bar(),
			MarksOff: off, HasMark: has}); err != nil {
			t.Fatal(err)
		}
		return buf.String()
	}
	off := render(true, false)
	if !strings.Contains(off, `data-hook="marks-off"`) {
		t.Errorf("FAIL: no explanation where there is nowhere to put an image")
	}
	if strings.Contains(off, `type="file"`) {
		t.Errorf("FAIL: an upload control that cannot work was drawn anyway")
	}
	if !strings.Contains(off, "/fleet/settings") {
		t.Errorf("FAIL: the explanation does not say where to fix it")
	}
	// With storage, the control — and no Remove until there is
	// something to remove.
	on := render(false, false)
	if !strings.Contains(on, `type="file"`) || !strings.Contains(on, `action="/t/core/avatar"`) {
		t.Errorf("FAIL: no upload control where one would work")
	}
	if strings.Contains(on, `name="clear"`) {
		t.Errorf("FAIL: a Remove was offered with nothing to remove")
	}
	if !strings.Contains(render(false, true), `name="clear"`) {
		t.Errorf("FAIL: no Remove where there is an image")
	}
}

// quarry's own repository wears quarry's logo rather than an identicon
// derived from its name — the project's repository looking like the
// software.
func TestQuarrysRepositoryWearsTheLogo(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })

	// With no lookup at all, and with one that says no: both are the
	// "nobody uploaded anything" case, and both must reach the mark.
	for _, tag := range []func(kind, name string) (string, bool){
		nil,
		func(kind, name string) (string, bool) { return "", false },
	} {
		avatarTag = tag
		got := string(repoIcon("quarry"))
		if !strings.Contains(got, "idn-mark") {
			t.Errorf("FAIL: quarry does not wear the logo: %s", got)
		}
		if got == string(identicon("quarry")) {
			t.Errorf("FAIL: quarry still draws its identicon")
		}
	}
	// Everything else is unchanged.
	avatarTag = nil
	if got := repoIcon("zit"); got != identicon("zit") {
		t.Errorf("FAIL: another repository lost its identicon")
	}
	// A TEAM called quarry is not the software: the built-in is a
	// repository's.
	if got := teamIcon("quarry"); got != identicon("quarry") {
		t.Errorf("FAIL: a team borrowed the repository's logo")
	}
	// And an uploaded mark still wins: the built-in is a default.
	avatarTag = func(kind, name string) (string, bool) { return strings.Repeat("c", 64), true }
	if got := string(repoIcon("quarry")); !strings.Contains(got, "<img") {
		t.Errorf("FAIL: an uploaded mark did not replace the built-in: %s", got)
	}
}

// Every surface that draws a named thing goes through the seam, so an
// uploaded mark appears there. The dashboard's team list did not — it
// called identicon directly, so a team that uploaded a picture kept
// its generated block on the one page its members open most.
func TestTheDashboardAndAccountMenuDrawUploadedMarks(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })
	avatarTag = func(kind, name string) (string, bool) {
		return kind + "aaaaaaaaaaaaaaaa", true
	}
	var page strings.Builder
	if err := dashboardPage.Execute(&page, pageData{Title: "Home", SignedIn: "mia",
		Dash: &dashData{Teams: []teamRow{{Name: "grove", Title: "Grove"}}}}); err != nil {
		t.Fatal(err)
	}
	got := page.String()
	// The team's own mark, at the team's own address.
	if !strings.Contains(got, `src="/t/grove/avatar`) {
		t.Errorf("FAIL: the dashboard draws no uploaded mark for a team")
	}
	// And the signed-in person's, at theirs.
	if !strings.Contains(got, `src="/people/mia/avatar`) {
		t.Errorf("FAIL: the account button draws no uploaded picture")
	}
}

// No page carries its own <style> element.
//
// The strict CSP (style-src 'self') drops one SILENTLY — the tag stays
// in the DOM, gets no sheet, and applies nothing — so a page that
// styles itself that way is a page whose layout is missing in
// production and present in every local render, which is the worst way
// to not find out. /me shipped like that: a two-column rail that was
// never two columns for any reader.
//
// Read from the package's OWN SOURCE rather than from a list of page
// templates, because a list is what missed this. /me was not on
// anybody's list, and a new page would not be on mine either. The rule
// is about what the source may contain, so the source is what is
// checked.
func TestNoPageCarriesItsOwnStyleElement(t *testing.T) {
	files, err := filepath.Glob("*.go")
	if err != nil {
		t.Fatal(err)
	}
	for _, name := range files {
		if strings.HasSuffix(name, "_test.go") {
			continue
		}
		raw, err := os.ReadFile(name)
		if err != nil {
			t.Fatal(err)
		}
		// A comment may name the thing it warns about — the sheet's own
		// header does, at length, including inside CSS block comments
		// that run for a dozen lines. Tracking whether we are inside
		// one is cheaper and more honest than a per-line guess.
		inComment := false
		for i, line := range strings.Split(string(raw), "\n") {
			trimmed := strings.TrimSpace(line)
			was := inComment
			if strings.Contains(line, "/*") && !strings.Contains(line, "*/") {
				inComment = true
			}
			if strings.Contains(line, "*/") {
				inComment = false
			}
			if !strings.Contains(line, "<style") {
				continue
			}
			if was || inComment || strings.HasPrefix(trimmed, "//") ||
				strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
				continue
			}
			t.Errorf("FAIL: %s:%d defines a <style> element — the CSP drops it; put the rules in the served sheet:\n\t%s",
				name, i+1, strings.TrimSpace(line))
		}
	}
}

// Every image editor offers a paste target, and every one ships it
// hidden.
//
// A phone has no paste shortcut: pasting is long-press then Paste, and
// that menu only appears over something editable — so a document-level
// paste listener never fires there, and the only way to paste on a
// phone is to have somewhere to paste INTO. Hidden in the markup
// because without script it is a box that does nothing.
func TestEveryImageEditorOffersAPasteTarget(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })
	avatarTag = func(kind, name string) (string, bool) { return "", false }

	td := teamDetail{Team: teamRow{Name: "core", Title: "Core"}, CanManage: true, Sect: "settings"}
	pages := map[string]func() (string, error){
		"team settings": func() (string, error) {
			var b strings.Builder
			err := teamSettingsPage.Execute(&b, teamSettingsData(&td, false, nil))
			return b.String(), err
		},
		"repository settings": func() (string, error) {
			var b strings.Builder
			err := settingsPage.ExecuteTemplate(&b, "content", pageData{Repo: "quarry"})
			return b.String(), err
		},
		"your account": func() (string, error) {
			var b strings.Builder
			err := mePageTmpl.ExecuteTemplate(&b, "content", pageData{
				Me: &meView{Name: "mia", Notify: &prefsView{}}})
			return b.String(), err
		},
	}
	for name, render := range pages {
		got, err := render()
		if err != nil {
			t.Fatalf("%s: %v", name, err)
		}
		if !strings.Contains(got, `data-hook="avatar-paste"`) {
			t.Errorf("FAIL: the %s editor offers no paste target — a phone cannot paste without one", name)
			continue
		}
		if !strings.Contains(got, `data-hook="avatar-paste" hidden`) {
			t.Errorf("FAIL: the %s paste target is not hidden in the markup", name)
		}
	}
}

// A person with a picture is drawn with it wherever they appear beside
// their name — not only on their own account page.
func TestAPersonsPictureShowsBesideTheirName(t *testing.T) {
	old := avatarTag
	t.Cleanup(func() { avatarTag = old })
	avatarTag = func(kind, name string) (string, bool) {
		if kind == "user" && name == "dan" {
			return strings.Repeat("c", 64), true
		}
		return "", false
	}
	want := `src="/people/dan/avatar?v=cccccccccccc"`
	whoOf := func(user string) string {
		tpl := template.Must(template.Must(base.Clone()).Parse(`{{define "t"}}{{who "quarry" .}}{{end}}`))
		var b strings.Builder
		if err := tpl.ExecuteTemplate(&b, "t", user); err != nil {
			t.Fatal(err)
		}
		return b.String()
	}
	who := whoOf("dan")
	for name, got := range map[string]string{
		"who":        who,
		"chat here":  string(chatHereHTML([]string{"dan"})),
		"chat quote": string(chatQuoteBlock(1, "dan", "hello", "quarry")),
	} {
		if !strings.Contains(got, want) {
			t.Errorf("FAIL: %s draws dan without his picture: %s", name, got)
		}
	}
	if strings.Contains(whoOf("eve"), "<img") {
		t.Error("FAIL: someone with no picture got an image")
	}
}