@@ -211,6 +211,201 @@ describe('useMemberEnrollment', () => {
211211 )
212212 } )
213213
214+ it . each ( [
215+ [ 'existing' , 'account_mismatch' ] ,
216+ [ 'existing' , 'denied' ] ,
217+ [ 'existing' , 'expired' ] ,
218+ [ 'new' , 'account_mismatch' ] ,
219+ [ 'new' , 'denied' ] ,
220+ [ 'new' , 'expired' ] ,
221+ ] as const ) (
222+ 'ignores the previous %s source’s %s while its retry request is pending' ,
223+ ( source , failure ) => {
224+ mount ( new Set ( ) , true , mocks . connectionError )
225+ const mutation = source === 'existing' ? mocks . enrollmentMutate : mocks . sourceConnectionMutate
226+ const connect = ( ) => {
227+ if ( source === 'existing' ) enrollment ( ) . connect ( 'kb-1' , 'connector-1' )
228+ else enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } )
229+ }
230+ act ( connect )
231+ act ( ( ) =>
232+ mutation . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
233+ url : 'https://provider.test/previous' ,
234+ connectorId : 'connector-1' ,
235+ } )
236+ )
237+ act ( ( ) => vi . advanceTimersByTime ( 9 * 60_000 ) )
238+ act ( connect )
239+ if ( failure !== 'expired' ) {
240+ act ( ( ) => mocks . channels [ 0 ] . onmessage ?.( new MessageEvent ( 'message' , { data : failure } ) ) )
241+ }
242+ act ( ( ) => vi . advanceTimersByTime ( 60_000 ) )
243+ expect ( mocks . connectionError ) . not . toHaveBeenCalled ( )
244+ expect ( enrollment ( ) . error ) . toBeNull ( )
245+ act ( ( ) =>
246+ mutation . mock . calls [ 1 ] [ 1 ] . onSuccess ( {
247+ url : 'https://provider.test/retry' ,
248+ connectorId : 'connector-1' ,
249+ } )
250+ )
251+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
252+ expect ( mocks . channels [ 1 ] . close ) . not . toHaveBeenCalled ( )
253+ }
254+ )
255+
256+ it . each ( [
257+ [ 'existing' , 'success' ] ,
258+ [ 'existing' , 'failure' ] ,
259+ [ 'new' , 'success' ] ,
260+ [ 'new' , 'failure' ] ,
261+ ] as const ) ( 'ignores a superseded %s source request’s late %s' , ( source , outcome ) => {
262+ mount ( new Set ( ) , true , mocks . connectionError )
263+ const mutation = source === 'existing' ? mocks . enrollmentMutate : mocks . sourceConnectionMutate
264+ const retryTab = { location : { href : '' } , closed : false , close : vi . fn ( ) }
265+ vi . mocked ( window . open )
266+ . mockReturnValueOnce ( enrollmentTab as unknown as Window )
267+ . mockReturnValueOnce ( retryTab as unknown as Window )
268+ for ( let index = 0 ; index < 2 ; index += 1 ) {
269+ act ( ( ) => {
270+ if ( source === 'existing' ) enrollment ( ) . connect ( 'kb-1' , 'connector-1' )
271+ else enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } )
272+ } )
273+ }
274+ act ( ( ) =>
275+ mutation . mock . calls [ 1 ] [ 1 ] . onSuccess ( {
276+ url : 'https://provider.test/retry' ,
277+ connectorId : 'connector-1' ,
278+ } )
279+ )
280+ act ( ( ) => {
281+ if ( outcome === 'failure' ) {
282+ mutation . mock . calls [ 0 ] [ 1 ] . onError ( new Error ( 'Previous request failed' ) )
283+ } else {
284+ mutation . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
285+ url : 'https://provider.test/previous' ,
286+ connectorId : 'connector-1' ,
287+ } )
288+ }
289+ } )
290+ expect ( enrollmentTab . location . href ) . toBe ( '' )
291+ expect ( retryTab . location . href ) . toBe ( 'https://provider.test/retry' )
292+ expect ( retryTab . close ) . not . toHaveBeenCalled ( )
293+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
294+ expect ( mocks . channels [ 1 ] . close ) . not . toHaveBeenCalled ( )
295+ expect ( mocks . connectionError ) . not . toHaveBeenCalled ( )
296+ expect ( enrollment ( ) . error ) . toBeNull ( )
297+ } )
298+
299+ it ( 'retires a first-source authorization when retrying its resolved connector' , ( ) => {
300+ mount ( new Set ( ) , true , mocks . connectionError )
301+ act ( ( ) => enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } ) )
302+ act ( ( ) =>
303+ mocks . sourceConnectionMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
304+ url : 'https://provider.test/previous' ,
305+ connectorId : 'connector-1' ,
306+ } )
307+ )
308+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
309+ act ( ( ) => mocks . channels [ 0 ] . onmessage ?.( new MessageEvent ( 'message' , { data : 'denied' } ) ) )
310+ expect ( mocks . channels [ 0 ] . close ) . toHaveBeenCalledOnce ( )
311+ expect ( mocks . connectionError ) . not . toHaveBeenCalled ( )
312+ } )
313+
314+ it ( 'does not let a delayed first-source response replace its newer connector authorization' , ( ) => {
315+ mount ( new Set ( ) , true , mocks . connectionError )
316+ act ( ( ) => enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } ) )
317+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
318+ act ( ( ) =>
319+ mocks . enrollmentMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( { url : 'https://provider.test/retry' } )
320+ )
321+ act ( ( ) =>
322+ mocks . sourceConnectionMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
323+ url : 'https://provider.test/previous' ,
324+ connectorId : 'connector-1' ,
325+ } )
326+ )
327+ expect ( enrollmentTab . location . href ) . toBe ( 'https://provider.test/retry' )
328+ expect ( mocks . channels [ 1 ] . close ) . not . toHaveBeenCalled ( )
329+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
330+ } )
331+
332+ it ( 'ignores first-source success while a newer request for its connector is still pending' , ( ) => {
333+ mount ( new Set ( ) , true , mocks . connectionError )
334+ const retryTab = { location : { href : '' } , closed : false , close : vi . fn ( ) }
335+ vi . mocked ( window . open )
336+ . mockReturnValueOnce ( enrollmentTab as unknown as Window )
337+ . mockReturnValueOnce ( retryTab as unknown as Window )
338+ act ( ( ) => enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } ) )
339+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
340+ act ( ( ) =>
341+ mocks . sourceConnectionMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
342+ url : 'https://provider.test/previous' ,
343+ connectorId : 'connector-1' ,
344+ } )
345+ )
346+ expect ( enrollmentTab . location . href ) . toBe ( '' )
347+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( false )
348+ act ( ( ) => mocks . channels [ 0 ] . onmessage ?.( new MessageEvent ( 'message' , { data : 'denied' } ) ) )
349+ expect ( mocks . connectionError ) . not . toHaveBeenCalled ( )
350+ act ( ( ) =>
351+ mocks . enrollmentMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( { url : 'https://provider.test/retry' } )
352+ )
353+ expect ( retryTab . location . href ) . toBe ( 'https://provider.test/retry' )
354+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
355+ expect ( mocks . channels [ 1 ] . close ) . not . toHaveBeenCalled ( )
356+ } )
357+
358+ it ( 'retires a pending connector request when a newer first-source request resolves to it' , ( ) => {
359+ mount ( new Set ( ) , true , mocks . connectionError )
360+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
361+ act ( ( ) => enrollment ( ) . connectSource ( 'workspace-1' , 'jira' , { projectKey : 'ENG' } ) )
362+ act ( ( ) =>
363+ mocks . sourceConnectionMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( {
364+ url : 'https://provider.test/retry' ,
365+ connectorId : 'connector-1' ,
366+ } )
367+ )
368+ act ( ( ) => mocks . enrollmentMutate . mock . calls [ 0 ] [ 1 ] . onError ( new Error ( 'Previous request failed' ) ) )
369+ expect ( mocks . connectionError ) . not . toHaveBeenCalled ( )
370+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
371+ expect ( mocks . channels [ 0 ] . close ) . toHaveBeenCalledOnce ( )
372+ expect ( mocks . channels [ 1 ] . close ) . not . toHaveBeenCalled ( )
373+ } )
374+
375+ it ( 'keeps the previous authorization active when the retry popup is blocked' , ( ) => {
376+ mount ( new Set ( ) , true , mocks . connectionError )
377+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
378+ act ( ( ) =>
379+ mocks . enrollmentMutate . mock . calls [ 0 ] [ 1 ] . onSuccess ( { url : 'https://provider.test/previous' } )
380+ )
381+ vi . mocked ( window . open ) . mockReturnValueOnce ( null )
382+ act ( ( ) => enrollment ( ) . connect ( 'kb-1' , 'connector-1' ) )
383+ expect ( mocks . channels [ 0 ] . close ) . not . toHaveBeenCalled ( )
384+ expect ( enrollment ( ) . isAwaiting ( 'connector-1' ) ) . toBe ( true )
385+ expect ( mocks . enrollmentMutate ) . toHaveBeenCalledOnce ( )
386+ } )
387+
388+ it ( 'keeps pending source requests with different scopes or configurations independent' , ( ) => {
389+ mount ( new Set ( ) , true , mocks . connectionError )
390+ for ( const [ owner , projectKey ] of [
391+ [ 'workspace-1' , 'ENG' ] ,
392+ [ 'workspace-1' , 'SUPPORT' ] ,
393+ [ 'workspace-2' , 'ENG' ] ,
394+ ] ) {
395+ act ( ( ) => enrollment ( ) . connectSource ( owner , 'jira' , { projectKey } ) )
396+ }
397+ for ( let index = 0 ; index < 3 ; index += 1 ) {
398+ act ( ( ) =>
399+ mocks . sourceConnectionMutate . mock . calls [ index ] [ 1 ] . onSuccess ( {
400+ url : `https://provider.test/attempt-${ index } ` ,
401+ connectorId : `connector-${ index } ` ,
402+ } )
403+ )
404+ expect ( enrollment ( ) . isAwaiting ( `connector-${ index } ` ) ) . toBe ( true )
405+ expect ( mocks . channels [ index ] . close ) . not . toHaveBeenCalled ( )
406+ }
407+ } )
408+
214409 it ( 'reports a blocked popup once without starting a connection' , ( ) => {
215410 mount ( new Set ( ) , true , mocks . connectionError )
216411 vi . mocked ( window . open ) . mockReturnValueOnce ( null )
0 commit comments