@@ -84,6 +84,41 @@ def oauth_provider_without_scope(oauth_provider: OAuthClientProvider) -> OAuthCl
84
84
oauth_provider .context .client_metadata .scope = None
85
85
return oauth_provider
86
86
87
+
88
+ @pytest .fixture
89
+ def oauth_metadata_response ():
90
+ """Common OAuth metadata response with scopes."""
91
+ return httpx .Response (
92
+ 200 ,
93
+ content = (
94
+ b'{"issuer": "https://auth.example.com", '
95
+ b'"authorization_endpoint": "https://auth.example.com/authorize", '
96
+ b'"token_endpoint": "https://auth.example.com/token", '
97
+ b'"registration_endpoint": "https://auth.example.com/register", '
98
+ b'"scopes_supported": ["read", "write", "admin"]}'
99
+ ),
100
+ )
101
+
102
+
103
+ @pytest .fixture
104
+ def prm_metadata ():
105
+ """PRM metadata with scopes."""
106
+ return ProtectedResourceMetadata (
107
+ resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
108
+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
109
+ scopes_supported = ["resource:read" , "resource:write" ],
110
+ )
111
+
112
+
113
+ @pytest .fixture
114
+ def prm_metadata_without_scopes ():
115
+ """PRM metadata without scopes."""
116
+ return ProtectedResourceMetadata (
117
+ resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
118
+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
119
+ scopes_supported = None ,
120
+ )
121
+
87
122
class TestPKCEParameters :
88
123
"""Test PKCE parameter generation."""
89
124
@@ -397,28 +432,15 @@ async def test_handle_metadata_response_success(self, oauth_provider: OAuthClien
397
432
assert str (oauth_provider .context .oauth_metadata .issuer ) == "https://auth.example.com/"
398
433
399
434
@pytest .mark .anyio
400
- async def test_prioritize_prm_scopes_over_oauth_metadata (self , oauth_provider_without_scope : OAuthClientProvider ):
435
+ async def test_prioritize_prm_scopes_over_oauth_metadata (
436
+ self , oauth_provider_without_scope : OAuthClientProvider ,
437
+ oauth_metadata_response : httpx .Response , prm_metadata : ProtectedResourceMetadata
438
+ ):
401
439
"""Test that PRM scopes are prioritized over auth server metadata scopes."""
402
440
provider = oauth_provider_without_scope
403
441
404
442
# Set up PRM metadata with specific scopes
405
- provider .context .protected_resource_metadata = ProtectedResourceMetadata (
406
- resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
407
- authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
408
- scopes_supported = ["resource:read" , "resource:write" ],
409
- )
410
-
411
- # Create OAuth metadata response with different scopes
412
- oauth_metadata_response = httpx .Response (
413
- 200 ,
414
- content = (
415
- b'{"issuer": "https://auth.example.com", '
416
- b'"authorization_endpoint": "https://auth.example.com/authorize", '
417
- b'"token_endpoint": "https://auth.example.com/token", '
418
- b'"registration_endpoint": "https://auth.example.com/register", '
419
- b'"scopes_supported": ["read", "write", "admin"]}'
420
- ),
421
- )
443
+ provider .context .protected_resource_metadata = prm_metadata
422
444
423
445
# Process the OAuth metadata
424
446
await provider ._handle_oauth_metadata_response (oauth_metadata_response )
@@ -428,29 +450,14 @@ async def test_prioritize_prm_scopes_over_oauth_metadata(self, oauth_provider_wi
428
450
429
451
@pytest .mark .anyio
430
452
async def test_fallback_to_oauth_metadata_scopes_when_no_prm_scopes (
431
- self , oauth_provider_without_scope : OAuthClientProvider
453
+ self , oauth_provider_without_scope : OAuthClientProvider ,
454
+ oauth_metadata_response : httpx .Response , prm_metadata_without_scopes : ProtectedResourceMetadata
432
455
):
433
456
"""Test fallback to OAuth metadata scopes when PRM has no scopes."""
434
457
provider = oauth_provider_without_scope
435
458
436
459
# Set up PRM metadata without scopes
437
- provider .context .protected_resource_metadata = ProtectedResourceMetadata (
438
- resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
439
- authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
440
- scopes_supported = None , # No scopes in PRM
441
- )
442
-
443
- # Create OAuth metadata response with scopes
444
- oauth_metadata_response = httpx .Response (
445
- 200 ,
446
- content = (
447
- b'{"issuer": "https://auth.example.com", '
448
- b'"authorization_endpoint": "https://auth.example.com/authorize", '
449
- b'"token_endpoint": "https://auth.example.com/token", '
450
- b'"registration_endpoint": "https://auth.example.com/register", '
451
- b'"scopes_supported": ["read", "write", "admin"]}'
452
- ),
453
- )
460
+ provider .context .protected_resource_metadata = prm_metadata_without_scopes
454
461
455
462
# Process the OAuth metadata
456
463
await provider ._handle_oauth_metadata_response (oauth_metadata_response )
@@ -459,19 +466,18 @@ async def test_fallback_to_oauth_metadata_scopes_when_no_prm_scopes(
459
466
assert provider .context .client_metadata .scope == "read write admin"
460
467
461
468
@pytest .mark .anyio
462
- async def test_no_scope_changes_when_both_missing (self , oauth_provider_without_scope : OAuthClientProvider ):
469
+ async def test_no_scope_changes_when_both_missing (
470
+ self , oauth_provider_without_scope : OAuthClientProvider ,
471
+ prm_metadata_without_scopes : ProtectedResourceMetadata
472
+ ):
463
473
"""Test that no scope changes occur when both PRM and OAuth metadata lack scopes."""
464
474
provider = oauth_provider_without_scope
465
475
466
476
# Set up PRM metadata without scopes
467
- provider .context .protected_resource_metadata = ProtectedResourceMetadata (
468
- resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
469
- authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
470
- scopes_supported = None , # No scopes in PRM
471
- )
477
+ provider .context .protected_resource_metadata = prm_metadata_without_scopes
472
478
473
479
# Create OAuth metadata response without scopes
474
- oauth_metadata_response = httpx .Response (
480
+ custom_oauth_metadata_response = httpx .Response (
475
481
200 ,
476
482
content = (
477
483
b'{"issuer": "https://auth.example.com", '
@@ -483,36 +489,21 @@ async def test_no_scope_changes_when_both_missing(self, oauth_provider_without_s
483
489
)
484
490
485
491
# Process the OAuth metadata
486
- await provider ._handle_oauth_metadata_response (oauth_metadata_response )
492
+ await provider ._handle_oauth_metadata_response (custom_oauth_metadata_response )
487
493
488
494
# Verify that scope remains None
489
495
assert provider .context .client_metadata .scope is None
490
496
491
497
@pytest .mark .anyio
492
498
async def test_preserve_existing_client_scope (
493
- self , oauth_provider : OAuthClientProvider
499
+ self , oauth_provider : OAuthClientProvider ,
500
+ oauth_metadata_response : httpx .Response , prm_metadata : ProtectedResourceMetadata
494
501
):
495
502
"""Test that existing client scope is preserved regardless of metadata."""
496
503
provider = oauth_provider
497
504
498
505
# Set up PRM metadata with scopes
499
- provider .context .protected_resource_metadata = ProtectedResourceMetadata (
500
- resource = AnyHttpUrl ("https://api.example.com/v1/mcp" ),
501
- authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
502
- scopes_supported = ["resource:read" , "resource:write" ],
503
- )
504
-
505
- # Create OAuth metadata response with scopes
506
- oauth_metadata_response = httpx .Response (
507
- 200 ,
508
- content = (
509
- b'{"issuer": "https://auth.example.com", '
510
- b'"authorization_endpoint": "https://auth.example.com/authorize", '
511
- b'"token_endpoint": "https://auth.example.com/token", '
512
- b'"registration_endpoint": "https://auth.example.com/register", '
513
- b'"scopes_supported": ["read", "write", "admin"]}'
514
- ),
515
- )
506
+ provider .context .protected_resource_metadata = prm_metadata
516
507
517
508
# Process the OAuth metadata
518
509
await provider ._handle_oauth_metadata_response (oauth_metadata_response )
0 commit comments