package web
import (
"os"
"strings"
"testing"
"craftdesign.group/quarry/internal/ops"
)
// A page must gate a control on the LETTER its op requires.
//
// Five surfaces gated write controls on mayWrite, a wrapper pinned at
// ops.Token. Token answers "is this account a member of the
// repository"; the ops behind those controls answer to capability
// letters. So a member holding no letters was shown the ticket
// retitle box, the description editor, the triage selects, the
// comment/close composer, both boards' move control, the new-ticket
// form, and propose/close/reopen on a check-in — and was refused by
// every one of them on submit.
//
// This table is the contract between a drawn control and the op behind
// it. It does not read the handlers, which is not something a test can
// do honestly; it pins the OP side, so that changing an op's Caps
// fails here and names the control that has to move with it. That is
// the direction the drift actually travels: a letter gets adjusted on
// the op and the page quietly keeps offering the old answer.
var gateContract = []struct {
op string
letters string
drawn string
}{
{"ticket.set", "cw",
"ticketPage CanWrite: retitle, description, triage selects, comment/close composer; and both boards' move control"},
{"ticket.new", "n", "ticketNewPage CanWrite, on a repository that keeps tickets in the artifact log"},
{"ticket.intake", "n", "ticketNewPage CanWrite, on a repository that keeps them in the local tier"},
{"checkin.close", "i", "checkinPage CanClose and CanReopen"},
{"landing.open", "3", "checkinPage CanWrite: the Propose button"},
}
func TestEveryGatedControlMatchesItsOpsLetters(t *testing.T) {
r := referenceRegistry(t)
for _, c := range gateContract {
d, ok := r.Lookup(c.op)
if !ok {
t.Errorf("FAIL: %s is not registered, but %s is gated on it", c.op, c.drawn)
continue
}
if d.Caps != c.letters {
t.Errorf("FAIL: %s now declares Caps %q, not %q — %s still gates on %q, "+
"so it is drawn for accounts the op refuses",
c.op, d.Caps, c.letters, c.drawn, c.letters)
}
if d.Caps == "" {
t.Errorf("FAIL: %s declares no Caps at all, so %s has nothing to gate on",
c.op, c.drawn)
}
if d.Auth != ops.Token && d.Auth != ops.Anonymous {
t.Errorf("FAIL: %s sits at Auth %v; the contract assumes a Token floor", c.op, d.Auth)
}
}
}
// And the wrapper does not come back. It was removed rather than left
// unused, because its name reads like the right thing to reach for and
// its own comment warned against exactly what it caused.
func TestTheTokenFlooredWriteWrapperIsGone(t *testing.T) {
src, err := os.ReadFile("web.go")
if err != nil {
t.Fatal(err)
}
if strings.Contains(string(src), "func mayWrite(") {
t.Error("FAIL: mayWrite is back. Gate a control on mayCap and the op's own " +
"Caps string; mayAt is for the places where the question really is the level.")
}
}
// Every write control on a ticket hangs off CanWrite, so that one
// field carries the whole letter decision for the page.
//
// This is the template half of the contract. The handler half — that
// CanWrite is now mayCap(..., "cw", ...) and not a membership check —
// is pinned by gateContract above. Together: the handler asks for the
// right letter, and the page draws nothing that escapes the answer.
//
// If a new control is added outside CanWrite it will be drawn for
// readers ticket.set refuses, which is the bug this whole change is
// about, and this test is what notices.
func TestNoTicketWriteControlEscapesCanWrite(t *testing.T) {
tk := tktFixture()
base := pageData{Repo: "q", Ticket: tk, Vocab: map[string][]string{
"status": {"Open", "Closed"}, "priority": {"High", "Low"}}}
base.TicketFields = ticketControls(tk, base.Vocab)
off, on := base, base
on.CanWrite = true
for _, block := range []string{"tkt-side", "content"} {
cold, warm := renderTkt(t, block, off), renderTkt(t, block, on)
// The post targets are the controls: a form that writes to the
// ticket must not appear for a reader who holds no letter.
if n := strings.Count(cold, `method="post"`); n != 0 {
t.Errorf("FAIL: %s draws %d posting form(s) with CanWrite false — "+
"ticket.set refuses every one of them", block, n)
}
if strings.Count(warm, `method="post"`) == 0 && block == "content" {
t.Errorf("FAIL: %s draws no posting form even with CanWrite true, "+
"so this test is asserting nothing", block)
}
if strings.Contains(cold, `id="t-edit-title"`) || strings.Contains(cold, `id="t-edit-desc"`) {
t.Errorf("FAIL: %s offers the inline editors with CanWrite false", block)
}
}
}