@@ -116,6 +116,7 @@ const connectGoogleAccount = (input: {
116116 readonly target : TargetShape ;
117117 readonly integration : IntegrationSlug ;
118118 readonly oauthClient : OAuthClientSlug ;
119+ readonly newConnection ?: boolean ;
119120} ) =>
120121 Effect . gen ( function * ( ) {
121122 const redirectUri = new URL ( "/api/oauth/callback" , input . target . baseUrl ) . toString ( ) ;
@@ -146,6 +147,25 @@ const connectGoogleAccount = (input: {
146147 } ,
147148 } ) ;
148149
150+ return yield * runGoogleOAuthFlow ( input , redirectUri ) ;
151+ } ) ;
152+
153+ /** Run one authorize+complete round through the already-registered app. Split
154+ * from `connectGoogleAccount` so a scenario can connect a SECOND account
155+ * through the same app (`newConnection: true`) without re-registering it. */
156+ const runGoogleOAuthFlow = (
157+ input : {
158+ readonly client : Client ;
159+ readonly target : TargetShape ;
160+ readonly integration : IntegrationSlug ;
161+ readonly oauthClient : OAuthClientSlug ;
162+ readonly newConnection ?: boolean ;
163+ } ,
164+ redirectUriOverride ?: string ,
165+ ) =>
166+ Effect . gen ( function * ( ) {
167+ const redirectUri =
168+ redirectUriOverride ?? new URL ( "/api/oauth/callback" , input . target . baseUrl ) . toString ( ) ;
149169 const started = yield * input . client . oauth . start ( {
150170 payload : {
151171 client : input . oauthClient ,
@@ -155,6 +175,7 @@ const connectGoogleAccount = (input: {
155175 integration : input . integration ,
156176 template : GOOGLE_AUTH_TEMPLATE ,
157177 redirectUri,
178+ ...( input . newConnection === true ? { newConnection : true } : { } ) ,
158179 } ,
159180 } ) ;
160181 expect ( started . status , "OAuth starts with an emulator redirect" ) . toBe ( "redirect" ) ;
@@ -167,6 +188,7 @@ const connectGoogleAccount = (input: {
167188 expect ( completed . integration , "OAuth completion creates the connection" ) . toBe (
168189 input . integration ,
169190 ) ;
191+ return completed ;
170192 } ) ;
171193
172194scenario (
@@ -334,13 +356,53 @@ scenario(
334356 refreshed . find ( ( connection ) => connection . name === CONNECTION ) ?. identityLabel ,
335357 "Google Sheets still shows the OAuth grant identity after no-probe health" ,
336358 ) . toBe ( GOOGLE_EMULATOR_ACCOUNT_EMAIL ) ;
359+
360+ // Reconnecting the SAME connection must not clobber a curated label
361+ // with the grant identity.
362+ yield * client . connections . update ( {
363+ params : { owner : "org" , integration : slug , name : CONNECTION } ,
364+ payload : { identityLabel : "Finance account" } ,
365+ } ) ;
366+ yield * runGoogleOAuthFlow ( { client, target, integration : slug , oauthClient } ) ;
367+ const reconnected = yield * client . connections . list ( {
368+ query : { owner : "org" , integration : slug } ,
369+ } ) ;
370+ expect (
371+ reconnected . find ( ( connection ) => connection . name === CONNECTION ) ?. identityLabel ,
372+ "reconnect keeps the curated label over the grant identity" ,
373+ ) . toBe ( "Finance account" ) ;
374+
375+ // A `newConnection` connect under a taken name mints a SECOND
376+ // connection with a suffixed name instead of replacing the first.
377+ const second = yield * runGoogleOAuthFlow ( {
378+ client,
379+ target,
380+ integration : slug ,
381+ oauthClient,
382+ newConnection : true ,
383+ } ) ;
384+ expect ( String ( second . name ) , "second account mints a suffixed connection" ) . toBe (
385+ `${ String ( CONNECTION ) } 2` ,
386+ ) ;
387+ expect ( second . identityLabel , "second account label comes from the grant identity" ) . toBe (
388+ GOOGLE_EMULATOR_ACCOUNT_EMAIL ,
389+ ) ;
390+ const both = yield * client . connections . list ( {
391+ query : { owner : "org" , integration : slug } ,
392+ } ) ;
393+ expect (
394+ both . map ( ( connection ) => String ( connection . name ) ) . sort ( ) ,
395+ "both accounts coexist" ,
396+ ) . toEqual ( [ String ( CONNECTION ) , `${ String ( CONNECTION ) } 2` ] ) ;
337397 } ) ,
338398 Effect . gen ( function * ( ) {
339- yield * client . connections
340- . remove ( {
341- params : { owner : "org" , integration : slug , name : CONNECTION } ,
342- } )
343- . pipe ( Effect . ignore ) ;
399+ for ( const name of [ CONNECTION , ConnectionName . make ( `${ String ( CONNECTION ) } 2` ) ] ) {
400+ yield * client . connections
401+ . remove ( {
402+ params : { owner : "org" , integration : slug , name } ,
403+ } )
404+ . pipe ( Effect . ignore ) ;
405+ }
344406 yield * client . oauth
345407 . removeClient ( { params : { slug : oauthClient } , payload : { owner : "org" } } )
346408 . pipe ( Effect . ignore ) ;
0 commit comments