Skip to content
quarry Artifact [9dac090eac]
Login

Artifact [9dac090eac]

Artifact 9dac090eacfef430d7aa15dab0c6838b719c4a988e767749443a42c848754eda:


package api

import (
	"context"
	"errors"
	"os/exec"
	"strings"
	"testing"

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

// Retargeting decides where the next merge is written, so it takes
// accept's letter rather than the forum one. With the forum letter,
// anyone who may post in a record's thread could aim somebody else's
// work at a branch they never chose.
func TestRetargetSurfaceTakesTheCheckinLetter(t *testing.T) {
	r := ops.NewRegistry()
	if err := registerLandingRetarget(r, Deps{}); err != nil {
		t.Fatal(err)
	}
	op, ok := r.Lookup("landing.retarget")
	if !ok {
		t.Fatal("landing.retarget is not registered")
	}
	if op.Caps != "i" {
		t.Fatalf("caps = %q, want i — the letter that writing a merge takes", op.Caps)
	}
	if op.Auth != ops.Token {
		t.Fatalf("auth = %v, want Token", op.Auth)
	}
	if op.Method != "POST" || op.Pattern != "/api/v1/r/{repo}/landing/{id}/retarget" {
		t.Fatalf("route = %s %s", op.Method, op.Pattern)
	}
}

// An unknown record is not found rather than a fault, so the page can
// say so instead of apologising.
func TestRetargetOfAnUnknownRecordIsNotFound(t *testing.T) {
	r := ops.NewRegistry()
	ix := wikiIndex(t)
	stub := &Repo{Index: ix}
	if err := registerLandingRetarget(r, Deps{Repos: NewRepos(map[string]*Repo{"test": stub})}); err != nil {
		t.Fatal(err)
	}
	_, err := r.Invoke(t.Context(), "landing.retarget",
		landingRetargetIn{Repo: "test", ID: "nosuchrecord", To: "trunk"})
	if !errors.Is(err, ops.ErrNotFound) {
		t.Fatalf("err = %v, want ErrNotFound", err)
	}
}

// A record's target is the author's to move, or a maintainer's — never
// any passing developer's.
//
// The letters cannot answer this alone, which is the lesson G-147 left:
// `i` says the caller may check in here, and retargeting your OWN
// record is part of proposing while moving somebody else's is the
// ACCEPTOR'S act: dmestas ruled (ae900759) that whoever could accept a
// record onto its target may also move it — the check-in letter plus
// the target branch's protection, exactly as accept checks them, and no
// new capability. Before that it took the moderation letters, so a
// forum moderator could aim a merge and a branch owner could not.
func TestRetargetIsTheAuthorsOrAMaintainers(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	ix := landingIndex(t, "ada", "feat/x", "trunk", "")
	// A real (empty) store, so the calls that PASS the gate fail in the
	// write for want of the record rather than panicking on a nil
	// client — the gate is what this test is about, and it must be
	// possible to tell "refused" from "reached the write".
	dir := t.TempDir()
	if err := zitcli.Init(context.Background(), dir); err != nil {
		t.Fatal(err)
	}
	// trunk is protected and owned by mai alone; bob and mod hold `i`
	// but are not owners, and mod also holds the moderation letter that
	// used to be enough.
	deps := Deps{
		Repos:   NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(dir)}}),
		Protect: stubProtect{rules: []oltp.Protection{{Branch: "trunk", Owners: []string{"mai"}}}},
	}
	r := ops.NewRegistry()
	if err := registerLandingRetarget(r, deps); err != nil {
		t.Fatal(err)
	}
	move := func(ctx context.Context) error {
		_, err := r.Invoke(ctx, "landing.retarget",
			landingRetargetIn{Repo: "test", ID: landingHash[:12], To: "release"})
		return err
	}
	// A developer who did not open it and could not accept it onto
	// trunk: refused, and told whose it is and whose act moving it is.
	err := move(as("bob", "i"))
	if !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a passing developer moved somebody else's record: %v", err)
	} else if !strings.Contains(err.Error(), "ada") || !strings.Contains(err.Error(), "trunk") {
		t.Errorf("FAIL: the refusal does not name the author and the target: %v", err)
	}
	// The moderation letters are not the accept letter: a moderator who
	// does not own trunk may not aim a merge at it either.
	if err := move(as("mod", "i45")); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: a moderator who cannot accept onto trunk moved the record: %v", err)
	}
	// Anonymous is nobody, not everybody — an empty author must not
	// match an empty record author.
	if err := move(ops.WithCaps(context.Background(), oltp.CapSet("i"))); !errors.Is(err, ops.ErrForbidden) {
		t.Errorf("FAIL: an anonymous caller moved a record: %v", err)
	}
	// The author, and the owner who could accept it onto trunk, reach
	// the write — which fails here for want of a store, and that is past
	// the gate this test is about.
	for _, ctx := range []context.Context{as("ada", "i"), as("mai", "i")} {
		if err := move(ctx); errors.Is(err, ops.ErrForbidden) {
			t.Errorf("FAIL: %s was refused the gate: %v", ops.User(ctx), err)
		}
	}
	t.Log("ok: the author or a maintainer moves a record, nobody else")
}