Files
summercms/modules/cabana/scope_limit_for_test.go
Jakub Zych f14eae96dd test(19.7-05): framework unit tests for every validation row and the gate's admin stage
- pier: TestCatalog, TestDispatch, TestGuardRules, TestSessions (Redis
  across two instances and the memory store), TestFanOut; the store clock
  and stateless-server seams in export_test.go
- surf: TestStreamingRoute (throttle, CORS profile and access-log pattern on
  a stream route) and TestPierMount in both builds; the streaming smoke
  restores the mount hook it replaces
- wristband TestMCPScopes; cabana TestAccountContributions (Postgres part:
  transaction and unchanged My account payloads), TestRecordActionPrompt,
  TestScopeLimitForMethods; TestRecordActionPromptRun pins the empty Value
  of an action without a prompt
- vitest: 403 and failed-load cases for My account contributions, the
  cancelled prompt
- gate: every name required (tagged runs included), account.go unchanged
  since 51d088a, admin stage live
2026-10-23 14:04:16 +02:00

50 lines
2.1 KiB
Go

package cabana
import (
"net/http"
"slices"
"testing"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
)
// TestScopeLimitForMethods (phase 19.7 D-22, the scope ceiling of an admin
// personal token on the MCP endpoint): ScopeLimitFor filters a token's
// stored scopes by the dispatched route's method exactly as the backend
// guard does for a request. A GET-only scope limits GET and leaves no scope
// for POST (known 1, method-allowed 0), "*" is full for every method, and
// scopes the registry no longer knows count as none (unauthenticated).
func TestScopeLimitForMethods(t *testing.T) {
reg, err := compileTokenCatalog([]party.Plugin{scopePlugin{
id: "acme.demo",
perms: []pact.Permission{{Code: "acme.demo.access", Roles: []string{"developer"}}},
scopes: []pact.TokenScope{{
Name: "demo-read", Label: "Demo", Description: "Reads the demo.",
Permissions: []string{"acme.demo.access"}, Methods: []string{"GET", "HEAD"},
}},
}})
if err != nil {
t.Fatal(err)
}
app := backpack.New(nil)
if err := app.Publish(&backendResolver{guard: &compositeGuard{reg: reg}}); err != nil {
t.Fatal(err)
}
limit, known, ok := ScopeLimitFor(app, []string{"demo-read"}, http.MethodGet)
if limit == nil || limit.Full || known != 1 || ok != 1 || len(limit.Scopes) != 1 || !slices.Equal(limit.Scopes[0], []string{"acme.demo.access"}) {
t.Fatalf("GET = %+v, %d, %d", limit, known, ok)
}
if limit, known, ok := ScopeLimitFor(app, []string{"demo-read"}, http.MethodPost); limit != nil || known != 1 || ok != 0 {
t.Fatalf("POST = %+v, %d, %d, want no scope for the method", limit, known, ok)
}
if limit, known, ok := ScopeLimitFor(app, []string{"*"}, http.MethodDelete); limit == nil || !limit.Full || known != 1 || ok != 1 {
t.Fatalf("* DELETE = %+v, %d, %d, want full", limit, known, ok)
}
if limit, known, ok := ScopeLimitFor(app, []string{"gone"}, http.MethodGet); limit != nil || known != 0 || ok != 0 {
t.Fatalf("unknown scope = %+v, %d, %d", limit, known, ok)
}
}