- 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)
66 lines
2.6 KiB
Go
66 lines
2.6 KiB
Go
package pier_test
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
)
|
|
|
|
// TestPersonalTokenBinding pins plan 06's personal-token binding rule
|
|
// (A-WR-09): a resolver answering a subject without a TokenID is refused
|
|
// with 401 at /mcp (it would put every user of the resolver on one
|
|
// binding), and two users' personal tokens get distinct bindings, so one
|
|
// cannot use or DELETE the other's session.
|
|
func TestPersonalTokenBinding(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
defer cancel()
|
|
personal := fakePersonalResolver{tokens: map[string]pact.MCPSubject{
|
|
"usr_no_token_id": {Guard: "acme", UserID: 31, Scopes: []string{"read", "write"}},
|
|
"usr_alice": {Guard: "acme", UserID: 32, TokenID: 501, Scopes: []string{"read", "write"}},
|
|
"usr_bob": {Guard: "acme", UserID: 33, TokenID: 502, Scopes: []string{"read", "write"}},
|
|
}}
|
|
in := startInstance(t, newApp(t, appSpec{
|
|
mcp: "enabled: false\nallow_personal_tokens: true\nsessions:\n store: memory\n",
|
|
guards: []pact.MCPGuard{&acmeGuard{}},
|
|
personal: personal,
|
|
}), acmePlugin{tools: []pact.MCPTool{echoTool()}})
|
|
t.Cleanup(in.pier().StopReaper)
|
|
|
|
t.Run("a subject without a TokenID is 401", func(t *testing.T) {
|
|
if r := rpc(t, ctx, in.url(), "usr_no_token_id", "", initializeBody); r.status != http.StatusUnauthorized {
|
|
t.Fatalf("initialize with a TokenID-less subject = %d %s", r.status, r.body)
|
|
}
|
|
if _, err := in.pier().Resolve(ctx, "usr_no_token_id"); err == nil {
|
|
t.Fatal("Resolve accepted a subject without a TokenID")
|
|
}
|
|
})
|
|
|
|
t.Run("two users get distinct bindings", func(t *testing.T) {
|
|
alice, err := in.pier().Resolve(ctx, "usr_alice")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bob, err := in.pier().Resolve(ctx, "usr_bob")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if alice.Binding != "pat:user:501" || bob.Binding != "pat:user:502" {
|
|
t.Fatalf("bindings = %q, %q", alice.Binding, bob.Binding)
|
|
}
|
|
sid := openSession(t, ctx, in, "usr_alice")
|
|
if r := rpc(t, ctx, in.url(), "usr_bob", sid, callBody); r.status != http.StatusForbidden || !strings.Contains(r.body, "session_binding") {
|
|
t.Fatalf("bob on alice's session = %d %s", r.status, r.body)
|
|
}
|
|
if code := deleteSession(t, ctx, in.url(), "usr_bob", sid); code != http.StatusForbidden {
|
|
t.Fatalf("bob's DELETE of alice's session = %d", code)
|
|
}
|
|
if r := rpc(t, ctx, in.url(), "usr_alice", sid, callBody); r.status != http.StatusOK || !strings.Contains(r.body, `\"principal\":32`) {
|
|
t.Fatalf("alice's session after bob's attempts = %d %s", r.status, r.body)
|
|
}
|
|
})
|
|
}
|