package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"craftdesign.group/quarry/internal/oltp"
"craftdesign.group/quarry/internal/ops"
)
// A gated op must say what its letters are ABOUT.
//
// Four incidents this week share one shape: a gate resolved its subject
// to "" and asked anyway. The caps lookup answered confidently about the
// fleet, the caller got a 403 naming a capability and a grantor, and it
// read exactly like a policy decision — b683c54b sat open two days on
// that disguise. The subject is the thing nobody was declaring, so now
// it cannot be left undeclared.
func TestAGatedOpMustDeclareItsScope(t *testing.T) {
r := ops.NewRegistry()
err := ops.Register(r, ops.Op[struct{}, struct{}]{
Name: "nameless.scope", Caps: "a", Kind: ops.Command, Auth: ops.Token,
Route: "POST /api/v1/somewhere", Doc: "an op with letters and no subject",
Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
})
if err == nil {
t.Fatal("an op with Caps, no Scope and no {repo} in its route registered anyway: " +
"nothing forces the question to be answered")
}
for _, want := range []string{"ScopeRepo", "ScopeFleet", "ScopeCaller"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not offer %s, so it does not say how to fix it: %v", want, err)
}
}
}
// An op whose route names {repo} needs no declaration: the router always
// has a repository there, so the subject cannot go missing.
func TestARouteNamingTheRepoNeedsNoScope(t *testing.T) {
r := ops.NewRegistry()
if err := ops.Register(r, ops.Op[struct{}, struct{}]{
Name: "scoped.by.route", Caps: "a", Kind: ops.Query, Auth: ops.Token,
Route: "GET /api/v1/r/{repo}/thing", Doc: "the route carries the subject",
Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
}); err != nil {
t.Fatalf("an op whose route names {repo} was refused: %v", err)
}
}
// THE INCIDENT, as a test.
//
// A repo-scoped op reached with no repository must fail LOUDLY as a
// forge fault, not quietly as a capability refusal. The whole cost of
// b683c54b was that the wrong answer was indistinguishable from a right
// one: account.explain said the caller held "a" while repo.update said
// they did not, and both were reporting honestly about different
// subjects.
func TestARepoScopedOpWithNoRepositoryIsAForgeFault(t *testing.T) {
type in struct {
Name string `json:"name"`
}
r := ops.NewRegistry()
if err := ops.Register(r, ops.Op[in, struct{}]{
Name: "body.scoped", Caps: "a", Scope: ops.ScopeRepo, Kind: ops.Command, Auth: ops.Token,
Route: "POST /api/v1/things/update", Doc: "names its repository in the body",
Run: func(context.Context, in) (struct{}, error) { 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)
}
// A body that names no repository at all: the subject is missing.
body, _ := json.Marshal(in{})
req := httptest.NewRequest("POST", "/api/v1/things/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("answered 403: a missing subject was reported as a capability refusal, "+
"which is the disguise that cost two days: %s", w.Body.String())
}
if w.Code != http.StatusInternalServerError {
t.Fatalf("answered %d, want 500 — a subject the forge failed to resolve is the "+
"forge's fault: %s", w.Code, w.Body.String())
}
if !strings.Contains(w.Body.String(), "fault in the forge") {
t.Errorf("the message does not tell the reader it is not their credentials: %s", w.Body.String())
}
// And the ordinary case still works: subject present, caps held.
ok, _ := json.Marshal(in{Name: "sandbox"})
req2 := httptest.NewRequest("POST", "/api/v1/things/update", bytes.NewReader(ok))
req2.Header.Set("Content-Type", "application/json")
req2 = req2.WithContext(ops.WithUser(req2.Context(), "jva"))
w2 := httptest.NewRecorder()
h.ServeHTTP(w2, req2)
if w2.Code != http.StatusOK {
t.Errorf("a request that DOES name its repository was refused: %d %s", w2.Code, w2.Body.String())
}
}
// Fleet- and caller-scoped ops are untouched: they never had a
// repository and are not expected to grow one.
func TestFleetScopedOpsAreUnaffected(t *testing.T) {
r := ops.NewRegistry()
if err := ops.Register(r, ops.Op[struct{}, struct{}]{
Name: "fleetwide", Caps: "s", Scope: ops.ScopeFleet, Kind: ops.Query, Auth: ops.Token,
Route: "GET /api/v1/fleetwide", Doc: "about the forge",
Run: func(context.Context, struct{}) (struct{}, error) { return struct{}{}, nil },
}); err != nil {
t.Fatal(err)
}
h, err := RouterWithCaps(r, "test", func(_, _ string) CapSet { return oltp.CapSet("s") }, nil)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest("GET", "/api/v1/fleetwide", nil)
req = req.WithContext(ops.WithUser(req.Context(), "root"))
w := httptest.NewRecorder()
h.ServeHTTP(w, req)
if w.Code != http.StatusOK {
t.Errorf("a fleet-scoped op answered %d: %s", w.Code, w.Body.String())
}
}