Skip to content
quarry Artifact [1250a623f8]
Login

Artifact [1250a623f8]

Artifact 1250a623f8e5c4427efeb6e732ad5e06a07b7f0bfe00830aff030ed58e07611a:


package api

import (
	"context"
	"errors"
	"strings"
	"testing"

	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/oltp"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// closeIndex folds root->child on trunk and a leaf on dev, so a test can
// close the tip of a protected branch and of an unprotected one.
func closeIndex(t *testing.T) (*index.Index, string, string) {
	t.Helper()
	h1, h2, h3 := strings.Repeat("11", 32), strings.Repeat("22", 32), strings.Repeat("33", 32)
	ix := wikiIndex(t)
	if err := ix.FoldNow(branchArts(h1, h2, h3)); err != nil {
		t.Fatal(err)
	}
	return ix, h2, h3 // trunk tip, dev tip
}

func closeRegistry(t *testing.T, ix *index.Index, rules []oltp.Protection) *ops.Registry {
	t.Helper()
	r := ops.NewRegistry()
	if err := RegisterAll(r, Deps{Version: "v",
		Repos:   NewRepos(map[string]*Repo{"test": {Index: ix, Zit: &zitcli.Client{}}}),
		Protect: stubProtect{rules: rules},
	}); err != nil {
		t.Fatal(err)
	}
	return r
}

// Closing the tip of a protected branch takes an owner's hand.
//
// checkin.close writes fossil's `closed` tag and asked for `i` and
// nothing else, so branch protection did not reach it. The bypass was
// available to exactly the population the control exists to constrain:
// protection limits people who ALREADY hold check-in rights, and
// holding check-in rights is precisely what let them write the tag
// (ad732c8f, Severe, jva's argument).
//
// Reversibility does not mitigate it. The owner's symptom is their own
// commit failing with "cannot commit against a closed leaf", which
// names neither the cause nor the actor, and the recovery is a raw tag
// cancel that appears in no UI.
func TestClosingAProtectedBranchNeedsAnOwner(t *testing.T) {
	ix, trunkTip, _ := closeIndex(t)
	r := closeRegistry(t, ix, []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}})

	_, err := r.Invoke(ops.WithUser(context.Background(), "bob"), "checkin.close",
		checkinCloseIn{Repo: "test", Hash: trunkTip})
	if !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("bob closed the tip of a branch alice owns: err = %v, want ErrForbidden", err)
	}
	// And the refusal names the owner, so the person refused knows whose
	// hand it takes rather than only that it was not theirs.
	if err != nil && !strings.Contains(err.Error(), "alice") {
		t.Errorf("the refusal does not name the owner: %v", err)
	}
}

// Reopening is the same decision. A non-owner who could reopen could
// undo an owner's close, which is the same membership question wearing
// the opposite sign.
func TestReopeningAProtectedBranchNeedsAnOwner(t *testing.T) {
	ix, trunkTip, _ := closeIndex(t)
	r := closeRegistry(t, ix, []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}})

	_, err := r.Invoke(ops.WithUser(context.Background(), "bob"), "checkin.close",
		checkinCloseIn{Repo: "test", Hash: trunkTip, Reopen: true})
	if !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("bob reopened a branch alice owns: err = %v, want ErrForbidden", err)
	}
}

// The gate must not widen into a refusal for everybody: an owner still
// closes, and an unprotected branch is still anybody's with `i`.
//
// Asserted as "not forbidden" rather than "no error", because the write
// itself goes to a store this test does not run. A gate that refused
// these would be the over-refusal that the protected-branch work has
// already caused once.
func TestClosingIsStillAllowedWhereItShouldBe(t *testing.T) {
	ix, trunkTip, devTip := closeIndex(t)
	r := closeRegistry(t, ix, []oltp.Protection{{Branch: "trunk", Owners: []string{"alice"}}})

	for _, tc := range []struct{ who, hash, what string }{
		{"alice", trunkTip, "the owner closing her own protected branch"},
		{"bob", devTip, "anybody closing an unprotected branch"},
	} {
		_, err := r.Invoke(ops.WithUser(context.Background(), tc.who), "checkin.close",
			checkinCloseIn{Repo: "test", Hash: tc.hash})
		if errors.Is(err, ops.ErrForbidden) {
			t.Errorf("%s was refused: %v", tc.what, err)
		}
	}
}