Files
summercms/modules/wristband/mcp_scopes_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

81 lines
3.0 KiB
Go

package wristband
import (
"encoding/json"
"net/http"
"net/http/httptest"
"slices"
"testing"
)
// TestMCPScopes (D-21, the framework side of the backend scope): the MCP
// flavour's authorize allow-list is exactly Options.ScopesSupported. The
// default server refuses backend; a server that lists it accepts "read
// backend" and advertises it in its metadata; on either, openid and a scope
// nobody lists stay refused.
func TestMCPScopes(t *testing.T) {
_, challenge := s256Pair(t)
authorize := func(t *testing.T, s *Server, scope string) map[string]string {
t.Helper()
rec := httptest.NewRecorder()
s.Authorize(rec, authorizeRequest(map[string]string{
"client_id": "cli-scopes", "redirect_uri": authorizeTestRedirect, "response_type": "code",
"code_challenge": challenge, "code_challenge_method": "S256", "scope": scope, "state": "s",
}))
if rec.Code != http.StatusFound {
t.Fatalf("%q: status = %d, want 302", scope, rec.Code)
}
return queryOf(t, rec.Header().Get("Location"))
}
plain, plainBackend := newAuthorizeTestServer()
insertAuthorizeTestClient(plainBackend, "cli-scopes", []string{authorizeTestRedirect}, nil)
listing := DefaultOptions()
listing.Issuer = "https://issuer.example.test"
listing.Resource = "https://resource.example.test/mcp"
listing.ScopesSupported = append(listing.ScopesSupported, "backend")
withBackend := NewServer(listing)
backend := newMemoryBackend()
withBackend.SetBackend(backend)
insertAuthorizeTestClient(backend, "cli-scopes", []string{authorizeTestRedirect}, nil)
t.Run("default server refuses backend", func(t *testing.T) {
if q := authorize(t, plain, "read backend"); q["error"] != "invalid_scope" {
t.Fatalf("default server = %v", q)
}
if q := authorize(t, plain, "read write"); q["error"] != "" {
t.Fatalf("default server refused read write: %v", q)
}
})
t.Run("listing server accepts and advertises backend", func(t *testing.T) {
if q := authorize(t, withBackend, "read backend"); q["error"] != "" {
t.Fatalf("read backend = %v", q)
}
rec := httptest.NewRecorder()
withBackend.Metadata(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil))
var doc struct {
Scopes []string `json:"scopes_supported"`
}
if err := json.Unmarshal(rec.Body.Bytes(), &doc); err != nil || !slices.Contains(doc.Scopes, "backend") {
t.Fatalf("scopes_supported = %v (%v)", doc.Scopes, err)
}
rec = httptest.NewRecorder()
plain.Metadata(rec, httptest.NewRequest(http.MethodGet, "/.well-known/oauth-authorization-server", nil))
if err := json.Unmarshal(rec.Body.Bytes(), &doc); err != nil || slices.Contains(doc.Scopes, "backend") {
t.Fatalf("default scopes_supported = %v (%v)", doc.Scopes, err)
}
})
t.Run("openid and unlisted scopes stay refused", func(t *testing.T) {
for _, s := range []*Server{plain, withBackend} {
for _, scope := range []string{"openid backend", "read openid", "read admin"} {
if q := authorize(t, s, scope); q["error"] != "invalid_scope" {
t.Fatalf("%q = %v", scope, q)
}
}
}
})
}