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)
}
}
}