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) } }) }