Skip to content
quarry Artifact [73c1285ca5]
Login

Artifact [73c1285ca5]

Artifact 73c1285ca514fb8f9599de0dee4120fe8afe42111ec3a00152382623e3582ec8:


package web

import (
	"regexp"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/oltp"
)

// The tab comment that held the strip at two set its own acceptance
// test: "a tab that opens onto an empty page teaches a reader the forge
// is broken rather than that the feature is unbuilt." So the Teams tab
// appears only when the forge has one.
func TestTheTeamsTabAppearsOnlyOnceATeamExists(t *testing.T) {
	var none, some strings.Builder
	if err := peoplePage.ExecuteTemplate(&none, "content",
		pageData{Explore: "people", FleetTotal: 20, PeopleTotal: 3}); err != nil {
		t.Fatal(err)
	}
	if strings.Contains(none.String(), `href="/teams"`) {
		t.Error("FAIL: a forge with no teams draws the Teams tab")
	}
	if err := peoplePage.ExecuteTemplate(&some, "content",
		pageData{Explore: "people", FleetTotal: 20, PeopleTotal: 3, TeamsTotal: 2}); err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(some.String(), `href="/teams"`) {
		t.Error("FAIL: a forge with teams does not draw the Teams tab")
	}
	// And the other two are still there, in order.
	body := some.String()
	repos, people, teams := strings.Index(body, `href="/?view=explore"`),
		strings.Index(body, `href="/people"`), strings.Index(body, `href="/teams"`)
	if !(repos < people && people < teams) {
		t.Errorf("FAIL: tab order is repos %d, people %d, teams %d", repos, people, teams)
	}
}

// A team is shown by its title where it has one, and never by both —
// "Craft Design Group (craft-design-group)" is the same fact twice and
// the handle is already in the URL.
func TestATeamIsLabelledOnceNotTwice(t *testing.T) {
	named := teamRow{Name: "craft-design-group", Title: "Craft Design Group"}
	if got := named.Label(); got != "Craft Design Group" {
		t.Errorf("FAIL: label = %q", got)
	}
	if !named.Named() {
		t.Error("FAIL: a titled team does not read as named")
	}
	bare := teamRow{Name: "core"}
	if got := bare.Label(); got != "core" {
		t.Errorf("FAIL: an untitled team labels as %q", got)
	}
	if bare.Named() {
		t.Error("FAIL: an untitled team would print its handle twice")
	}
	// A title that merely repeats the handle is not a second fact.
	if (teamRow{Name: "core", Title: "core"}).Named() {
		t.Error("FAIL: a title equal to the handle reads as named")
	}
}

// The directory says where a team's substance is, and a team with no
// repositories is a real row rather than one that reads as broken.
func TestTheDirectoryNamesAnEmptyTeamHonestly(t *testing.T) {
	var buf strings.Builder
	if err := teamsPage.ExecuteTemplate(&buf, "content", pageData{
		Explore: "teams", TeamsTotal: 2,
		Teams: []teamRow{
			{Name: "craft-design-group", Title: "Craft Design Group",
				Descr: "quarry and zit", Members: 1, Repos: 0},
			{Name: "firestorm-dataworks", Title: "FireStorm DataWorks", Members: 4, Repos: 3},
		},
	}); err != nil {
		t.Fatal(err)
	}
	body := buf.String()
	for _, want := range []string{
		`href="/t/craft-design-group"`, "Craft Design Group", "quarry and zit",
		"no repositories yet", "3 repositories", "1 member", "4 members",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: the directory is missing %q", want)
		}
	}
	// A count of zero must not render as a bare "0 repositories".
	if strings.Contains(body, "0 repositor") {
		t.Error("FAIL: an empty team reads as a zero rather than as a sentence")
	}
}

// An empty forge says what a team is for, rather than showing an empty
// list — the same rule the tab comment states.
func TestAnEmptyDirectorySaysWhatATeamIs(t *testing.T) {
	var buf strings.Builder
	if err := teamsPage.ExecuteTemplate(&buf, "content", pageData{Explore: "teams"}); err != nil {
		t.Fatal(err)
	}
	if !strings.Contains(buf.String(), "No teams yet") {
		t.Error("FAIL: an empty directory renders an empty list")
	}
}

// A member's standing is named in the words the People editor uses: the
// preset where the string matches one, the letters where it does not.
func TestAMembersRoleReadsAsARoleWhereItIsOne(t *testing.T) {
	for _, tc := range []struct {
		caps, want string
	}{
		{"", "by membership"}, // RFC 0018's third route, named
		{oltp.Maintain.Caps(), "Maintainer"},
		{"a", "Admin"},
		{"2i", "2i"}, // no preset covers it; the letters stand
	} {
		if got := (teamMember{Caps: tc.caps}).Role(); got != tc.want {
			t.Errorf("FAIL: Role(%q) = %q, want %q", tc.caps, got, tc.want)
		}
	}
}

// One team's page: the repositories under it, the members, and the
// owner marked. An unlisted member reaching a manager is marked as
// such, so the manager can see why the public page differs.
func TestTheTeamPageShowsReposMembersAndWhoOwnsIt(t *testing.T) {
	var buf strings.Builder
	td := teamDetail{
		Team: teamRow{Name: "craft-design-group", Title: "Craft Design Group",
			Descr: "quarry and zit", Members: 2, Repos: 2},
		Repos: []teamRepo{
			{Repo: "quarry", Added: "2026-09-12T00:00:00Z"},
			{Repo: "zit", Added: "2026-09-12T00:00:00Z"},
		},
		Members: []teamMember{
			{Account: "dmestas", Owner: true, Listed: true},
			{Account: "lou", Caps: oltp.Maintain.Caps(), Listed: false},
		},
		CanManage: true,
	}
	if err := teamPage.ExecuteTemplate(&buf, "content",
		pageData{Title: "Craft Design Group", OwnH1: true, Team: &td}); err != nil {
		t.Fatal(err)
	}
	body := buf.String()
	for _, want := range []string{
		"Craft Design Group", "quarry and zit",
		`href="/r/quarry"`, `href="/r/zit"`,
		`href="/people/dmestas"`, "owner", "Maintainer", "unlisted",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: the team page is missing %q", want)
		}
	}
}

// A team holding nothing says so on its own page too, in a sentence
// that tells a reader how a repository gets there.
func TestAnEmptyTeamPageExplainsItself(t *testing.T) {
	var buf strings.Builder
	td := teamDetail{Team: teamRow{Name: "core", Members: 1}}
	if err := teamPage.ExecuteTemplate(&buf, "content", pageData{Team: &td}); err != nil {
		t.Fatal(err)
	}
	body := buf.String()
	if !strings.Contains(body, "somebody who administers it") {
		t.Error("FAIL: an empty team does not say how a repository joins")
	}
	if regexp.MustCompile(`0 repositor`).MatchString(body) {
		t.Error("FAIL: renders a zero instead of a sentence")
	}
}

// The repository's own People section must name the route, not just the
// result. RFC 0018 calls this the riskiest change in the feature and
// not one of the new pages: the section answered from stored strings
// alone, so a floor-only team member held read access and did not
// appear at all.
func TestTheRepoRosterNamesTheTeamRoute(t *testing.T) {
	for _, tc := range []struct {
		name string
		row  rosterRow
		want []string
		not  []string
	}{{
		name: "floor only, no string of their own",
		row:  rosterRow{Name: "lou", Team: "core", Route: "by membership of core"},
		want: []string{"by membership via core", `href="/t/core"`, "All of it comes from"},
		// No role chip: they hold no string here, so there is no role
		// to name, and no controls either — the change is on the team.
		not: []string{`class="chip role"`, "row-act"},
	}, {
		name: "a team role",
		row:  rosterRow{Name: "lou", Team: "core", TeamCaps: "a", Route: "via core"},
		want: []string{"Admin via core"},
		not:  []string{"row-act"},
	}, {
		name: "both routes at once",
		row:  rosterRow{Name: "lou", Caps: "a", Role: "Admin", Team: "core", TeamCaps: "uv"},
		want: []string{`class="chip role"`, "via core"},
		// It has a string of its own, so the controls appear and the
		// "all of it comes from" line does not.
		not: []string{"All of it comes from"},
	}, {
		name: "no team at all, the ordinary case",
		row:  rosterRow{Name: "dan", Caps: "a", Role: "Admin"},
		want: []string{`class="chip role"`},
		not:  []string{"via-team", "All of it comes from"},
	}} {
		t.Run(tc.name, func(t *testing.T) {
			var buf strings.Builder
			err := settingsPage.ExecuteTemplate(&buf, "content", pageData{
				Repo:   "secret",
				Roster: &rosterPanel{Rows: []rosterRow{tc.row}, Presets: oltp.Presets},
			})
			if err != nil {
				t.Fatal(err)
			}
			body := buf.String()
			for _, w := range tc.want {
				if !strings.Contains(body, w) {
					t.Errorf("FAIL: missing %q", w)
				}
			}
			for _, n := range tc.not {
				if strings.Contains(body, n) {
					t.Errorf("FAIL: should not contain %q", n)
				}
			}
		})
	}
}

// The team route reads in the same words on both surfaces, so one
// grant cannot be described two ways.
func TestTheRouteReadsTheSameOnBothSurfaces(t *testing.T) {
	caps := oltp.Maintain.Caps()
	onTeamPage := (teamMember{Caps: caps}).Role()
	onRepoPage := (rosterRow{Team: "core", TeamCaps: caps}).TeamRole()
	if onTeamPage != onRepoPage {
		t.Errorf("FAIL: team page says %q, repo page says %q", onTeamPage, onRepoPage)
	}
	if floor := (rosterRow{Team: "core"}).TeamRole(); floor != "by membership" {
		t.Errorf("FAIL: the floor reads as %q", floor)
	}
}

func peopleScreen(t *testing.T, tp teamPeople) string {
	t.Helper()
	tp.Presets = oltp.Presets
	var buf strings.Builder
	if err := teamPeoplePage.ExecuteTemplate(&buf, "content",
		pageData{TeamPeople: &tp}); err != nil {
		t.Fatal(err)
	}
	return buf.String()
}

// The screen RFC 0018 exists for. The heading counts what the list is
// ABOUT, because "everyone" means nothing without saying everyone who
// reaches what.
func TestThePeopleScreenSaysWhatItIsCounting(t *testing.T) {
	body := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core", Title: "Core"}, Repos: 3,
		People: []teamPersonRow{{Account: "dan", Member: true, Owner: true}},
	})
	if !strings.Contains(body, "reaches these 3 repositories") {
		t.Error("FAIL: the heading does not say what it counts")
	}
	// A team holding nothing says so rather than listing people
	// against no repositories.
	empty := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 0,
		People: []teamPersonRow{{Account: "dan", Member: true, Owner: true}},
	})
	if !strings.Contains(empty, "holds no repositories yet") {
		t.Error("FAIL: a team holding nothing does not say so")
	}
	if strings.Contains(empty, "reaches these 0") {
		t.Error("FAIL: renders a zero instead of a sentence")
	}
}

// All three routes on one screen, each distinguishable, and a direct
// grant linked to where it can actually be changed.
func TestThePeopleScreenNamesEveryRoute(t *testing.T) {
	body := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 2, CanManage: true,
		People: []teamPersonRow{
			{Account: "dan", Member: true, Owner: true},
			{Account: "lou", Member: true},
			{Account: "zeitgeist", Member: true, Caps: oltp.Maintain.Caps()},
			{Account: "ann", Direct: []teamDirectRow{{Repo: "zit", Caps: "a"}}},
		},
	})
	for _, want := range []string{
		"owner", "by membership", "Maintainer",
		`href="/r/zit/settings#people"`, "zit: a",
		"Not in this team", "Change that where it was granted",
	} {
		if !strings.Contains(body, want) {
			t.Errorf("FAIL: missing %q", want)
		}
	}
}

// A manager edits a team role in place — the whole point of one screen
// — and cannot edit a direct grant here, because it is not the team's.
func TestOnlyTheTeamsOwnGrantIsEditableHere(t *testing.T) {
	rows := []teamPersonRow{
		{Account: "lou", Member: true},
		{Account: "ann", Direct: []teamDirectRow{{Repo: "zit", Caps: "a"}}},
	}
	managed := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 1, CanManage: true, People: rows})
	if !strings.Contains(managed, `action="/t/core/member"`) {
		t.Error("FAIL: a manager gets no control at all")
	}
	if strings.Count(managed, "row-act") != 1 {
		t.Errorf("FAIL: %d editable rows, want only the member's", strings.Count(managed, "row-act"))
	}
	// A reader who cannot manage gets the list and no controls.
	read := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 1, People: rows})
	if strings.Contains(read, "row-act") || strings.Contains(read, "roster-add") {
		t.Error("FAIL: a reader is offered controls")
	}
	if !strings.Contains(read, "lou") {
		t.Error("FAIL: a reader cannot see the list")
	}
}

// A team string no preset covers must not be silently rounded to the
// nearest role by an edit.
func TestACustomTeamStringSaysSoBeforeItIsOverwritten(t *testing.T) {
	body := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 1, CanManage: true,
		People: []teamPersonRow{{Account: "lou", Member: true, Caps: "2i"}},
	})
	if !strings.Contains(body, "which no role covers") {
		t.Error("FAIL: a custom string does not warn before being replaced")
	}
	row := teamPersonRow{Member: true, Caps: "2i"}
	if !row.Custom() || row.Preset() != "" {
		t.Errorf("FAIL: %q reads as a preset", row.Caps)
	}
	if (teamPersonRow{Member: true, Caps: "a"}).Custom() {
		t.Error("FAIL: Admin reads as custom")
	}
}

// Removal warns what it actually does, because on a private repository
// the membership floor is the whole of somebody's access.
func TestRemovalSaysWhatItCosts(t *testing.T) {
	body := peopleScreen(t, teamPeople{
		Team: teamRow{Name: "core"}, Repos: 2, CanManage: true,
		People: []teamPersonRow{{Account: "lou", Member: true}},
	})
	if !strings.Contains(body, "that is all of their access") {
		t.Error("FAIL: removal does not say what it costs on a private repository")
	}
}

// One grant, one set of words, on all three surfaces.
func TestTheRoleReadsTheSameOnAllThreeSurfaces(t *testing.T) {
	caps := oltp.Maintain.Caps()
	onTeam := (teamMember{Caps: caps}).Role()
	onRepo := (rosterRow{Team: "core", TeamCaps: caps}).TeamRole()
	onScreen := (teamPersonRow{Member: true, Caps: caps}).Role()
	if onTeam != onRepo || onRepo != onScreen {
		t.Errorf("FAIL: team page %q, repo roster %q, people screen %q", onTeam, onRepo, onScreen)
	}
	if got := (teamPersonRow{Member: true}).Role(); got != "by membership" {
		t.Errorf("FAIL: the floor reads as %q on the people screen", got)
	}
}

// A control that would fail is not offered — quarry's rule, and the
// one the repository roster already follows for the last admin. The
// guard refuses removing a team's last owner, so the button must not
// be live.
func TestTheLastOwnersRemoveIsNotOffered(t *testing.T) {
	sole := teamPeople{Team: teamRow{Name: "core"}, Repos: 1, CanManage: true, Owners: 1,
		People: []teamPersonRow{{Account: "dmestas", Member: true, Owner: true}}}
	body := peopleScreen(t, sole)
	if !strings.Contains(body, "the last owner; make somebody else an owner first") {
		t.Error("FAIL: the last owner is offered a Remove that would be refused")
	}
	if strings.Contains(body, `name="remove"`) {
		t.Error("FAIL: the live Remove button is still there for the last owner")
	}
	// With a second owner, both are removable.
	two := teamPeople{Team: teamRow{Name: "core"}, Repos: 1, CanManage: true, Owners: 2,
		People: []teamPersonRow{
			{Account: "dmestas", Member: true, Owner: true},
			{Account: "lou", Member: true, Owner: true},
		}}
	body = peopleScreen(t, two)
	if strings.Count(body, `name="remove"`) != 2 {
		t.Errorf("FAIL: %d live Remove buttons with two owners, want 2", strings.Count(body, `name="remove"`))
	}
	// And an ordinary member is always removable.
	if !sole.LastOwner(teamPersonRow{Member: true, Owner: true}) {
		t.Error("FAIL: LastOwner is false for the sole owner")
	}
	if sole.LastOwner(teamPersonRow{Member: true}) {
		t.Error("FAIL: an ordinary member reads as the last owner")
	}
}