Skip to content
quarry Artifact [9feb371286]
Login

Artifact [9feb371286]

Artifact 9feb37128648f7f6fe795c848629b376be07cbf522703be9d2a2b241226a952a:


package api

import (
	"bytes"
	"context"
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"testing"

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

// bodyRepoIn is the shape of the ops that carry their repository as a
// FIELD rather than in the path — repos/update, accounts/grant.
type bodyRepoIn struct {
	Name        string `json:"name"`
	Description string `json:"description"`
}

// bodyRepoRouter mounts one such op, gated on "a", against a roster
// that grants "a" on exactly one repository and nothing at the fleet
// level — which is what a repository admin who is not a site admin
// actually holds.
func bodyRepoRouter(t *testing.T, seen *string) http.Handler {
	t.Helper()
	r := ops.NewRegistry()
	if err := ops.Register(r, ops.Op[bodyRepoIn, struct{}]{
		Name: "repo.update.test",
		Caps: "a",
		// Declared, now that a gated op must say what its letters are
		// about — and this fixture is precisely the case that needs it:
		// the repository is in the body, not the route.
		Scope: ops.ScopeRepo,
		Kind:  ops.Command,
		Auth:  ops.Token,
		Route: "POST /api/v1/repos/update",
		Doc:   "test double for the repos/* family, which names its repository in the body",
		Run: func(ctx context.Context, in bodyRepoIn) (struct{}, error) {
			// Proves the body still reaches the op: a gate that reads
			// the body must put it back.
			*seen = in.Description
			return struct{}{}, nil
		},
	}); err != nil {
		t.Fatal(err)
	}
	h, err := RouterWithCaps(r, "test", func(_, repo string) CapSet {
		if repo == "sandbox" {
			return oltp.CapSet("a")
		}
		return oltp.CapSet("")
	}, nil)
	if err != nil {
		t.Fatal(err)
	}
	return h
}

// A repository admin must reach an op that names their repository in a
// JSON body.
//
// The gate resolved the repository with req.ParseForm(), which parses
// FORM bodies only. Every API caller sends JSON, so the field came back
// empty, caps resolved against "" — the fleet scope, where a
// repository admin holds nothing — and the op refused its own admin
// with 'needs capability "a", which an admin of the repository grants'.
// account.explain, reading the same letters per repository, said the
// caller held "a" at the same moment. That contradiction is b683c54b.
func TestARepoAdminReachesAnOpThatNamesItsRepoInAJSONBody(t *testing.T) {
	var seen string
	h := bodyRepoRouter(t, &seen)

	body, _ := json.Marshal(bodyRepoIn{Name: "sandbox", Description: "a new description"})
	req := httptest.NewRequest("POST", "/api/v1/repos/update", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = req.WithContext(ops.WithUser(req.Context(), "jva"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)

	if w.Code == http.StatusForbidden {
		t.Fatalf("the repository's own admin was refused: %d %s\n"+
			"the gate could not see \"sandbox\" in a JSON body, so it asked about the fleet instead",
			w.Code, w.Body.String())
	}
	if w.Code != http.StatusOK {
		t.Fatalf("unexpected status %d: %s", w.Code, w.Body.String())
	}
	// The gate reads the body to find the repository; the op must still
	// get it. Consuming it would turn a fixed gate into an empty update.
	if seen != "a new description" {
		t.Errorf("the op received description %q — the gate consumed the body", seen)
	}
}

// The other direction, unchanged: a stranger to that repository is
// still refused, and the refusal still names the repository it asked
// about rather than an empty string.
func TestAStrangerToTheRepoIsStillRefused(t *testing.T) {
	var seen string
	h := bodyRepoRouter(t, &seen)

	body, _ := json.Marshal(bodyRepoIn{Name: "somebody-elses", Description: "nope"})
	req := httptest.NewRequest("POST", "/api/v1/repos/update", bytes.NewReader(body))
	req.Header.Set("Content-Type", "application/json")
	req = req.WithContext(ops.WithUser(req.Context(), "mallory"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)

	if w.Code != http.StatusForbidden {
		t.Fatalf("a stranger reached the op: %d %s", w.Code, w.Body.String())
	}
	if seen != "" {
		t.Error("a refused call still ran the op")
	}
	if !bytes.Contains(w.Body.Bytes(), []byte("somebody-elses")) {
		t.Errorf("the refusal does not name the repository it asked about: %s", w.Body.String())
	}
}

// A form-encoded body must keep working, since that is the path the
// current code was written for.
func TestAFormEncodedBodyStillResolvesTheRepo(t *testing.T) {
	var seen string
	h := bodyRepoRouter(t, &seen)

	req := httptest.NewRequest("POST", "/api/v1/repos/update",
		bytes.NewReader([]byte("name=sandbox&description=form+path")))
	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	req = req.WithContext(ops.WithUser(req.Context(), "jva"))
	w := httptest.NewRecorder()
	h.ServeHTTP(w, req)

	if w.Code == http.StatusForbidden {
		t.Fatalf("the form path regressed: %d %s", w.Code, w.Body.String())
	}
}