package web
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/go-chi/chi/v5"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/vaughan0/go-ini"
"sourcecraft.dev/bigbes/sr-ht-ecore/grants"
"sourcecraft.dev/bigbes/sr-ht-spec/authn"
)
// mustGrants parses a grant string or fails the test.
func mustGrants(t *testing.T, s string) grants.Grants {
t.Helper()
g, err := grants.Parse(s)
require.NoError(t, err, "parse grants %q", s)
return g
}
// grantRouter mounts the read plane with a fixed principal injected into every
// request, bypassing token resolution: what put the principal there is the
// resolver's business, and this file is about what the handlers do with it.
func grantRouter(t *testing.T, p authn.Principal) http.Handler {
t.Helper()
srv, err := New(Options{
Conf: ini.File{
"sr.ht": ini.Section{
"network-key": testConf.Section("sr.ht")["network-key"],
"site-name": "sourcehut",
"environment": "development",
"owner-name": "bigbes",
},
"webhooks": ini.Section{"private-key": testConf.Section("webhooks")["private-key"]},
"spec.sr.ht": ini.Section{"origin": "https://spec.example"},
"meta.sr.ht": ini.Section{"origin": "https://meta.example"},
"tokens.sr.ht": ini.Section{"origin": "https://tokens.example"},
},
Reader: newFakeReader(),
Searcher: &fakeSearcher{},
Resolver: testResolver(t),
})
require.NoError(t, err)
r := chi.NewRouter()
r.Use(func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
next.ServeHTTP(w, req.WithContext(authn.WithPrincipal(req.Context(), p)))
})
})
srv.Register(r)
return r
}
func instancePrincipal(t *testing.T, grantString string) authn.Principal {
t.Helper()
return authn.Principal{
Kind: authn.KindAgent, Owner: "bigbes", Agent: "claude-code", Session: "s-1",
Plane: authn.PlaneInstance, Grants: mustGrants(t, grantString),
}
}
// Every read route asks for spec:read from a tokens.sr.ht working token, and
// asks nothing extra of the credentials that carry no grants. The refusal is a
// 403 and pointedly not a login redirect: the caller is already authenticated,
// so sending it to meta would loop it back with the same token.
func TestReadRoutesRequireTheReadGrant(t *testing.T) {
targets := []string{
"/~bigbes/rfcs",
"/~bigbes/rfcs/specs/0007-storage",
"/~bigbes/rfcs/specs/0007-storage.md",
"/~bigbes/rfcs/specs/0007-storage.json",
"/search?q=storage",
"/inbox",
}
t.Run("refused without it", func(t *testing.T) {
h := grantRouter(t, instancePrincipal(t, "spec:propose"))
for _, target := range targets {
t.Run(target, func(t *testing.T) {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
assert.Equal(t, http.StatusForbidden, rec.Code, "body: %s", rec.Body)
assert.Contains(t, rec.Body.String(), authn.ActionRead)
assert.Empty(t, rec.Header().Get("Location"),
"an authenticated caller must not be redirected to a login")
})
}
})
// Everything that may read: the owner's cookie, an agent a local process
// asserted (no credential, so no grant to read), and the instance tokens
// minted for reading. All served, no 403 anywhere.
for name, p := range map[string]authn.Principal{
"owner cookie": {Kind: authn.KindOwner, Owner: "bigbes", CookieUser: "bigbes"},
"locally asserted agent": {Kind: authn.KindAgent, Owner: "bigbes"},
"instance token, spec:read": instancePrincipal(t, "spec:read"),
"instance token, universal": instancePrincipal(t, "*"),
} {
t.Run("served for "+name, func(t *testing.T) {
h := grantRouter(t, p)
for _, target := range targets {
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
assert.Equal(t, http.StatusOK, rec.Code, "%s: body %s", target, rec.Body)
}
})
}
}
// An anonymous browser is still sent to meta's login and an anonymous bot still
// gets a 401: the grant check sits behind the identity one and does not change
// what happens when there is no identity at all.
func TestAnonymousDenialIsUnchanged(t *testing.T) {
h := grantRouter(t, authn.Anonymous())
rec := httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/~bigbes/rfcs/specs/0007-storage", nil))
assert.Equal(t, http.StatusFound, rec.Code)
assert.Contains(t, rec.Header().Get("Location"), "meta.example")
rec = httptest.NewRecorder()
h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/~bigbes/rfcs/specs/0007-storage.md", nil))
assert.Equal(t, http.StatusUnauthorized, rec.Code)
}