Skip to content

Commit 7ddd90b

Browse files
improvement(cron): fire-and-forget for cron-invoked endpoints (#4764)
* improvement(cron): fire-and-forget for cron-invoked endpoints * fix(cron): add staleness takeover to single-flight guard * improvement(cron): drop single-flight guard, rely on DB row claiming
1 parent 28766dd commit 7ddd90b

11 files changed

Lines changed: 714 additions & 332 deletions

File tree

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* Tests for the Teams subscription renewal cron route.
3+
*
4+
* @vitest-environment node
5+
*/
6+
import {
7+
authOAuthUtilsMock,
8+
createMockRequest,
9+
dbChainMock,
10+
dbChainMockFns,
11+
redisConfigMock,
12+
redisConfigMockFns,
13+
resetDbChainMock,
14+
} from '@sim/testing'
15+
import { beforeEach, describe, expect, it, vi } from 'vitest'
16+
17+
const { mockVerifyCronAuth } = vi.hoisted(() => ({
18+
mockVerifyCronAuth: vi.fn().mockReturnValue(null),
19+
}))
20+
21+
vi.mock('@/lib/auth/internal', () => ({
22+
verifyCronAuth: mockVerifyCronAuth,
23+
}))
24+
25+
vi.mock('@/lib/core/config/redis', () => redisConfigMock)
26+
vi.mock('@sim/db', () => dbChainMock)
27+
vi.mock('@/app/api/auth/oauth/utils', () => authOAuthUtilsMock)
28+
29+
import { GET } from './route'
30+
31+
function createRequest() {
32+
return createMockRequest(
33+
'GET',
34+
undefined,
35+
{},
36+
'http://localhost:3000/api/cron/renew-subscriptions'
37+
)
38+
}
39+
40+
const flushMicrotasks = () => new Promise((resolve) => setTimeout(resolve, 0))
41+
42+
describe('Teams subscription renewal route (fire-and-forget)', () => {
43+
beforeEach(() => {
44+
vi.clearAllMocks()
45+
resetDbChainMock()
46+
redisConfigMockFns.mockAcquireLock.mockResolvedValue(true)
47+
redisConfigMockFns.mockReleaseLock.mockResolvedValue(true)
48+
mockVerifyCronAuth.mockReturnValue(null)
49+
})
50+
51+
it('returns the auth error when cron auth fails', async () => {
52+
mockVerifyCronAuth.mockReturnValueOnce(new Response(null, { status: 401 }) as never)
53+
54+
const response = await GET(createRequest())
55+
56+
expect(response.status).toBe(401)
57+
expect(redisConfigMockFns.mockAcquireLock).not.toHaveBeenCalled()
58+
})
59+
60+
it('acknowledges with 202 and renews in the background after acquiring the lock', async () => {
61+
const response = await GET(createRequest())
62+
63+
expect(response.status).toBe(202)
64+
const data = await response.json()
65+
expect(data).toMatchObject({ status: 'started' })
66+
expect(redisConfigMockFns.mockAcquireLock).toHaveBeenCalledWith(
67+
'teams-subscription-renewal-lock',
68+
expect.any(String),
69+
expect.any(Number)
70+
)
71+
72+
await flushMicrotasks()
73+
expect(dbChainMockFns.select).toHaveBeenCalled()
74+
expect(redisConfigMockFns.mockReleaseLock).toHaveBeenCalledWith(
75+
'teams-subscription-renewal-lock',
76+
expect.any(String)
77+
)
78+
})
79+
80+
it('skips with 202 when the lock is already held', async () => {
81+
redisConfigMockFns.mockAcquireLock.mockResolvedValueOnce(false)
82+
83+
const response = await GET(createRequest())
84+
85+
expect(response.status).toBe(202)
86+
const data = await response.json()
87+
expect(data).toMatchObject({ status: 'skip' })
88+
expect(dbChainMockFns.select).not.toHaveBeenCalled()
89+
})
90+
})

0 commit comments

Comments
 (0)