@@ -225,6 +225,126 @@ describe("OAuth Authorization", () => {
225
225
} ) ;
226
226
} ) ;
227
227
228
+ it ( "falls back to root discovery when path-aware discovery returns 404" , async ( ) => {
229
+ // First call (path-aware) returns 404
230
+ mockFetch . mockResolvedValueOnce ( {
231
+ ok : false ,
232
+ status : 404 ,
233
+ } ) ;
234
+
235
+ // Second call (root fallback) succeeds
236
+ mockFetch . mockResolvedValueOnce ( {
237
+ ok : true ,
238
+ status : 200 ,
239
+ json : async ( ) => validMetadata ,
240
+ } ) ;
241
+
242
+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
243
+ expect ( metadata ) . toEqual ( validMetadata ) ;
244
+
245
+ const calls = mockFetch . mock . calls ;
246
+ expect ( calls . length ) . toBe ( 2 ) ;
247
+
248
+ // First call should be path-aware
249
+ const [ firstUrl , firstOptions ] = calls [ 0 ] ;
250
+ expect ( firstUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server/path/name" ) ;
251
+ expect ( firstOptions . headers ) . toEqual ( {
252
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
253
+ } ) ;
254
+
255
+ // Second call should be root fallback
256
+ const [ secondUrl , secondOptions ] = calls [ 1 ] ;
257
+ expect ( secondUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
258
+ expect ( secondOptions . headers ) . toEqual ( {
259
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
260
+ } ) ;
261
+ } ) ;
262
+
263
+ it ( "returns undefined when both path-aware and root discovery return 404" , async ( ) => {
264
+ // First call (path-aware) returns 404
265
+ mockFetch . mockResolvedValueOnce ( {
266
+ ok : false ,
267
+ status : 404 ,
268
+ } ) ;
269
+
270
+ // Second call (root fallback) also returns 404
271
+ mockFetch . mockResolvedValueOnce ( {
272
+ ok : false ,
273
+ status : 404 ,
274
+ } ) ;
275
+
276
+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/path/name" ) ;
277
+ expect ( metadata ) . toBeUndefined ( ) ;
278
+
279
+ const calls = mockFetch . mock . calls ;
280
+ expect ( calls . length ) . toBe ( 2 ) ;
281
+ } ) ;
282
+
283
+ it ( "does not fallback when the original URL is already at root path" , async ( ) => {
284
+ // First call (path-aware for root) returns 404
285
+ mockFetch . mockResolvedValueOnce ( {
286
+ ok : false ,
287
+ status : 404 ,
288
+ } ) ;
289
+
290
+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/" ) ;
291
+ expect ( metadata ) . toBeUndefined ( ) ;
292
+
293
+ const calls = mockFetch . mock . calls ;
294
+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
295
+
296
+ const [ url ] = calls [ 0 ] ;
297
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
298
+ } ) ;
299
+
300
+ it ( "does not fallback when the original URL has no path" , async ( ) => {
301
+ // First call (path-aware for no path) returns 404
302
+ mockFetch . mockResolvedValueOnce ( {
303
+ ok : false ,
304
+ status : 404 ,
305
+ } ) ;
306
+
307
+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com" ) ;
308
+ expect ( metadata ) . toBeUndefined ( ) ;
309
+
310
+ const calls = mockFetch . mock . calls ;
311
+ expect ( calls . length ) . toBe ( 1 ) ; // Should not attempt fallback
312
+
313
+ const [ url ] = calls [ 0 ] ;
314
+ expect ( url . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
315
+ } ) ;
316
+
317
+ it ( "falls back when path-aware discovery encounters CORS error" , async ( ) => {
318
+ // First call (path-aware) fails with TypeError (CORS)
319
+ mockFetch . mockImplementationOnce ( ( ) => Promise . reject ( new TypeError ( "CORS error" ) ) ) ;
320
+
321
+ // Retry path-aware without headers (simulating CORS retry)
322
+ mockFetch . mockResolvedValueOnce ( {
323
+ ok : false ,
324
+ status : 404 ,
325
+ } ) ;
326
+
327
+ // Second call (root fallback) succeeds
328
+ mockFetch . mockResolvedValueOnce ( {
329
+ ok : true ,
330
+ status : 200 ,
331
+ json : async ( ) => validMetadata ,
332
+ } ) ;
333
+
334
+ const metadata = await discoverOAuthMetadata ( "https://auth.example.com/deep/path" ) ;
335
+ expect ( metadata ) . toEqual ( validMetadata ) ;
336
+
337
+ const calls = mockFetch . mock . calls ;
338
+ expect ( calls . length ) . toBe ( 3 ) ;
339
+
340
+ // Final call should be root fallback
341
+ const [ lastUrl , lastOptions ] = calls [ 2 ] ;
342
+ expect ( lastUrl . toString ( ) ) . toBe ( "https://auth.example.com/.well-known/oauth-authorization-server" ) ;
343
+ expect ( lastOptions . headers ) . toEqual ( {
344
+ "MCP-Protocol-Version" : LATEST_PROTOCOL_VERSION
345
+ } ) ;
346
+ } ) ;
347
+
228
348
it ( "returns metadata when first fetch fails but second without MCP header succeeds" , async ( ) => {
229
349
// Set up a counter to control behavior
230
350
let callCount = 0 ;
0 commit comments