Skip to content
quarry Artifact [f191fdbf24]
Login

Artifact [f191fdbf24]

Artifact f191fdbf243fee7c0abd4c45100107c3e074173c3a8003f48f27a950fdc0db97:


package web

import (
	"io"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

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

type stubCaps struct{ letters string }

func (s stubCaps) CapsOf(string, string) CapSet { return s }
func (s stubCaps) HasAny(letters string) bool   { return strings.ContainsAny(letters, s.letters) }

// stubPolicy protects trunk for alice; dan holds `i` and owns nothing.
type stubPolicy struct{ owner string }

func (p stubPolicy) MayJoin(_, user, branch string) bool { return branch != "trunk" || user == p.owner }
func (p stubPolicy) Protections(string) ([]oltp.Protection, error) {
	return []oltp.Protection{{Branch: "trunk", Owners: []string{p.owner}}}, nil
}

func protectedLandingServer(t *testing.T, ix *index.Index, owner string) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos:   api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}}),
		Protect: stubPolicy{owner: owner}}); 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
}

func asDan(t *testing.T, srv *httptest.Server, path string) string {
	t.Helper()
	req, _ := http.NewRequest("GET", srv.URL+path, nil)
	req.AddCookie(&http.Cookie{Name: sessionCookie, Value: "good"})
	resp, err := srv.Client().Do(req)
	if err != nil {
		t.Fatal(err)
	}
	defer resp.Body.Close()
	b, _ := io.ReadAll(resp.Body)
	return string(b)
}

// RFC 0007 §Testing, "non-owner with i / forge accept / control
// disabled". A control that would fail is not offered
// (docs/landings.md); one that names why is better than none — and it
// says forge policy, never a capability.
func TestProtectedTargetDisablesAcceptWithTheReason(t *testing.T) {
	srv := protectedLandingServer(t, landingIndex(t, false), "alice")
	html := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(html, `disabled title="trunk is protected; owners: alice"`) {
		t.Fatalf("no disabled control with the reason:\n%.800s", html)
	}
	if strings.Contains(html, `formaction="/r/alpha/landing/a1a1a1a1a1a1/accept"`) {
		t.Error("an enabled accept was drawn for a non-owner")
	}
	if !strings.Contains(html, "is protected on this forge") {
		t.Error("the page does not say this is the forge's policy")
	}
}

// ...and for the owner, protection is not the reason for anything.
// Whether the control is offered is mergeability's call (unknowable
// without a store behind the index, and unknowable must not read as
// clean); what this proves is that an owner is never shown the
// non-owner's refusal.
func TestOwnerIsNotRefused(t *testing.T) {
	srv := protectedLandingServer(t, landingIndex(t, false), "dan")
	html := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if strings.Contains(html, "is protected on this forge") ||
		strings.Contains(html, `title="trunk is protected`) {
		t.Fatalf("the owner is shown the non-owner's reason:\n%.800s", html)
	}
}

// A conflicting rung gets the steps, not a sentence: the reader here has
// a conflict and a clone. And it says the rungs above follow on their
// own — merge-based rungs propagate a fix upward by the DAG, which is
// what other tools need a restack for.
func TestConflictingRungShowsTheStepsAndThatRungsAboveFollow(t *testing.T) {
	for _, want := range []string{"zit merge {{.Target}}", "zit push", "Rungs stacked on"} {
		if !strings.Contains(landingSource, want) {
			t.Errorf("landing page source lacks %q", want)
		}
	}
}

// zit#152 on the page: Update from <target> is offered only when the
// probe says the merge is clean AND the target has actually moved
// (RFC 0009: a verb that would do nothing is not offered; the card's
// "has moved" row says why this one would).
func TestUpdateControlFollowsTheProbe(t *testing.T) {
	if !strings.Contains(landingSource, `{{if and (eq $.Landing.Mergeable "clean") (gt $.Landing.Behind 0) $.Landing.CanAccept (eq $.Landing.State "open")}}`) {
		t.Fatal("the update control is not gated on a clean probe and a moved target")
	}
	if !strings.Contains(landingSource, `/update" title="merge {{$.Landing.Target}} into {{$.Landing.Branch}} on the store`) {
		t.Error("the update control is missing or aimed elsewhere")
	}
	if !strings.Contains(landingSource, "brought the target in") {
		t.Error("an update reply is not lifted into the record's events")
	}
}

// A verifier makes capability checks real. Without one, every check
// answers yes — which is how a reviewer with i but not 3 lost the
// accept control behind the composer's gate, unseen by these tests.
func verifiedLandingServer(t *testing.T, ix *index.Index, letters string, signedIn bool) *httptest.Server {
	t.Helper()
	r := ops.NewRegistry()
	if err := api.RegisterAll(r, api.Deps{Version: "v",
		Repos: api.NewRepos(map[string]*api.Repo{"alpha": {Index: ix}})}); err != nil {
		t.Fatal(err)
	}
	user := "dan"
	if !signedIn {
		user = ""
	}
	srv := httptest.NewServer(HandlerWith(r, nil, nil,
		[]Option{WithSessions(fakeSessions{user: user}), WithCaps(stubCaps{letters: letters})},
		Verify(func(string, string, string, ops.Auth) api.Decision { return api.Denied })))
	t.Cleanup(srv.Close)
	return srv
}

func TestReviewerWhoMayLandButNotPostStillGetsTheControl(t *testing.T) {
	srv := verifiedLandingServer(t, landingIndex(t, false), "i", true)
	html := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if !strings.Contains(html, `formaction="/r/alpha/landing/a1a1a1a1a1a1/accept"`) {
		t.Fatalf("i without 3: no accept control:\n%.900s", html)
	}
	if strings.Contains(html, `id="composer"`) {
		t.Error("i without 3: the composer was drawn")
	}
	if strings.Contains(html, "Sign in to take part") {
		t.Error("a signed-in reviewer was told to sign in")
	}
	if !strings.Contains(html, "Remarks and rejection need the forum capability") {
		t.Error("the missing capability is not named")
	}
}

func TestSignedInReaderWithoutCapabilitiesIsToldWhatIsMissing(t *testing.T) {
	srv := verifiedLandingServer(t, landingIndex(t, false), "", true)
	html := asDan(t, srv, "/r/alpha/landing/"+strings.Repeat("a1", 6))
	if strings.Contains(html, "Sign in to take part") || !strings.Contains(html, "Taking part needs the forum capability") {
		t.Errorf("signed-in reader: wrong message:\n%.600s", html)
	}
	anon := verifiedLandingServer(t, landingIndex(t, false), "", false)
	if html := asDan(t, anon, "/r/alpha/landing/"+strings.Repeat("a1", 6)); !strings.Contains(html, "Sign in to take part") {
		t.Errorf("anonymous: not told to sign in:\n%.600s", html)
	}
}