Skip to content
quarry Artifact [76254aa344]
Login

Artifact [76254aa344]

Artifact 76254aa3445f9e65a59130bb16e46b251f99bb30c184eb59042083e155a8d0bf:


package api

import (
	"context"
	"errors"
	"path/filepath"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
)

// openIndexAt is an empty index of its own, so the two trackers on the
// board are genuinely separate stores rather than one seen twice.
func openIndexAt(t *testing.T) *index.Index {
	t.Helper()
	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	return ix
}

// boardRig: a team holding two repositories with tickets in both, some
// assigned and some not.
func boardRig(t *testing.T) (*ops.Registry, *oltp.DB) {
	t.Helper()
	db, err := oltp.Open(filepath.Join(t.TempDir(), "o.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { db.Close() })
	for _, who := range []string{"owen", "mia", "nils"} {
		if err := db.AddAccount(who, "a-long-enough-password", false); err != nil {
			t.Fatal(err)
		}
	}
	mk := func(ix *index.Index, id, title, assignee string) {
		fields := []artifact.Field{
			{Name: "title", Value: title}, {Name: "status", Value: "Open"},
		}
		if assignee != "" {
			fields = append(fields, artifact.Field{Name: "assignee", Value: assignee})
		}
		if err := ix.FoldNow([]index.Parsed{{Hash: strings.Repeat(id, 20),
			Art: &artifact.Artifact{Type: artifact.Ticket, TicketID: strings.Repeat(id, 20),
				Date: "2026-09-0" + id[:1] + "T10:00:00", User: "owen", Fields: fields}}}); err != nil {
			t.Fatal(err)
		}
	}
	alpha, beta := openIndexAt(t), openIndexAt(t)
	mk(alpha, "a1", "alpha assigned", "mia")
	mk(alpha, "a2", "alpha spare", "")
	mk(beta, "b1", "beta assigned", "nils")
	mk(beta, "b2", "beta spare", "")

	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v",
		Repos: NewRepos(map[string]*Repo{
			"alpha": {Index: alpha, Code: "a"}, "beta": {Index: beta, Code: "b"}}),
		Teams:    db,
		Accounts: acctAdapter{db},
		// The read gate, wired as the forge wires it. Without this
		// CanRead is nil, which means "every repository is readable" —
		// so a visibility test against an unwired rig proves nothing
		// about the op. The first version of this test did exactly
		// that and passed a leak.
		CanRead: func(ctx context.Context, repo string) bool {
			return db.EffectiveCaps(ops.User(ctx), repo).HasAny("o")
		},
		CapsFor: func(a, repo string) CapSet { return db.EffectiveCaps(a, repo) }}); err != nil {
		t.Fatal(err)
	}
	if _, err := r.Invoke(asUser("owen"), "team.create", teamCreateIn{Name: "core"}); err != nil {
		t.Fatal(err)
	}
	for _, repo := range []string{"alpha", "beta"} {
		if err := db.Grant("owen", repo, oltp.RepoAdmin); err != nil {
			t.Fatal(err)
		}
		if _, err := r.Invoke(asUser("owen"), "team.repo.add",
			teamRepoIn{Name: "core", Repo: repo}); err != nil {
			t.Fatal(err)
		}
	}
	return r, db
}

func board(t *testing.T, r *ops.Registry, ctx context.Context, in teamBoardIn) teamBoardOut {
	t.Helper()
	if in.Limit == 0 {
		in.Limit = 50
	}
	out, err := r.Invoke(ctx, "team.tickets", in)
	if err != nil {
		t.Fatal(err)
	}
	return out.(teamBoardOut)
}

// One list across the team's repositories, with the assignee columns
// beside it and nobody LAST — which RFC 0018 asks for because the
// unassigned column is what a team opens the page for.
func TestTheBoardGathersBothTrackersAndPutsNobodyLast(t *testing.T) {
	r, _ := boardRig(t)
	got := board(t, r, asUser("owen"), teamBoardIn{Name: "core"})

	if len(got.Tickets) != 4 {
		t.Fatalf("FAIL: the board holds %d tickets, want 4", len(got.Tickets))
	}
	// Both trackers, and each row still says which one it came from.
	repos := map[string]bool{}
	for _, tk := range got.Tickets {
		repos[tk.Repo] = true
	}
	if !repos["alpha"] || !repos["beta"] {
		t.Errorf("FAIL: the board is not merging both trackers: %v", repos)
	}

	// mia, nils, then nobody.
	if len(got.Columns) != 3 {
		t.Fatalf("FAIL: %d columns, want mia, nils and nobody: %+v", len(got.Columns), got.Columns)
	}
	if got.Columns[0].Assignee != "mia" || got.Columns[1].Assignee != "nils" {
		t.Errorf("FAIL: named columns are not sorted: %+v", got.Columns)
	}
	last := got.Columns[len(got.Columns)-1]
	if !last.Nobody() {
		t.Errorf("FAIL: the unassigned column is not last: %+v", got.Columns)
	}
	if last.Open != 2 {
		t.Errorf("FAIL: the unassigned column counts %d, want 2", last.Open)
	}
}

// Nobody is pickable, which is the whole point of putting it on the
// board: the empty assignee has to be a real filter value and not
// "every ticket".
func TestTheUnassignedColumnIsPickable(t *testing.T) {
	r, _ := boardRig(t)
	nobody := board(t, r, asUser("owen"), teamBoardIn{Name: "core", Nobody: true})
	if len(nobody.Tickets) != 2 {
		t.Fatalf("FAIL: picking nobody gave %d tickets, want 2", len(nobody.Tickets))
	}
	for _, tk := range nobody.Tickets {
		if tk.Assignee != "" {
			t.Errorf("FAIL: %q is assigned to %q", tk.Title, tk.Assignee)
		}
	}
	// One person's column.
	hers := board(t, r, asUser("owen"), teamBoardIn{Name: "core", Assignee: "mia"})
	if len(hers.Tickets) != 1 || hers.Tickets[0].Assignee != "mia" {
		t.Errorf("FAIL: mia's column holds %+v", hers.Tickets)
	}
	// Picking a column must not make the OTHER columns vanish from the
	// strip that got you there.
	if len(hers.Columns) != 3 {
		t.Errorf("FAIL: picking a column collapsed the strip to %+v", hers.Columns)
	}
}

// Visibility is per REPOSITORY, not membership of the team. A board has
// no team-own content, so the gate is the one each repository already
// applies — and a private repository stays absent for somebody who
// cannot read it even though the team holds it.
func TestABoardHidesARepositoryTheReaderCannotSee(t *testing.T) {
	r, db := boardRig(t)
	// Make beta private: both categories empty.
	for _, cat := range []string{"nobody", "anonymous"} {
		if err := db.SetCategoryCaps("beta", cat, ""); err != nil {
			t.Fatal(err)
		}
	}
	// mia is nobody here: not in the team, nothing granted.
	got := board(t, r, asUser("mia"), teamBoardIn{Name: "core"})
	for _, tk := range got.Tickets {
		if tk.Repo == "beta" {
			t.Errorf("FAIL: a reader who cannot see beta was shown %q", tk.Title)
		}
	}
	for _, name := range got.Rooms {
		if name == "beta" {
			t.Error("FAIL: the board names a repository the reader cannot see")
		}
	}
	// owen administers it, so he still sees it.
	his := board(t, r, asUser("owen"), teamBoardIn{Name: "core"})
	if len(his.Tickets) != 4 {
		t.Errorf("FAIL: the admin sees %d tickets, want 4", len(his.Tickets))
	}
}

// A board for a team that does not exist is not found, rather than an
// empty board that reads as "this team has no work".
func TestAnUnknownTeamHasNoBoard(t *testing.T) {
	r, _ := boardRig(t)
	if _, err := r.Invoke(asUser("owen"), "team.tickets",
		teamBoardIn{Name: "nope", Limit: 50}); !errors.Is(err, ops.ErrNotFound) {
		t.Errorf("FAIL: an unknown team gave %v", err)
	}
}