@@ -723,21 +723,23 @@ describe('StreamableHTTPClientTransport', () => {
723723 const authSpy = vi . spyOn ( authModule , 'auth' ) ;
724724 authSpy . mockResolvedValue ( 'AUTHORIZED' ) ;
725725
726- await transport . send ( message ) ;
727-
728- // Verify fetch was called twice
729- expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
726+ try {
727+ await transport . send ( message ) ;
730728
731- // Verify auth was called with the new scope
732- expect ( authSpy ) . toHaveBeenCalledWith (
733- mockAuthProvider ,
734- expect . objectContaining ( {
735- scope : 'new_scope' ,
736- resourceMetadataUrl : new URL ( 'http://example.com/resource' )
737- } )
738- ) ;
729+ // Verify fetch was called twice
730+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ;
739731
740- authSpy . mockRestore ( ) ;
732+ // Verify auth was called with the new scope
733+ expect ( authSpy ) . toHaveBeenCalledWith (
734+ mockAuthProvider ,
735+ expect . objectContaining ( {
736+ scope : 'new_scope' ,
737+ resourceMetadataUrl : new URL ( 'http://example.com/resource' )
738+ } )
739+ ) ;
740+ } finally {
741+ authSpy . mockRestore ( ) ;
742+ }
741743 } ) ;
742744
743745 it ( 'prevents infinite upscoping on repeated 403' , async ( ) => {
@@ -765,21 +767,23 @@ describe('StreamableHTTPClientTransport', () => {
765767 const authSpy = vi . spyOn ( authModule as typeof import ( '../../src/client/auth.js' ) , 'auth' ) ;
766768 authSpy . mockResolvedValue ( 'AUTHORIZED' ) ;
767769
768- // First send: should trigger upscoping
769- await expect ( transport . send ( message ) ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
770+ try {
771+ // First send: should trigger upscoping
772+ await expect ( transport . send ( message ) ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
770773
771- expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ; // Initial call + one retry after auth
772- expect ( authSpy ) . toHaveBeenCalledTimes ( 1 ) ; // Auth called once
774+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 2 ) ; // Initial call + one retry after auth
775+ expect ( authSpy ) . toHaveBeenCalledTimes ( 1 ) ; // Auth called once
773776
774- // Second send: should fail immediately without re-calling auth
775- fetchMock . mockClear ( ) ;
776- authSpy . mockClear ( ) ;
777- await expect ( transport . send ( message ) ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
777+ // Second send: should fail immediately without re-calling auth
778+ fetchMock . mockClear ( ) ;
779+ authSpy . mockClear ( ) ;
780+ await expect ( transport . send ( message ) ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
778781
779- expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ; // Only one fetch call
780- expect ( authSpy ) . not . toHaveBeenCalled ( ) ; // Auth not called again
781-
782- authSpy . mockRestore ( ) ;
782+ expect ( fetchMock ) . toHaveBeenCalledTimes ( 1 ) ; // Only one fetch call
783+ expect ( authSpy ) . not . toHaveBeenCalled ( ) ; // Auth not called again
784+ } finally {
785+ authSpy . mockRestore ( ) ;
786+ }
783787 } ) ;
784788
785789 describe ( 'mergeScopes behavior (via transport)' , ( ) => {
@@ -965,17 +969,60 @@ describe('StreamableHTTPClientTransport', () => {
965969 const authSpy = vi . spyOn ( authModule , 'auth' ) ;
966970 authSpy . mockResolvedValue ( 'AUTHORIZED' ) ;
967971
968- await expect (
969- transport . send ( {
970- jsonrpc : '2.0' ,
971- method : 'test' ,
972- params : { } ,
973- id : 'test-id'
972+ try {
973+ await expect (
974+ transport . send ( {
975+ jsonrpc : '2.0' ,
976+ method : 'test' ,
977+ params : { } ,
978+ id : 'test-id'
979+ } )
980+ ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
981+
982+ expect ( authSpy ) . toHaveBeenCalledTimes ( 1 ) ;
983+ } finally {
984+ authSpy . mockRestore ( ) ;
985+ }
986+ } ) ;
987+
988+ it ( 'preserves resource metadata URL across 401 responses without resource_metadata' , async ( ) => {
989+ const message : JSONRPCMessage = {
990+ jsonrpc : '2.0' ,
991+ method : 'test' ,
992+ params : { } ,
993+ id : 'test-id'
994+ } ;
995+
996+ const resourceMetadataUrl = 'http://example.com/.well-known/oauth-protected-resource' ;
997+ ( globalThis . fetch as Mock )
998+ . mockResolvedValueOnce ( {
999+ ok : false ,
1000+ status : 401 ,
1001+ statusText : 'Unauthorized' ,
1002+ headers : new Headers ( {
1003+ 'WWW-Authenticate' : `Bearer resource_metadata="${ resourceMetadataUrl } ", scope="read:op1"`
1004+ } ) ,
1005+ text : ( ) => Promise . resolve ( 'Unauthorized' )
9741006 } )
975- ) . rejects . toThrow ( 'Server returned 403 after trying upscoping' ) ;
1007+ . mockResolvedValueOnce ( {
1008+ ok : false ,
1009+ status : 401 ,
1010+ statusText : 'Unauthorized' ,
1011+ headers : new Headers ( {
1012+ 'WWW-Authenticate' : 'Bearer scope="read:op2"'
1013+ } ) ,
1014+ text : ( ) => Promise . resolve ( 'Unauthorized' )
1015+ } ) ;
1016+
1017+ await expect ( transport . send ( message ) ) . rejects . toThrow ( UnauthorizedError ) ;
1018+ expect ( ( transport as unknown as { _resourceMetadataUrl : URL | undefined } ) . _resourceMetadataUrl ) . toEqual (
1019+ new URL ( resourceMetadataUrl )
1020+ ) ;
9761021
977- expect ( authSpy ) . toHaveBeenCalledTimes ( 1 ) ;
978- authSpy . mockRestore ( ) ;
1022+ await expect ( transport . send ( message ) ) . rejects . toThrow ( UnauthorizedError ) ;
1023+ expect ( ( transport as unknown as { _resourceMetadataUrl : URL | undefined } ) . _resourceMetadataUrl ) . toEqual (
1024+ new URL ( resourceMetadataUrl )
1025+ ) ;
9791026 } ) ;
9801027
9811028 describe ( 'Reconnection Logic' , ( ) => {
0 commit comments