Skip to content
quarry Artifact [3e7d7e68f0]
Login

Artifact [3e7d7e68f0]

Artifact 3e7d7e68f08d8d2f4a82ad0403cdc36273bf60e2ef0ea5402c414963b7ff12eb:


package web

import (
	"context"
	"net/http"
	"net/http/httptest"
	"net/url"
	"path/filepath"
	"strings"
	"testing"

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

// fakeChecks answers RFC 0009's policy and rules from maps.
type fakeChecks struct {
	required map[string][]string
	premerge map[string]bool
}

func (f fakeChecks) RequiredChecks(_, branch string) []string { return f.required[branch] }
func (f fakeChecks) PremergeMode(_, branch string) bool       { return f.premerge[branch] }

// fakeCI serves runs for whatever check-in is asked.
type fakeCI struct{ runs []api.CIRun }

func (f fakeCI) CIRuns(string, int) ([]api.CIRun, error)           { return f.runs, nil }
func (f fakeCI) CIRunsFor(string, string) ([]api.CIRun, error)     { return f.runs, nil }
func (f fakeCI) CIRunsForPair(string, string) ([]api.CIRun, error) { return f.runs, nil }

func checksServer(t *testing.T, ix *index.Index, required map[string][]string, runs []api.CIRun, owner string) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos:  api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Checks: fakeChecks{required: required},
		CI:     fakeCI{runs: runs}}
	if owner != "" {
		deps.Protect = stubPolicy{owner: owner}
	}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}))
	t.Cleanup(srv.Close)
	return srv
}

// RFC 0009 §verdicts: the panel shows a row per run, required checks
// are chipped, and a required check with no run reads waiting.
func TestChecksPanelShowsRunsAndWaiting(t *testing.T) {
	ix := landingIndex(t, false)
	srv := checksServer(t, ix,
		map[string][]string{"trunk": {"test", "sign"}},
		[]api.CIRun{{RunID: "run-1", Workflow: "test", Pipeline: "ci/test.json", Status: "success", Started: "2026-08-28"}},
		"")
	body := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	for _, want := range []string{`id="l-checks"`, "ci/test.json", ">pass<", ">waiting<", "<code>sign</code>", `class="chip">required`} {
		if !strings.Contains(body, want) {
			t.Fatalf("panel lacks %q", want)
		}
	}
}

// A red required set closes the accept with the names in words; the
// override control appears only for who may use it.
func TestUnmetChecksCloseTheAccept(t *testing.T) {
	ix := landingIndex(t, false)
	srv := checksServer(t, ix,
		map[string][]string{"trunk": {"test"}},
		[]api.CIRun{{RunID: "run-1", Workflow: "test", Status: "failed"}},
		"")
	body := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(body, "required checks not green") || !strings.Contains(body, "<code>test</code>") {
		t.Fatal("the closed accept does not name the red check")
	}
	if strings.Contains(body, "Accept anyway") {
		t.Fatal("override offered to someone who may not use it")
	}
	// The op refuses too — the button is honesty, not the gate.
	resp := postAsDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6)+"/accept", url.Values{})
	if resp.StatusCode == http.StatusSeeOther {
		loc := resp.Header.Get("Location")
		if !strings.Contains(loc, "landing") {
			t.Fatalf("accept redirected oddly: %s", loc)
		}
	}
}

// A branch owner gets the override control, and the accept carries the
// stated reason.
func TestOwnerMayOverrideWithAReason(t *testing.T) {
	ix := landingIndex(t, false)
	srv := checksServer(t, ix,
		map[string][]string{"trunk": {"test"}},
		nil, "dan")
	body := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(body, "Accept anyway") || !strings.Contains(body, `name="override"`) {
		t.Fatal("owner does not get the override control")
	}
}

var _ = oltp.CheckRule{} // settings-side types live in oltp

// RFC 0009 §external-ci: a reporter token posts a state, the panel
// shows the row with its own link, and a green external check
// satisfies the required set.
func TestExternalCheckReportsAndSatisfies(t *testing.T) {
	ix := landingIndex(t, false)
	odb, err := oltp.Open(filepath.Join(t.TempDir(), "oltp.db"))
	if err != nil {
		t.Fatal(err)
	}
	defer odb.Close()
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos:    api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Checks:   fakeChecks{required: map[string][]string{"trunk": {"jenkins/test"}}},
		External: odb}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}))
	t.Cleanup(srv.Close)

	landing := "/r/alpha/landing/" + strings.Repeat("a1", 6)
	if body := asDan(t, srv, landing); !strings.Contains(body, "required checks not green") {
		t.Fatal("accept open before any report")
	}
	tok, err := odb.MintReporterToken("alpha", "jenkins")
	if err != nil {
		t.Fatal(err)
	}
	// The subject is the landing branch's tip; the fixture's branch has
	// one check-in — read it off the page's commits? Simpler: report on
	// every commit hash the landing lists by scraping is brittle; use
	// the index directly.
	cs, err := ix.BranchCommits("fossil-caps", 50)
	if err != nil || len(cs) == 0 {
		t.Fatalf("fixture branch: %v %v", cs, err)
	}
	tip := cs[len(cs)-1].Hash
	// The op is the contract; the HTTP route is the standard router's
	// wiring, covered where every other op's is.
	if _, err := invoke(context.Background(), r, "check.report", url.Values{
		"repo": {"alpha"}, "checkin": {tip}, "context": {"jenkins/test"},
		"state": {"success"}, "url": {"https://ci.example/build/9"}, "token": {tok}}); err != nil {
		t.Fatalf("report: %v", err)
	}
	body := asDan(t, srv, landing)
	for _, want := range []string{"jenkins/test", "https://ci.example/build/9", ">details<"} {
		if !strings.Contains(body, want) {
			t.Fatalf("panel lacks %q", want)
		}
	}
	if strings.Contains(body, "required checks not green") {
		t.Fatal("a green external check did not satisfy the required set")
	}
	// A wrong token is refused.
	if _, err := invoke(context.Background(), r, "check.report", url.Values{
		"repo": {"alpha"}, "checkin": {tip}, "context": {"jenkins/test"},
		"state": {"success"}, "token": {"qcr_wrong"}}); err == nil {
		t.Fatal("a wrong token reported")
	}
}

// The re-run button posts through ci.trigger.
func TestReRunButtonTriggers(t *testing.T) {
	ix := landingIndex(t, false)
	var got []string
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos:     api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		CITrigger: func(repo, checkin string) error { got = append(got, repo+"@"+checkin); return nil }}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}))
	t.Cleanup(srv.Close)
	resp := postAsDan(t, srv, "/r/alpha/ci/trigger", url.Values{"back": {"/r/alpha/landing/x"}})
	if resp.StatusCode != http.StatusSeeOther || len(got) != 1 || got[0] != "alpha@" {
		t.Fatalf("trigger: %d %v", resp.StatusCode, got)
	}
}

// RFC 0009 §premerge: in premerge mode the subject is the pair of
// tips — tip runs do not count, pair runs do, and the page says which
// tree the checks are about.
func TestPremergeModeKeysThePair(t *testing.T) {
	ix := landingIndex(t, false)
	tipRun := []api.CIRun{{RunID: "tip-1", Workflow: "test", Status: "success"}}
	r := ops.NewRegistry()
	deps := api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Checks: fakeChecks{required: map[string][]string{"trunk": {"test"}},
			premerge: map[string]bool{"trunk": true}},
		CI: pairAwareCI{tip: tipRun, pair: nil}}
	if err := api.RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}))
	t.Cleanup(srv.Close)
	body := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(body, "premerge: these checks test the MERGED tree") {
		t.Fatal("the page does not say the subject is the merge")
	}
	if !strings.Contains(body, "required checks not green") {
		t.Fatal("a green TIP run satisfied a PREMERGE requirement")
	}
	// The same server with a green pair run is satisfied.
	r2 := ops.NewRegistry()
	deps.CI = pairAwareCI{pair: []api.CIRun{{RunID: "pm-1", Workflow: "test", Status: "success"}}}
	if err := api.RegisterAll(r2, deps); err != nil {
		t.Fatal(err)
	}
	srv2 := httptest.NewServer(HandlerWith(r2, nil, nil,
		[]Option{WithSessions(fakeSessions{user: "dan"}), WithCaps(stubCaps{letters: "i"})}))
	t.Cleanup(srv2.Close)
	body2 := asDan(t, srv2, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if strings.Contains(body2, "required checks not green") {
		t.Fatal("a green pair run did not satisfy")
	}
}

// pairAwareCI serves different runs for tip and pair subjects.
type pairAwareCI struct{ tip, pair []api.CIRun }

func (f pairAwareCI) CIRuns(string, int) ([]api.CIRun, error)           { return nil, nil }
func (f pairAwareCI) CIRunsFor(string, string) ([]api.CIRun, error)     { return f.tip, nil }
func (f pairAwareCI) CIRunsForPair(string, string) ([]api.CIRun, error) { return f.pair, nil }

// RFC 0009: PairVerdict judges the pair against the required set —
// all green passes, a red required check fails, silence stays silent.
func TestPairVerdictJudgesThePair(t *testing.T) {
	deps := api.Deps{
		Repos:  api.NewRepos(map[string]*api.Repo{}),
		Checks: fakeChecks{required: map[string][]string{"trunk": {"test", "build"}}},
		CI: pairAwareCI{pair: []api.CIRun{
			{Workflow: "test", Status: "success"},
			{Workflow: "build", Status: "success"}}}}
	if v := deps.PairVerdict("alpha", "pair:a:b", "trunk"); v != "ci-pass" {
		t.Fatalf("all green: %q", v)
	}
	deps.CI = pairAwareCI{pair: []api.CIRun{
		{Workflow: "test", Status: "success"},
		{Workflow: "build", Status: "failed"}}}
	if v := deps.PairVerdict("alpha", "pair:a:b", "trunk"); v != "ci-fail" {
		t.Fatalf("red required: %q", v)
	}
	deps.CI = pairAwareCI{pair: []api.CIRun{{Workflow: "test", Status: "success"}}}
	if v := deps.PairVerdict("alpha", "pair:a:b", "trunk"); v != "" {
		t.Fatalf("half-run pair judged: %q", v)
	}
	deps.Checks = fakeChecks{}
	if v := deps.PairVerdict("alpha", "pair:a:b", "trunk"); v != "" {
		t.Fatalf("no required set judged: %q", v)
	}
}

// RFC 0009: the update verb renders only when it would do something —
// the card says how far the target has moved, and a branch that has
// seen everything is offered no no-op button.
func TestUpdateOffersOnlyWhenBehind(t *testing.T) {
	ix := landingIndex(t, false)
	srv := checksServer(t, ix, nil, nil, "")
	body := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if strings.Contains(body, "has moved") {
		t.Fatal("fixture branch is not behind, but the row says it is")
	}
	if strings.Contains(body, "/update") {
		t.Fatal("a no-op update was offered")
	}
}