@@ -63,22 +63,11 @@ function mockSelectChain(limitResult: unknown[]) {
6363 * Creates a chainable mock for db.update() calls.
6464 * Returns a nested chain: update() -> set() -> where()
6565 */
66- function mockUpdateChain ( returningResult : unknown [ ] = [ { id : 'updated-account' } ] ) {
67- const mockReturning = vi . fn ( ) . mockResolvedValue ( returningResult )
68- const mockWhere = vi . fn ( ) . mockReturnValue ( { returning : mockReturning } )
66+ function mockUpdateChain ( ) {
67+ const mockWhere = vi . fn ( ) . mockResolvedValue ( { } )
6968 const mockSet = vi . fn ( ) . mockReturnValue ( { where : mockWhere } )
7069 mockDb . update . mockReturnValueOnce ( { set : mockSet } )
71- return { mockSet, mockWhere, mockReturning }
72- }
73-
74- function refreshChain ( overrides : Record < string , unknown > = { } ) {
75- return {
76- accessToken : 'expired-token' ,
77- refreshToken : 'refresh-token' ,
78- accessTokenExpiresAt : new Date ( Date . now ( ) - 3600 * 1000 ) ,
79- chainVersion : new Date ( '2026-08-19T20:00:00.000Z' ) ,
80- ...overrides ,
81- }
70+ return { mockSet, mockWhere }
8271}
8372
8473describe ( 'OAuth Utils' , ( ) => {
@@ -144,14 +133,14 @@ describe('OAuth Utils', () => {
144133 accessTokenExpiresAt : new Date ( Date . now ( ) - 3600 * 1000 ) ,
145134 providerId : 'google' ,
146135 }
136+
147137 mockRefreshOAuthToken . mockResolvedValueOnce ( {
148138 ok : true ,
149139 accessToken : 'new-token' ,
150140 expiresIn : 3600 ,
151141 refreshToken : 'new-refresh-token' ,
152142 } )
153143
154- mockSelectChain ( [ refreshChain ( ) ] )
155144 mockUpdateChain ( )
156145
157146 const result = await refreshTokenIfNeeded ( 'request-id' , mockCredential , 'credential-id' )
@@ -169,9 +158,6 @@ describe('OAuth Utils', () => {
169158 accessTokenExpiresAt : new Date ( Date . now ( ) - 3600 * 1000 ) ,
170159 providerId : 'google' ,
171160 }
172- const chainVersion = new Date ( '2026-08-19T20:00:00.000Z' )
173- mockSelectChain ( [ refreshChain ( { chainVersion } ) ] )
174- mockSelectChain ( [ { refreshToken : 'refresh-token' , updatedAt : chainVersion } ] )
175161
176162 mockRefreshOAuthToken . mockResolvedValueOnce ( {
177163 ok : false ,
@@ -242,7 +228,6 @@ describe('OAuth Utils', () => {
242228 }
243229 mockSelectChain ( [ mockResolvedCredential ] )
244230 mockSelectChain ( [ mockAccountRow ] )
245- mockSelectChain ( [ refreshChain ( ) ] )
246231 mockUpdateChain ( )
247232
248233 mockRefreshOAuthToken . mockResolvedValueOnce ( {
@@ -285,9 +270,6 @@ describe('OAuth Utils', () => {
285270 }
286271 mockSelectChain ( [ mockResolvedCredential ] )
287272 mockSelectChain ( [ mockAccountRow ] )
288- const chainVersion = new Date ( '2026-08-19T20:00:00.000Z' )
289- mockSelectChain ( [ refreshChain ( { chainVersion } ) ] )
290- mockSelectChain ( [ { refreshToken : 'refresh-token' , updatedAt : chainVersion } ] )
291273
292274 mockRefreshOAuthToken . mockResolvedValueOnce ( {
293275 ok : false ,
@@ -301,111 +283,6 @@ describe('OAuth Utils', () => {
301283 } )
302284 } )
303285
304- describe ( 'rotating per-account refresh' , ( ) => {
305- const past = new Date ( Date . now ( ) - 3600 * 1000 )
306- const chainVersion = new Date ( '2026-08-19T20:00:00.000Z' )
307-
308- function bitbucketCredential ( overrides : Record < string , unknown > = { } ) {
309- return {
310- id : 'account-1' ,
311- resolvedCredentialId : 'account-1' ,
312- accountId : '{bitbucket-user}' ,
313- accessToken : 'stale-access' ,
314- refreshToken : 'captured-stale-refresh' ,
315- accessTokenExpiresAt : past ,
316- providerId : 'bitbucket' ,
317- userId : 'user-1' ,
318- ...overrides ,
319- }
320- }
321-
322- it ( 'serializes through PostgreSQL and refreshes from the latest persisted chain' , async ( ) => {
323- mockSelectChain ( [
324- refreshChain ( {
325- accessToken : 'expired-current-access' ,
326- refreshToken : 'latest-refresh' ,
327- chainVersion,
328- } ) ,
329- ] )
330- mockRefreshOAuthToken . mockResolvedValueOnce ( {
331- ok : true ,
332- accessToken : 'new-access' ,
333- expiresIn : 3600 ,
334- refreshToken : 'rotated-refresh' ,
335- } )
336- const { mockSet, mockReturning } = mockUpdateChain ( )
337-
338- const result = await refreshTokenIfNeeded ( 'request-id' , bitbucketCredential ( ) , 'account-1' )
339-
340- expect ( result ) . toEqual ( { accessToken : 'new-access' , refreshed : true } )
341- expect ( mockDb . transaction ) . toHaveBeenCalledTimes ( 1 )
342- expect ( mockDb . execute ) . toHaveBeenCalledTimes ( 1 )
343- expect ( redisConfigMockFns . mockAcquireLock ) . toHaveBeenCalledWith (
344- 'oauth:refresh:account-1' ,
345- expect . any ( String ) ,
346- 30
347- )
348- expect ( mockRefreshOAuthToken ) . toHaveBeenCalledWith ( 'bitbucket' , 'latest-refresh' )
349- expect ( mockSet ) . toHaveBeenCalledWith (
350- expect . objectContaining ( {
351- accessToken : 'new-access' ,
352- refreshToken : 'rotated-refresh' ,
353- } )
354- )
355- expect ( mockReturning ) . toHaveBeenCalledWith ( { id : expect . anything ( ) } )
356- } )
357-
358- it ( 'falls back to the database lock when Redis lock acquisition fails' , async ( ) => {
359- redisConfigMockFns . mockAcquireLock . mockRejectedValueOnce ( new Error ( 'redis unavailable' ) )
360- mockSelectChain ( [ refreshChain ( { chainVersion } ) ] )
361- mockRefreshOAuthToken . mockResolvedValueOnce ( {
362- ok : true ,
363- accessToken : 'new-access' ,
364- expiresIn : 3600 ,
365- refreshToken : 'rotated-refresh' ,
366- } )
367- mockUpdateChain ( )
368-
369- const result = await refreshTokenIfNeeded (
370- 'request-id' ,
371- bitbucketCredential ( { refreshToken : 'refresh-token' } ) ,
372- 'account-1'
373- )
374-
375- expect ( result ) . toEqual ( { accessToken : 'new-access' , refreshed : true } )
376- expect ( mockDb . transaction ) . toHaveBeenCalledTimes ( 1 )
377- expect ( mockDb . execute ) . toHaveBeenCalledTimes ( 1 )
378- expect ( mockRefreshOAuthToken ) . toHaveBeenCalledTimes ( 1 )
379- } )
380-
381- it ( 'discards a CAS loser and returns the newer persisted access token' , async ( ) => {
382- mockSelectChain ( [ refreshChain ( { chainVersion } ) ] )
383- mockRefreshOAuthToken . mockResolvedValueOnce ( {
384- ok : true ,
385- accessToken : 'losing-access' ,
386- expiresIn : 3600 ,
387- refreshToken : 'losing-refresh' ,
388- } )
389- mockUpdateChain ( [ ] )
390- mockSelectChain ( [
391- refreshChain ( {
392- accessToken : 'winning-access' ,
393- refreshToken : 'winning-refresh' ,
394- accessTokenExpiresAt : new Date ( Date . now ( ) + 3600 * 1000 ) ,
395- chainVersion : new Date ( '2026-08-19T20:00:01.000Z' ) ,
396- } ) ,
397- ] )
398-
399- const result = await refreshTokenIfNeeded (
400- 'request-id' ,
401- bitbucketCredential ( { refreshToken : 'refresh-token' } ) ,
402- 'account-1'
403- )
404-
405- expect ( result ) . toEqual ( { accessToken : 'winning-access' , refreshed : true } )
406- } )
407- } )
408-
409286 describe ( 'Slack installation-scoped refresh' , ( ) => {
410287 const SLACK_ACCOUNT_ID = 'T08CM6ZNYBE-usr_U08USBQ9B1T-cbf46a7e-ca75-4a2e-bef5-fd467299eaae'
411288 const past = new Date ( Date . now ( ) - 3600 * 1000 )
@@ -475,13 +352,6 @@ describe('OAuth Utils', () => {
475352 } )
476353
477354 it ( 'keeps per-row behavior for pasted custom-bot account ids' , async ( ) => {
478- mockSelectChain ( [
479- refreshChain ( {
480- accessToken : 'stale-at' ,
481- refreshToken : 'stale-rt' ,
482- chainVersion : new Date ( '2026-08-19T20:00:00.000Z' ) ,
483- } ) ,
484- ] )
485355 mockRefreshOAuthToken . mockResolvedValueOnce ( {
486356 ok : true ,
487357 accessToken : 'new-at' ,
0 commit comments