Files
summercms/modules/pier/widening_test.go
Jakub Zych ccf77b9103 test(19.7-09): named tests for every framework and admin SPA fix of plans 06 and 07
- pier: TestSessionReserve (A-WR-03), TestLockScope (A-WR-07),
  TestScopeWidening (A-WR-04), TestStatelessFanOut (A-WR-05, A-WR-06),
  TestPersonalTokenBinding (A-WR-09), TestDispatchHardening (A-WR-01,
  A-WR-02, A-WR-08, A-IN-04, A-IN-05, A-IN-08), TestCatalogPathFields
  (A-WR-02, A-IN-07), TestPromptWriteCeiling (C-IN-01),
  TestTokenScopedRevoke (B-WR-04, C-IN-02), TestResolveClientAddress
  (B-IN-04); test seam WrapStore
- pact TestFrontendTokenScopes, towel TestClientIP, cabana
  TestTokenScopeNameParity
- cabana TestAccountContributionURL (B-WR-03), TestAccountContributionsIsolation
  (B-WR-06, B-IN-03), TestRecordActionPromptLocalized (B-IN-02), and the
  Postgres TestAccountTokenRevokeEvent (B-WR-04) and
  TestResolveBackendTokenRemote (B-IN-04)
- vitest: hotkeysEditing.test.ts (B-WR-01, B-WR-02) and the extended
  AccountContributions.test.ts (B-WR-03, B-WR-06, B-IN-01)
- check-phase19.7.sh requires every name (GO_TESTS, GO_TAGGED_TESTS,
  DOCKER_TESTS, ADMIN_FILES) and scans the new files (PHASE_TEST_FILES)
2026-10-23 18:36:16 +02:00

94 lines
3.4 KiB
Go

package pier_test
import (
"context"
"net/http"
"slices"
"testing"
"time"
"git.golem15.com/golem15/summercms/modules/pact"
)
// setGrant replaces (or adds) the grant a fake verifier answers for raw.
func (v *fakeVerifier) setGrant(raw string, g pact.MCPGrant) {
v.mu.Lock()
v.grants[raw] = g
v.mu.Unlock()
}
// TestScopeWidening pins plan 06's re-filtering of a returning binding
// (A-WR-04, P-26): a binding first seen with read and then with read write
// lists the write tool on its next request, on the stateless and on the
// stateful path; a backend subject added to the binding (the grant gains
// backend, the user is linked) adds the backend tools without any fan-out
// event; and a narrowed grant hides the tools again.
func TestScopeWidening(t *testing.T) {
links := newFakeLinks()
links.set(11, 42)
verifier := newFakeVerifier(nil)
front := &fakeGuard{}
back := &fakeGuard{name: "backend", tokenMW: "backend"}
acme := routePlugin{id: "test.acme",
routes: []testRoute{guarded(http.MethodPost, "/acme/echo/{id}"), guarded(http.MethodGet, "/acme/items/{id}")},
tools: []pact.MCPTool{
{Name: "echo", Method: "POST", Path: "/acme/echo/{id}", Input: echoInput{}},
{Name: "show", Method: "GET", Path: "/acme/items/{id}", Input: showInput{}},
}}
office := routePlugin{id: "test.office", guard: "backend", middleware: "backend",
routes: []testRoute{{method: http.MethodGet, path: "/office/posts", mw: []string{"backend"}, h: noContent}},
tools: []pact.MCPTool{{Name: "posts", Method: "GET", Path: "/office/posts", Input: struct{}{}}}}
in := startInstance(t, newApp(t, appSpec{
mcp: "enabled: false\nsessions:\n store: memory\n",
guards: []pact.MCPGuard{front, back},
verifier: verifier,
links: links,
}), acme, office)
t.Cleanup(in.pier().StopReaper)
readOnly := []string{"acme_show"}
readWrite := []string{"acme_echo", "acme_show"}
withBackend := []string{"acme_echo", "acme_show", "office_posts"}
for _, path := range []struct {
name string
version string
token string
client string
}{
{"stateless", "newest", "oas_at_wide_stateless", "c5"},
{"stateful", "", "oas_at_wide_stateful", "c6"},
} {
t.Run(path.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
verifier.setGrant(path.token, grant(11, path.client, 11, "read"))
cs := sdkClient(t, ctx, in.url(), path.token, clientOpts{version: path.version})
if (cs.ID() == "") != (path.name == "stateless") {
t.Fatalf("%s client session id %q", path.name, cs.ID())
}
if got := listNames(t, ctx, cs); !slices.Equal(got, readOnly) {
t.Fatalf("read grant lists %v", got)
}
verifier.setGrant(path.token, grant(11, path.client, 11, "read", "write"))
if got := listNames(t, ctx, cs); !slices.Equal(got, readWrite) {
t.Fatalf("after widening to read write: %v", got)
}
// The same binding returns with the backend scope; the user is
// linked, so a backend subject joins the identity. No event is
// fired: the request itself re-filters the cached server.
verifier.setGrant(path.token, grant(11, path.client, 11, "read", "write", "backend"))
if got := listNames(t, ctx, cs); !slices.Equal(got, withBackend) {
t.Fatalf("after adding the backend subject: %v", got)
}
verifier.setGrant(path.token, grant(11, path.client, 11, "read"))
if got := listNames(t, ctx, cs); !slices.Equal(got, readOnly) {
t.Fatalf("after narrowing to read: %v", got)
}
})
}
}