Skip to content
quarry Artifact [dbfd7fb78f]
Login

Artifact [dbfd7fb78f]

Artifact dbfd7fb78ffc67dcf609ab904ebbe32d582aef806d39011768a62bda1c41aee9:


package web

import (
	"io"
	"net/http"
	"net/http/httptest"
	"path/filepath"
	"strings"
	"testing"

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

// The cross-repository listing opens on what the reader is assigned, so
// that view carries no parameter and leaving it does. A repository's own
// tracker has no such default and must never draw the parameter at all.
func TestAssignedIsTheAcrossListingsDefault(t *testing.T) {
	across := ticketNav{Base: "/tickets", Mine: true}
	if got := across.URL("", ""); got != "/tickets" {
		t.Errorf("the default view is not the plain URL: %q", got)
	}
	if got := across.URL("mine", "0"); !strings.Contains(got, "mine=0") {
		t.Errorf("leaving the default does not say so: %q", got)
	}
	// Back in again, from the way out.
	out := across
	out.Mine = false
	if got := out.URL("mine", "1"); got != "/tickets" {
		t.Errorf("returning to the default is not the plain URL: %q", got)
	}
	// One repository's tracker: no mine, either way round.
	repo := ticketNav{Repo: "alpha"}
	for _, n := range []ticketNav{repo, {Repo: "alpha", Mine: true}} {
		if got := n.URL("", ""); strings.Contains(got, "mine") {
			t.Errorf("a tracker's own listing carries mine: %q", got)
		}
	}
}

// The rail names both sides of the default, and names neither on a
// tracker that has no default to leave.
func TestTheRailNamesBothSidesOfAssigned(t *testing.T) {
	labels := func(n ticketNav, me string) []string {
		t.Helper()
		var out []string
		for _, sec := range ticketRail(n, nil, nil, nil, me, nil) {
			for _, l := range sec.Links {
				out = append(out, l.Label)
			}
		}
		return out
	}
	has := func(ls []string, want string) bool {
		for _, l := range ls {
			if l == want {
				return true
			}
		}
		return false
	}
	across := labels(ticketNav{Base: "/tickets", Mine: true}, "dan")
	if !has(across, "assigned to you") || !has(across, "every ticket you can reach") {
		t.Errorf("the rail does not name both ways: %v", across)
	}
	repo := labels(ticketNav{Repo: "alpha"}, "dan")
	if has(repo, "assigned to you") {
		t.Errorf("a tracker's rail offers an assigned view: %v", repo)
	}
	// Signed out there is no queue to be assigned one.
	if anon := labels(ticketNav{Base: "/tickets", Mine: true}, ""); has(anon, "assigned to you") {
		t.Errorf("a signed-out rail offers assigned to you: %v", anon)
	}
}

// An empty assigned view says why it is empty and offers the way out,
// rather than reading as "this forge has no tickets".
func TestTheEmptyAssignedViewExplainsItself(t *testing.T) {
	var buf strings.Builder
	d := pageData{Title: "Tickets", Across: true,
		Nav: ticketNav{Base: "/tickets", Mine: true}}
	if err := ticketListPage.ExecuteTemplate(&buf, "tkt-view", d); err != nil {
		t.Fatal(err)
	}
	body := buf.String()
	if !strings.Contains(body, "assigned to you") {
		t.Errorf("the empty view does not say nothing is assigned: %s", body)
	}
	if !strings.Contains(body, "mine=0") {
		t.Errorf("the empty view offers no way to everything else")
	}
}

// The default yields when it finds nothing. A forge where no tracker has
// ever named an assignee would otherwise greet every reader with an empty
// page over a listing full of tickets — which is what production did.
func TestTheAssignedDefaultYieldsWhenNothingIsAssigned(t *testing.T) {
	ix := ticketIndexFor(t, "", "unassigned ticket")
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos:    api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Accounts: stubAccts{grants: map[string]map[string]string{"dan": {"alpha": "write"}}},
	}); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil, []Option{WithSessions(fakeSessions{user: "dan"})}))
	t.Cleanup(srv.Close)
	get := func(path string) string {
		t.Helper()
		req, _ := http.NewRequest("GET", srv.URL+path, nil)
		req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
		resp, err := srv.Client().Do(req)
		if err != nil {
			t.Fatal(err)
		}
		defer resp.Body.Close()
		b, _ := io.ReadAll(resp.Body)
		return string(b)
	}
	// The default: nothing assigned, so the reader still sees the ticket,
	// and the page says why it is showing it.
	body := get("/tickets")
	if !strings.Contains(body, "unassigned ticket") {
		t.Errorf("the default hid every ticket on the forge:\n%s", body)
	}
	if !strings.Contains(body, `data-hook="mine-fell-back"`) {
		t.Errorf("the page changed listing without saying so:\n%s", body)
	}
	// Asked for explicitly, the empty answer is the true one and stands.
	asked := get("/tickets?mine=1")
	if strings.Contains(asked, "unassigned ticket") {
		t.Errorf("an explicit mine=1 fell back anyway:\n%s", asked)
	}
	if strings.Contains(asked, `data-hook="mine-fell-back"`) {
		t.Error("an explicit mine=1 claimed to have fallen back")
	}
	// And with something assigned, the default answers for itself.
	assigned := ticketIndexFor(t, "dan", "held by dan")
	r2 := ops.NewRegistry()
	if err := api.RegisterAll(r2, api.Deps{Version: "v",
		Repos:    api.NewRepos(map[string]*api.Repo{"alpha": {Index: assigned}}),
		Accounts: stubAccts{grants: map[string]map[string]string{"dan": {"alpha": "write"}}},
	}); err != nil {
		t.Fatal(err)
	}
	srv2 := httptest.NewServer(HandlerWith(r2, nil, nil, []Option{WithSessions(fakeSessions{user: "dan"})}))
	t.Cleanup(srv2.Close)
	req, _ := http.NewRequest("GET", srv2.URL+"/tickets", nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	resp, err := srv2.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	if !strings.Contains(string(b), "held by dan") {
		t.Error("the assigned view lost the ticket assigned to the reader")
	}
	if strings.Contains(string(b), `data-hook="mine-fell-back"`) {
		t.Error("the default yielded even though something was assigned")
	}
}

// ticketIndexFor is one tracker holding one open ticket, assigned to who
// (empty for unassigned).
func ticketIndexFor(t *testing.T, who, title string) *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() })
	fields := []artifact.Field{{Name: "status", Value: "Open"}, {Name: "title", Value: title}}
	if who != "" {
		fields = append(fields, artifact.Field{Name: "assignee", Value: who})
	}
	if err := ix.FoldNow([]index.Parsed{{Hash: "t1t1", Art: &artifact.Artifact{
		Type: artifact.Ticket, TicketID: strings.Repeat("e", 40),
		Date: "2026-09-15T09:00:00", User: "eve", Fields: fields}}}); err != nil {
		t.Fatal(err)
	}
	return ix
}