package index
import (
"path/filepath"
"testing"
)
// Counts takes includePrivate and its ticket number depends on it — but
// the memo behind it was ONE slot, so whoever asked first decided what
// everyone got until the next fold. An admin loading any page warmed it
// with private tickets counted, and the next anonymous reader was told
// how many private tickets the tracker holds.
//
// A count is small, and it is still the answer to a question the reader
// was not allowed to ask.
func TestCountsMemoDoesNotLeakPrivateTickets(t *testing.T) {
ix, err := Open(filepath.Join(t.TempDir(), "index.db"))
if err != nil {
t.Fatal(err)
}
defer ix.Close()
ins := func(id string, private int) {
if _, err := ix.db.Exec(`INSERT INTO ticket
(id, created, updated, title, status, private)
VALUES (?, '2026-01-01', '2026-01-01', 'x', 'Open', ?)`, id, private); err != nil {
t.Fatal(err)
}
}
ins("public-1", 0)
ins("secret-1", 1)
ins("secret-2", 1)
// The admin looks first and warms the memo.
all, err := ix.Counts(true)
if err != nil {
t.Fatal(err)
}
if all.Tickets != 3 {
t.Fatalf("admin sees %d open tickets, want all 3", all.Tickets)
}
// Now somebody who may not see private tickets asks.
pub, err := ix.Counts(false)
if err != nil {
t.Fatal(err)
}
if pub.Tickets != 1 {
t.Errorf("a reader without private access is told there are %d open tickets, "+
"want 1: the memo is serving the admin's count", pub.Tickets)
}
// And the other way round: a public reader first must not leave the
// admin under-counting their own tracker.
ix.invalidateCounts()
if _, err := ix.Counts(false); err != nil {
t.Fatal(err)
}
again, err := ix.Counts(true)
if err != nil {
t.Fatal(err)
}
if again.Tickets != 3 {
t.Errorf("admin sees %d after a public reader warmed the memo, want 3", again.Tickets)
}
}