Skip to content
quarry Artifact [2962457bf9]
Login

Artifact [2962457bf9]

Artifact 2962457bf96a2d97a26b6d8579b0f00e2269528b62c20ae7a19fe5203d970732:


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 }

func (f fakeChecks) RequiredChecks(_, branch string) []string { return f.required[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 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)
	}
}