package wristband import ( "encoding/json" "net/http" "net/http/httptest" "slices" "testing" ) // TestMCPScopesSmoke covers the configurable authorize scope set of phase // 19.7 (RESEARCH Pitfall 5, D-21 framework side): the allow-list is exactly // Options.ScopesSupported, so a host that lists backend accepts it, while // the default server keeps refusing it. func TestMCPScopesSmoke(t *testing.T) { backend := newMemoryBackend() opts := DefaultOptions() opts.Issuer = "https://issuer.example.test" opts.Resource = "https://resource.example.test/mcp" opts.ScopesSupported = append(opts.ScopesSupported, "backend") srv := NewServer(opts) srv.SetBackend(backend) insertAuthorizeTestClient(backend, "cli-backend", []string{authorizeTestRedirect}, nil) _, challenge := s256Pair(t) authorize := func(s *Server, clientID string) map[string]string { rec := httptest.NewRecorder() s.Authorize(rec, authorizeRequest(map[string]string{ "client_id": clientID, "redirect_uri": authorizeTestRedirect, "response_type": "code", "code_challenge": challenge, "code_challenge_method": "S256", "scope": "read backend", })) if rec.Code != http.StatusFound { t.Fatalf("status = %d, want 302", rec.Code) } return queryOf(t, rec.Header().Get("Location")) } if q := authorize(srv, "cli-backend"); q["error"] != "" { t.Fatalf("host listing backend refused it: %v", q) } backend.mu.Lock() if n := len(backend.codes); n != 1 || !slices.Equal(backend.codes[0].Scopes, []string{"read", "backend"}) { backend.mu.Unlock() t.Fatalf("pending request scopes = %+v", backend.codes) } backend.mu.Unlock() rec := httptest.NewRecorder() srv.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 { t.Fatal(err) } if !slices.Equal(doc.Scopes, []string{"read", "write", "ai", "offline_access", "backend"}) { t.Fatalf("scopes_supported = %v", doc.Scopes) } // The default server (DefaultOptions scopes) still refuses backend. plain, plainBackend := newAuthorizeTestServer() insertAuthorizeTestClient(plainBackend, "cli-backend", []string{authorizeTestRedirect}, nil) q := authorize(plain, "cli-backend") if q["error"] != "invalid_scope" || q["error_description"] != "scope contains an unsupported value" { t.Fatalf("default server accepted backend: %v", q) } // An empty ScopesSupported keeps the PHP default allow-list. empty := DefaultOptions() empty.Issuer, empty.Resource, empty.ScopesSupported = opts.Issuer, opts.Resource, nil if got := NewServer(empty).allowedAuthorizeScopes(); !got["read"] || !got["offline_access"] || got["backend"] || len(got) != 4 { t.Fatalf("allowed scopes with empty ScopesSupported = %v", got) } }