8
8
from pydantic import AnyHttpUrl
9
9
from starlette .applications import Starlette
10
10
11
- from mcp .server .auth .routes import create_protected_resource_routes
11
+ from mcp .server .auth .routes import build_resource_metadata_url , create_protected_resource_routes
12
12
13
13
14
14
@pytest .fixture
@@ -36,10 +36,11 @@ async def test_client(test_app: Starlette):
36
36
37
37
38
38
@pytest .mark .anyio
39
- async def test_metadata_endpoint (test_client : httpx .AsyncClient ):
40
- """Test the OAuth 2.0 Protected Resource metadata endpoint."""
39
+ async def test_metadata_endpoint_with_path (test_client : httpx .AsyncClient ):
40
+ """Test the OAuth 2.0 Protected Resource metadata endpoint for path-based resource ."""
41
41
42
- response = await test_client .get ("/.well-known/oauth-protected-resource" )
42
+ # For resource with path "/resource", metadata should be accessible at the path-aware location
43
+ response = await test_client .get ("/.well-known/oauth-protected-resource/resource" )
43
44
assert response .json () == snapshot (
44
45
{
45
46
"resource" : "https://example.com/resource" ,
@@ -50,3 +51,148 @@ async def test_metadata_endpoint(test_client: httpx.AsyncClient):
50
51
"bearer_methods_supported" : ["header" ],
51
52
}
52
53
)
54
+
55
+
56
+ @pytest .mark .anyio
57
+ async def test_metadata_endpoint_root_path_returns_404 (test_client : httpx .AsyncClient ):
58
+ """Test that root path returns 404 for path-based resource."""
59
+
60
+ # Root path should return 404 for path-based resources
61
+ response = await test_client .get ("/.well-known/oauth-protected-resource" )
62
+ assert response .status_code == 404
63
+
64
+
65
+ @pytest .fixture
66
+ def root_resource_app ():
67
+ """Fixture to create protected resource routes for root-level resource."""
68
+
69
+ # Create routes for a resource without path component
70
+ protected_resource_routes = create_protected_resource_routes (
71
+ resource_url = AnyHttpUrl ("https://example.com" ),
72
+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
73
+ scopes_supported = ["read" ],
74
+ resource_name = "Root Resource" ,
75
+ )
76
+
77
+ app = Starlette (routes = protected_resource_routes )
78
+ return app
79
+
80
+
81
+ @pytest .fixture
82
+ async def root_resource_client (root_resource_app : Starlette ):
83
+ """Fixture to create an HTTP client for the root resource app."""
84
+ async with httpx .AsyncClient (
85
+ transport = httpx .ASGITransport (app = root_resource_app ), base_url = "https://mcptest.com"
86
+ ) as client :
87
+ yield client
88
+
89
+
90
+ @pytest .mark .anyio
91
+ async def test_metadata_endpoint_without_path (root_resource_client : httpx .AsyncClient ):
92
+ """Test metadata endpoint for root-level resource."""
93
+
94
+ # For root resource, metadata should be at standard location
95
+ response = await root_resource_client .get ("/.well-known/oauth-protected-resource" )
96
+ assert response .status_code == 200
97
+ assert response .json () == snapshot (
98
+ {
99
+ "resource" : "https://example.com/" ,
100
+ "authorization_servers" : ["https://auth.example.com/" ],
101
+ "scopes_supported" : ["read" ],
102
+ "resource_name" : "Root Resource" ,
103
+ "bearer_methods_supported" : ["header" ],
104
+ }
105
+ )
106
+
107
+
108
+ class TestMetadataUrlConstruction :
109
+ """Test URL construction utility function."""
110
+
111
+ def test_url_without_path (self ):
112
+ """Test URL construction for resource without path component."""
113
+ resource_url = AnyHttpUrl ("https://example.com" )
114
+ result = build_resource_metadata_url (resource_url )
115
+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource"
116
+
117
+ def test_url_with_path_component (self ):
118
+ """Test URL construction for resource with path component."""
119
+ resource_url = AnyHttpUrl ("https://example.com/mcp" )
120
+ result = build_resource_metadata_url (resource_url )
121
+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource/mcp"
122
+
123
+ def test_url_with_trailing_slash_only (self ):
124
+ """Test URL construction for resource with trailing slash only."""
125
+ resource_url = AnyHttpUrl ("https://example.com/" )
126
+ result = build_resource_metadata_url (resource_url )
127
+ # Trailing slash should be treated as empty path
128
+ assert str (result ) == "https://example.com/.well-known/oauth-protected-resource"
129
+
130
+ @pytest .mark .parametrize (
131
+ "resource_url,expected_url" ,
132
+ [
133
+ ("https://example.com" , "https://example.com/.well-known/oauth-protected-resource" ),
134
+ ("https://example.com/" , "https://example.com/.well-known/oauth-protected-resource" ),
135
+ ("https://example.com/mcp" , "https://example.com/.well-known/oauth-protected-resource/mcp" ),
136
+ ("http://localhost:8001/mcp" , "http://localhost:8001/.well-known/oauth-protected-resource/mcp" ),
137
+ ],
138
+ )
139
+ def test_various_resource_configurations (self , resource_url : str , expected_url : str ):
140
+ """Test URL construction with various resource configurations."""
141
+ result = build_resource_metadata_url (AnyHttpUrl (resource_url ))
142
+ assert str (result ) == expected_url
143
+
144
+
145
+ class TestRouteConsistency :
146
+ """Test consistency between URL generation and route registration."""
147
+
148
+ def test_route_path_matches_metadata_url (self ):
149
+ """Test that route path matches the generated metadata URL."""
150
+ resource_url = AnyHttpUrl ("https://example.com/mcp" )
151
+
152
+ # Generate metadata URL
153
+ metadata_url = build_resource_metadata_url (resource_url )
154
+
155
+ # Create routes
156
+ routes = create_protected_resource_routes (
157
+ resource_url = resource_url ,
158
+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
159
+ )
160
+
161
+ # Extract path from metadata URL
162
+ from urllib .parse import urlparse
163
+
164
+ metadata_path = urlparse (str (metadata_url )).path
165
+
166
+ # Verify consistency
167
+ assert len (routes ) == 1
168
+ assert routes [0 ].path == metadata_path
169
+
170
+ @pytest .mark .parametrize (
171
+ "resource_url,expected_path" ,
172
+ [
173
+ ("https://example.com" , "/.well-known/oauth-protected-resource" ),
174
+ ("https://example.com/" , "/.well-known/oauth-protected-resource" ),
175
+ ("https://example.com/mcp" , "/.well-known/oauth-protected-resource/mcp" ),
176
+ ],
177
+ )
178
+ def test_consistent_paths_for_various_resources (self , resource_url : str , expected_path : str ):
179
+ """Test that URL generation and route creation are consistent."""
180
+ resource_url_obj = AnyHttpUrl (resource_url )
181
+
182
+ # Test URL generation
183
+ metadata_url = build_resource_metadata_url (resource_url_obj )
184
+ from urllib .parse import urlparse
185
+
186
+ url_path = urlparse (str (metadata_url )).path
187
+
188
+ # Test route creation
189
+ routes = create_protected_resource_routes (
190
+ resource_url = resource_url_obj ,
191
+ authorization_servers = [AnyHttpUrl ("https://auth.example.com" )],
192
+ )
193
+ route_path = routes [0 ].path
194
+
195
+ # Both should match expected path
196
+ assert url_path == expected_path
197
+ assert route_path == expected_path
198
+ assert url_path == route_path
0 commit comments