@@ -10,16 +10,12 @@ import { oauthUtilsMock, oauthUtilsMockFns } from '@sim/testing/mocks/oauth-util
1010import { urlsMockFns } from '@sim/testing/mocks/urls.mock'
1111import type { NextRequest } from 'next/server'
1212import { afterAll , beforeEach , describe , expect , it , vi } from 'vitest'
13- import { ForbiddenOperationError } from '@/lib/core/application/forbidden'
14- import { OrchestrationError } from '@/lib/core/orchestration/types'
15- import { CredentialConnectionProviderMismatchError } from '@/lib/credentials/application/connection-target'
1613
1714const mocks = vi . hoisted ( ( ) => ( {
1815 betterAuthGET : vi . fn ( ) ,
1916 getSession : vi . fn ( ) ,
2017 linkAccount : vi . fn ( ) ,
2118 requireClient : vi . fn ( ) ,
22- createConnection : vi . fn ( ) ,
2319 launchConnection : vi . fn ( ) ,
2420 decryptQuickBooksClientConfig : vi . fn ( ) ,
2521 createQuickBooksState : vi . fn ( ) ,
@@ -46,12 +42,6 @@ vi.mock('@/lib/core/config/env-capabilities.server', () => ({
4642 execute : vi . fn ( ) ,
4743 } ) ,
4844} ) )
49- vi . mock ( '@/lib/credentials/application/create-credential-connection' , ( ) => ( {
50- createCredentialConnection : {
51- operation : { id : 'credentials.connections.create' } ,
52- execute : mocks . createConnection ,
53- } ,
54- } ) )
5545vi . mock ( '@/lib/credentials/application/launch-scoped-credential-connection' , ( ) => ( {
5646 launchScopedCredentialConnection : mocks . launchConnection ,
5747} ) )
@@ -97,13 +87,6 @@ describe('OAuth2 authorize route', () => {
9787 user : { id : 'user-1' } ,
9888 session : { id : 'session-1' } ,
9989 } )
100- mocks . createConnection . mockResolvedValue ( {
101- providerId : 'google-email' ,
102- workspaceId : WORKSPACE_ID ,
103- draftId : 'draft-1' ,
104- expiresAt : new Date ( '2026-08-14T12:00:00.000Z' ) ,
105- authorizationUrl : `${ BASE_URL } /api/auth/oauth2/authorize?draftId=draft-1` ,
106- } )
10790 mocks . launchConnection . mockResolvedValue ( {
10891 draft : {
10992 id : 'draft-1' ,
@@ -442,90 +425,51 @@ describe('OAuth2 authorize route', () => {
442425 expect ( mocks . betterAuthGET ) . not . toHaveBeenCalled ( )
443426 } )
444427
445- it ( 'refuses a cross-site workspace launch before creating a connection draft' , async ( ) => {
428+ it ( 'sends a workspace connect link to the confirmation page without starting it' , async ( ) => {
429+ const callbackURL = `${ BASE_URL } /workspace/${ WORKSPACE_ID } `
446430 const response = await GET (
447431 request (
448- { providerId : 'google-email' , workspaceId : WORKSPACE_ID , credentialId : 'credential-1' } ,
449- { 'sec-fetch-site' : 'cross-site' }
432+ {
433+ providerId : 'google-email' ,
434+ workspaceId : WORKSPACE_ID ,
435+ credentialId : 'credential-1' ,
436+ callbackURL,
437+ } ,
438+ { 'sec-fetch-site' : 'same-origin' }
450439 )
451440 )
452441
453- expect ( response . headers . get ( 'location' ) ) . toBe ( `${ BASE_URL } /home?error=oauth_link_invalid` )
454- expect ( mocks . createConnection ) . not . toHaveBeenCalled ( )
442+ const location = new URL ( response . headers . get ( 'location' ) ?? '' )
443+ expect ( `${ location . origin } ${ location . pathname } ` ) . toBe ( `${ BASE_URL } /oauth/connect` )
444+ expect ( Object . fromEntries ( location . searchParams ) ) . toEqual ( {
445+ providerId : 'google-email' ,
446+ workspaceId : WORKSPACE_ID ,
447+ credentialId : 'credential-1' ,
448+ callbackURL,
449+ } )
450+ expect ( mocks . launchConnection ) . not . toHaveBeenCalled ( )
455451 expect ( mocks . linkAccount ) . not . toHaveBeenCalled ( )
456452 } )
457453
458- it . each ( [ { 'sec-fetch-site' : 'none' } , { 'sec-fetch-site' : 'same-site' } ] ) (
459- 'starts a workspace launch that is not cross-site: %o' ,
460- async ( headers ) => {
461- const response = await GET (
462- request ( { providerId : 'google-email' , workspaceId : WORKSPACE_ID } , headers )
463- )
464-
465- expect ( mocks . createConnection ) . toHaveBeenCalledOnce ( )
466- expect ( response . headers . get ( 'location' ) ) . toBe ( 'https://provider.example/authorize' )
467- }
468- )
469-
470454 it ( 'starts a cross-site launch of the signed-in user’s own connection draft' , async ( ) => {
471455 const response = await GET ( request ( { draftId : 'draft-1' } , { 'sec-fetch-site' : 'cross-site' } ) )
472456
473457 expect ( mocks . launchConnection ) . toHaveBeenCalledOnce ( )
474458 expect ( response . headers . get ( 'location' ) ) . toBe ( 'https://provider.example/authorize' )
475459 } )
476460
477- it ( 'requires a configured OAuth client before creating a legacy draft' , async ( ) => {
478- mocks . requireClient . mockImplementationOnce ( ( ) => {
479- throw new Error ( 'OAuth client is not configured' )
480- } )
481-
482- const response = await GET ( request ( { providerId : 'google-email' , workspaceId : WORKSPACE_ID } ) )
483-
484- expect ( response . headers . get ( 'location' ) ) . toBe ( `${ BASE_URL } /home?error=oauth_link_failed` )
485- expect ( mocks . requireClient ) . toHaveBeenCalledWith ( 'google-email' )
486- expect ( mocks . createConnection ) . not . toHaveBeenCalled ( )
487- } )
488-
489- it ( 'maps a provider mismatch without exposing the credential' , async ( ) => {
490- mocks . createConnection . mockRejectedValue (
491- new CredentialConnectionProviderMismatchError ( 'google-email' , 'slack' )
492- )
493-
494- const response = await GET (
495- request ( {
496- providerId : 'slack' ,
497- workspaceId : WORKSPACE_ID ,
498- credentialId : 'credential-1' ,
499- } )
500- )
501-
502- expect ( response . headers . get ( 'location' ) ) . toBe (
503- `${ BASE_URL } /home?error=credential_provider_mismatch`
504- )
505- } )
506-
507- it ( 'maps credential and workspace authorization failures separately' , async ( ) => {
508- mocks . createConnection . mockRejectedValueOnce (
509- new ForbiddenOperationError (
510- 'CREDENTIAL_ADMIN_ACCESS_REQUIRED' ,
511- 'Credential admin permission required'
512- )
513- )
514- const credentialResponse = await GET (
515- request ( {
516- providerId : 'google-email' ,
517- workspaceId : WORKSPACE_ID ,
518- credentialId : 'credential-1' ,
519- } )
520- )
521- mocks . createConnection . mockRejectedValueOnce (
522- new OrchestrationError ( 'forbidden' , 'Write permission required' )
523- )
524- const workspaceResponse = await GET (
525- request ( { providerId : 'google-email' , workspaceId : WORKSPACE_ID } )
526- )
527-
528- expect ( credentialResponse . headers . get ( 'location' ) ) . toContain ( 'credential_access_denied' )
529- expect ( workspaceResponse . headers . get ( 'location' ) ) . toContain ( 'workspace_access_denied' )
530- } )
461+ it . each ( [
462+ {
463+ callbackURL : undefined ,
464+ errorCallbackURL : `${ BASE_URL } /oauth/credential-connected?result=failed` ,
465+ } ,
466+ { callbackURL : `${ BASE_URL } /oauth/chat-complete` , errorCallbackURL : undefined } ,
467+ ] ) (
468+ 'sends a failed draft launch to the status page only when no return page was named: %o' ,
469+ async ( { callbackURL, errorCallbackURL } ) => {
470+ await GET ( request ( { draftId : 'draft-1' , ...( callbackURL && { callbackURL } ) } ) )
471+
472+ expect ( mocks . linkAccount . mock . calls [ 0 ] [ 0 ] . body . errorCallbackURL ) . toBe ( errorCallbackURL )
473+ }
474+ )
531475} )
0 commit comments