Skip to content

Commit f5cf56c

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
chore(bitbucket): keep integration changes scoped
1 parent ee3bde0 commit f5cf56c

6 files changed

Lines changed: 116 additions & 357 deletions

File tree

apps/sim/app/api/auth/oauth/utils.test.ts

Lines changed: 4 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -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

8473
describe('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',

apps/sim/lib/concurrency/__tests__/leader-lock.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ describe('withLeaderLock', () => {
3636
onFollower: async () => null,
3737
})
3838

39-
const [acquireKey, acquireValue, acquireTtl] = redisConfigMockFns.mockAcquireLock.mock.calls[0]!
39+
const [acquireKey, acquireValue] = redisConfigMockFns.mockAcquireLock.mock.calls[0]!
4040
const [releaseKey, releaseValue] = redisConfigMockFns.mockReleaseLock.mock.calls[0]!
4141

4242
expect(acquireKey).toBe('k')
4343
expect(releaseKey).toBe('k')
4444
expect(acquireValue).toBe(releaseValue)
4545
expect(typeof acquireValue).toBe('string')
4646
expect((acquireValue as string).length).toBeGreaterThan(0)
47-
expect(acquireTtl).toBe(30)
4847
})
4948

5049
it('falls back to uncoordinated leader when acquireLock throws', async () => {

apps/sim/lib/concurrency/leader-lock.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import { acquireLock, releaseLock } from '@/lib/core/config/redis'
66

77
const logger = createLogger('LeaderLock')
88

9-
const DEFAULT_TTL_SEC = 30
9+
const DEFAULT_TTL_SEC = 10
1010
const DEFAULT_POLL_INTERVAL_MS = 100
11-
const DEFAULT_MAX_WAIT_MS = DEFAULT_TTL_SEC * 1000
11+
const DEFAULT_MAX_WAIT_MS = 3_000
1212

1313
export interface LeaderLockOptions<T> {
1414
key: string

0 commit comments

Comments
 (0)