Skip to content
quarry Artifact [d9ebe7b250]
Login

Artifact [d9ebe7b250]

Artifact d9ebe7b2501474cec0b813c6d4c75ed8b773d62a802ae1d30dac6d985aaa8cd4:


package index

import (
	"strings"
	"testing"

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

func foldTicket(t *testing.T, ix *Index, hash, id, date string, fields ...string) {
	t.Helper()
	var fs []artifact.Field
	for i := 0; i+1 < len(fields); i += 2 {
		fs = append(fs, artifact.Field{Name: fields[i], Value: fields[i+1]})
	}
	if err := ix.FoldNow([]Parsed{{Hash: hash, Art: &artifact.Artifact{
		Type: artifact.Ticket, TicketID: id, Date: date, User: "eve", Fields: fs,
	}}}); err != nil {
		t.Fatal(err)
	}
}

// A ticket its tracker marked private must be indistinguishable from one
// that does not exist, on every anonymous surface at once. The list and
// search already hide it; this pins the two that leaked — the timeline's
// event rows (id, author, date on public display) and the nav count —
// and the transition BACK to public, which must restore what hiding
// removed. Erring toward private is only safe if un-privating works;
// otherwise nobody flips the flag.
func TestPrivateTicketLeavesNoPublicTrace(t *testing.T) {
	ix := openIndex(t)
	id := strings.Repeat("cd", 20)
	foldTicket(t, ix, "tc1", id, "2026-01-01T10:00:00",
		"title", "the secret ticket", "status", "Open")
	foldTicket(t, ix, "tc2", id, "2026-01-02T10:00:00", "is_private", "1")

	if tl, _, err := ix.Timeline(TimelineFilter{Limit: 50}); err != nil {
		t.Fatal(err)
	} else {
		for _, e := range tl {
			if e.Kind == "t" {
				t.Fatalf("a private ticket's change is on the timeline: %+v", e)
			}
		}
	}
	if c, err := ix.Counts(false); err != nil {
		t.Fatal(err)
	} else if c.Tickets != 0 {
		t.Fatalf("the nav counts %d tickets; the only one is private", c.Tickets)
	}
	// Reads refuse it, but the token-gated write path must still resolve
	// it — a private ticket nobody can address is one nobody can ever
	// make public again.
	if _, ok, err := ix.TicketByPrefix(id[:10]); err != nil || ok {
		t.Fatalf("a private ticket resolved on the READ path (ok=%v err=%v)", ok, err)
	}
	if _, ok, err := ix.TicketByPrefixAll(id[:10]); err != nil || !ok {
		t.Fatalf("TicketByPrefixAll lost the private ticket (ok=%v err=%v); it is now uneditable", ok, err)
	}

	// Made public again: everything hidden comes back.
	foldTicket(t, ix, "tc3", id, "2026-01-03T10:00:00", "is_private", "0")
	tl, _, err := ix.Timeline(TimelineFilter{Limit: 50})
	if err != nil {
		t.Fatal(err)
	}
	events := 0
	for _, e := range tl {
		if e.Kind == "t" {
			events++
		}
	}
	if events != 3 {
		t.Fatalf("un-privating restored %d of 3 timeline events", events)
	}
	if c, _ := ix.Counts(false); c.Tickets != 1 {
		t.Fatalf("un-privating did not restore the count: %d", c.Tickets)
	}
}

// Private reports the folded flag for one ticket, for the read gate in
// ticket.get: the detail page is the surface that leaked the whole
// ticket — description, discussion, attachment list — to anyone holding
// an id prefix.
func TestPrivateFlagIsReadable(t *testing.T) {
	ix := openIndex(t)
	pub, priv := strings.Repeat("ab", 20), strings.Repeat("cd", 20)
	foldTicket(t, ix, "tp1", pub, "2026-01-01T10:00:00", "title", "public")
	foldTicket(t, ix, "tp2", priv, "2026-01-01T11:00:00", "title", "hidden", "is_private", "yes")

	if p, err := ix.TicketPrivate(pub); err != nil || p {
		t.Fatalf("public ticket reported private=%v err=%v", p, err)
	}
	if p, err := ix.TicketPrivate(priv); err != nil || !p {
		t.Fatalf("private ticket reported private=%v err=%v", p, err)
	}
	// Erring toward withholding: an id the folded table has never seen
	// answers private, not public.
	if p, err := ix.TicketPrivate(strings.Repeat("ef", 20)); err != nil || !p {
		t.Fatalf("unknown ticket reported private=%v err=%v; must err toward withholding", p, err)
	}
}