Artifact 2ed3367f34178a04916ffc1c395b41621627b917e5360f8154ecce3abdafb712:
- File
internal/index/private_test.go
— part of check-in
[9b2b188ece]
at
2026-08-23 11:08:48
on branch trunk
— caps: private tickets exist for holders of e
The edge resolved a CapSet and threw it away; it now rides the context, so an op can ask which letters the caller holds. Private tickets list, count and resolve for e, and for nobody else. One privacyWhere() so a fourth read path cannot silently ship without the clause. (user: dmestas, size: 3690) [annotate] [blame] [check-ins using]
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(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(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) } }