Skip to content
quarry Artifact [cb5256edc6]
Login

Artifact [cb5256edc6]

Artifact cb5256edc6380ae3c806f9f8c20520dd4f481a4f55df860b78a3ca62e6e29de6:


package web

import (
	"os"
	"path/filepath"
	"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) {
	// The whole package, not web.go: moving the helper to another file
	// would otherwise satisfy this while the hazard came back.
	files, err := filepath.Glob("*.go")
	if err != nil {
		t.Fatal(err)
	}
	var src []byte
	for _, f := range files {
		if strings.HasSuffix(f, "_test.go") {
			continue
		}
		b, rerr := os.ReadFile(f)
		if rerr != nil {
			t.Fatal(rerr)
		}
		src = append(src, b...)
	}
	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)
		}
	}
}