@@ -80,6 +80,56 @@ describe('credential group OAuth callback', () => {
8080 )
8181 } )
8282
83+ it ( 'restores the exact focused option after a successful Search connection' , async ( ) => {
84+ mocks . consumeAttempt . mockResolvedValue ( { ...attempt , optionId : 'site-two' , returnTo : 'search' } )
85+ const response = await GET ( request ( 'state=state-1&code=code-1' ) , context )
86+ expect ( response . headers . get ( 'location' ) ) . toBe (
87+ '/credential-groups/enroll/invitation-token?optionId=site-two&returnTo=search&connected=site-two'
88+ )
89+ expect ( mocks . completeOAuth ) . toHaveBeenCalledWith (
90+ expect . objectContaining ( {
91+ principal,
92+ input : expect . objectContaining ( {
93+ attempt : expect . objectContaining ( { optionId : 'site-two' } ) ,
94+ } ) ,
95+ } )
96+ )
97+ } )
98+
99+ it . each ( [
100+ [ new CredentialGroupInvitationUnavailableError ( ) , 'unavailable' ] ,
101+ [ new CredentialGroupOAuthError ( 'Sign in with your own account' , 403 ) , 'account_mismatch' ] ,
102+ [ new CredentialGroupOAuthError ( 'Missing scopes' , 403 ) , 'permissions_required' ] ,
103+ [ new CredentialGroupOAuthError ( 'Changed settings' , 409 ) , 'configuration_changed' ] ,
104+ [ new Error ( 'Provider failed' ) , 'failed' ] ,
105+ ] ) ( 'retains Search focus after a rejected provider exchange: %s' , async ( error , status ) => {
106+ mocks . consumeAttempt . mockResolvedValue ( { ...attempt , returnTo : 'search' } )
107+ mocks . completeOAuth . mockRejectedValueOnce ( error )
108+ const response = await GET ( request ( 'state=state-1&code=code-1' ) , context )
109+ expect ( response . headers . get ( 'location' ) ) . toBe (
110+ `/credential-groups/enroll/invitation-token?optionId=option-1&returnTo=search&oauth=${ status } `
111+ )
112+ } )
113+
114+ it . each ( [ 'denied' , 'rate_limited' ] ) (
115+ 'retains Search focus without exchanging after %s' ,
116+ async ( status ) => {
117+ mocks . consumeAttempt . mockResolvedValue ( { ...attempt , returnTo : 'search' } )
118+ if ( status === 'rate_limited' )
119+ mocks . rateLimit . mockResolvedValue ( NextResponse . json ( { } , { status : 429 } ) )
120+ const response = await GET (
121+ request (
122+ status === 'denied' ? 'state=state-1&error=access_denied' : 'state=state-1&code=code-1'
123+ ) ,
124+ context
125+ )
126+ expect ( response . headers . get ( 'location' ) ) . toBe (
127+ `/credential-groups/enroll/invitation-token?optionId=option-1&returnTo=search&oauth=${ status } `
128+ )
129+ expect ( mocks . completeOAuth ) . not . toHaveBeenCalled ( )
130+ }
131+ )
132+
83133 it ( 'rejects standard providers on the custom callback route' , async ( ) => {
84134 const response = await GET (
85135 new NextRequest (
0 commit comments