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 := noHooks.template(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
}