@@ -17,6 +17,7 @@ global.fetch = mockFetch;
1717describe ( "OAuth Authorization" , ( ) => {
1818 beforeEach ( ( ) => {
1919 mockFetch . mockReset ( ) ;
20+ jest . clearAllMocks ( ) ;
2021 } ) ;
2122
2223 describe ( "extractResourceMetadataUrl" , ( ) => {
@@ -625,6 +626,7 @@ describe("OAuth Authorization", () => {
625626 } ) ;
626627 } ) ;
627628
629+
628630 describe ( "registerClient" , ( ) => {
629631 const validClientMetadata = {
630632 redirect_uris : [ "http://localhost:3000/callback" ] ,
@@ -712,6 +714,7 @@ describe("OAuth Authorization", () => {
712714 } ) ;
713715 } ) ;
714716
717+
715718 describe ( "auth function" , ( ) => {
716719 const mockProvider : OAuthClientProvider = {
717720 get redirectUrl ( ) { return "http://localhost:3000/callback" ; } ,
@@ -727,12 +730,79 @@ describe("OAuth Authorization", () => {
727730 redirectToAuthorization : jest . fn ( ) ,
728731 saveCodeVerifier : jest . fn ( ) ,
729732 codeVerifier : jest . fn ( ) ,
733+ saveClientInformation : jest . fn ( ) ,
734+ } ;
735+
736+ const validMetadata = {
737+ issuer : "https://auth.example.com" ,
738+ authorization_endpoint : "https://auth.example.com/authorize" ,
739+ token_endpoint : "https://auth.example.com/token" ,
740+ registration_endpoint : "https://auth.example.com/register" ,
741+ response_types_supported : [ "code" ] ,
742+ code_challenge_methods_supported : [ "S256" ] ,
743+ } ;
744+
745+ const validClientInfo = {
746+ client_id : "client123" ,
747+ client_secret : "secret123" ,
748+ redirect_uris : [ "http://localhost:3000/callback" ] ,
749+ client_name : "Test Client" ,
730750 } ;
731751
732752 beforeEach ( ( ) => {
733753 jest . clearAllMocks ( ) ;
734754 } ) ;
735755
756+
757+ it ( "uses scope from registered client information if not present in clientMetadata" , async ( ) => {
758+ // Mock fetch for metadata discovery and registration
759+ mockFetch
760+ . mockResolvedValueOnce ( {
761+ ok : true ,
762+ status : 200 ,
763+ json : async ( ) => ( { } ) ,
764+ } ) // protected resource metadata
765+ . mockResolvedValueOnce ( {
766+ ok : true ,
767+ status : 200 ,
768+ json : async ( ) => validMetadata ,
769+ } ) // discovery
770+ . mockResolvedValueOnce ( {
771+ ok : true ,
772+ status : 200 ,
773+ json : async ( ) => ( {
774+ ...validClientInfo ,
775+ scope : "dynamic scope from registration" ,
776+ } ) ,
777+ } ) ; // registration
778+
779+ // Provider: clientInformation returns undefined first, then fullInformation after registration
780+ const fullInformation = {
781+ ...validClientInfo ,
782+ scope : "dynamic scope from registration" ,
783+ } ;
784+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
785+ const clientInformationMock = jest
786+ . fn ( )
787+ . mockResolvedValueOnce ( undefined )
788+ . mockResolvedValueOnce ( fullInformation ) ;
789+
790+
791+
792+ await auth ( mockProvider , {
793+ serverUrl : "https://auth.example.com" ,
794+ } ) ;
795+
796+ // Check saveClientInformation was called with fullInformation
797+ expect ( mockProvider . saveClientInformation ) . toHaveBeenCalledWith ( fullInformation ) ;
798+
799+ // Check that redirectToAuthorization was called with a URL containing the correct scope from registration
800+ expect ( mockProvider . redirectToAuthorization ) . toHaveBeenCalledTimes ( 1 ) ;
801+ const urlArg = ( mockProvider . redirectToAuthorization as jest . Mock ) . mock . calls [ 0 ] [ 0 ] ;
802+ expect ( urlArg ) . toBeInstanceOf ( URL ) ;
803+ expect ( urlArg . searchParams . get ( "scope" ) ) . toBe ( "dynamic scope from registration" ) ;
804+ } ) ;
805+
736806 it ( "falls back to /.well-known/oauth-authorization-server when no protected-resource-metadata" , async ( ) => {
737807 // Setup: First call to protected resource metadata fails (404)
738808 // Second call to auth server metadata succeeds
0 commit comments