@@ -178,6 +178,174 @@ describe("OAuth Authorization", () => {
178178 await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
179179 . rejects . toThrow ( ) ;
180180 } ) ;
181+
182+ it ( "returns metadata when discovery succeeds with path" , async ( ) => {
183+ mockFetch . mockResolvedValueOnce ( {
184+ ok : true ,
185+ status : 200 ,
186+ json : async ( ) => validMetadata ,
187+ } ) ;
188+
189+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path/name" ) ;
190+ expect ( metadata ) . toEqual ( validMetadata ) ;
191+ const calls = mockFetch . mock . calls ;
192+ expect ( calls . length ) . toBe ( 1 ) ;
193+ const [ url ] = calls [ 0 ] ;
194+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource/path/name" ) ;
195+ } ) ;
196+
197+ it ( "preserves query parameters in path-aware discovery" , async ( ) => {
198+ mockFetch . mockResolvedValueOnce ( {
199+ ok : true ,
200+ status : 200 ,
201+ json : async ( ) => validMetadata ,
202+ } ) ;
203+
204+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path?param=value" ) ;
205+ expect ( metadata ) . toEqual ( validMetadata ) ;
206+ const calls = mockFetch . mock . calls ;
207+ expect ( calls . length ) . toBe ( 1 ) ;
208+ const [ url ] = calls [ 0 ] ;
209+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource/path?param=value" ) ;
210+ } ) ;
211+
212+ it ( "falls back to root discovery when path-aware discovery returns 404" , async ( ) => {
213+ // First call (path-aware) returns 404
214+ mockFetch . mockResolvedValueOnce ( {
215+ ok : false ,
216+ status : 404 ,
217+ } ) ;
218+
219+ // Second call (root fallback) succeeds
220+ mockFetch . mockResolvedValueOnce ( {
221+ ok : true ,
222+ status : 200 ,
223+ json : async ( ) => validMetadata ,
224+ } ) ;
225+
226+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path/name" ) ;
227+ expect ( metadata ) . toEqual ( validMetadata ) ;
228+
229+ const calls = mockFetch . mock . calls ;
230+ expect ( calls . length ) . toBe ( 2 ) ;
231+
232+ // First call should be path-aware
233+ const [ firstUrl , firstOptions ] = calls [ 0 ] ;
234+ expect ( firstUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource/path/name" ) ;
235+ expect ( firstOptions . headers ) . toEqual ( {
236+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
237+ } ) ;
238+
239+ // Second call should be root fallback
240+ const [ secondUrl , secondOptions ] = calls [ 1 ] ;
241+ expect ( secondUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
242+ expect ( secondOptions . headers ) . toEqual ( {
243+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
244+ } ) ;
245+ } ) ;
246+
247+ it ( "throws error when both path-aware and root discovery return 404" , async ( ) => {
248+ // First call (path-aware) returns 404
249+ mockFetch . mockResolvedValueOnce ( {
250+ ok : false ,
251+ status : 404 ,
252+ } ) ;
253+
254+ // Second call (root fallback) also returns 404
255+ mockFetch . mockResolvedValueOnce ( {
256+ ok : false ,
257+ status : 404 ,
258+ } ) ;
259+
260+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path/name" ) )
261+ . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
262+
263+ const calls = mockFetch . mock . calls ;
264+ expect ( calls . length ) . toBe ( 2 ) ;
265+ } ) ;
266+
267+ it ( "does not fallback when the original URL is already at root path" , async ( ) => {
268+ // First call (path-aware for root) returns 404
269+ mockFetch . mockResolvedValueOnce ( {
270+ ok : false ,
271+ status : 404 ,
272+ } ) ;
273+
274+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/" ) )
275+ . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
276+
277+ const calls = mockFetch . mock . calls ;
278+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
279+
280+ const [ url ] = calls [ 0 ] ;
281+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
282+ } ) ;
283+
284+ it ( "does not fallback when the original URL has no path" , async ( ) => {
285+ // First call (path-aware for no path) returns 404
286+ mockFetch . mockResolvedValueOnce ( {
287+ ok : false ,
288+ status : 404 ,
289+ } ) ;
290+
291+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com" ) )
292+ . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
293+
294+ const calls = mockFetch . mock . calls ;
295+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
296+
297+ const [ url ] = calls [ 0 ] ;
298+ expect ( url . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
299+ } ) ;
300+
301+ it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
302+ // First call (path-aware) fails with TypeError (CORS)
303+ mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
304+
305+ // Retry path-aware without headers (simulating CORS retry)
306+ mockFetch . mockResolvedValueOnce ( {
307+ ok : false ,
308+ status : 404 ,
309+ } ) ;
310+
311+ // Second call (root fallback) succeeds
312+ mockFetch . mockResolvedValueOnce ( {
313+ ok : true ,
314+ status : 200 ,
315+ json : async ( ) => validMetadata ,
316+ } ) ;
317+
318+ const metadata = await discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/deep/path" ) ;
319+ expect ( metadata ) . toEqual ( validMetadata ) ;
320+
321+ const calls = mockFetch . mock . calls ;
322+ expect ( calls . length ) . toBe ( 3 ) ;
323+
324+ // Final call should be root fallback
325+ const [ lastUrl , lastOptions ] = calls [ 2 ] ;
326+ expect ( lastUrl . toString ( ) ) . toBe ( "https://resource.example.com/.well-known/oauth-protected-resource" ) ;
327+ expect ( lastOptions . headers ) . toEqual ( {
328+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
329+ } ) ;
330+ } ) ;
331+
332+ it ( "does not fallback when resourceMetadataUrl is provided" , async ( ) => {
333+ // Call with explicit URL returns 404
334+ mockFetch . mockResolvedValueOnce ( {
335+ ok : false ,
336+ status : 404 ,
337+ } ) ;
338+
339+ await expect ( discoverOAuthProtectedResourceMetadata ( "https://resource.example.com/path" , {
340+ resourceMetadataUrl : "https://custom.example.com/metadata"
341+ } ) ) . rejects . toThrow ( "Resource server does not implement OAuth 2.0 Protected Resource Metadata." ) ;
342+
343+ const calls = mockFetch . mock . calls ;
344+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback when explicit URL is provided
345+
346+ const [ url ] = calls [ 0 ] ;
347+ expect ( url . toString ( ) ) . toBe ( "https://custom.example.com/metadata" ) ;
348+ } ) ;
181349 } ) ;
182350
183351 describe ( "discoverOAuthMetadata" , ( ) => {
0 commit comments