Skip to content

Commit 8ce9561

Browse files
authored
test: add atproto lock tests (#1105)
1 parent de1425c commit 8ce9561

File tree

1 file changed

+154
-0
lines changed

1 file changed

+154
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { describe, expect, it, vi, beforeEach } from 'vitest'
2+
3+
const mockRedisSet = vi.fn()
4+
const mockRedisGet = vi.fn()
5+
const mockRedisDel = vi.fn()
6+
7+
vi.mock('@upstash/redis', () => ({
8+
Redis: class {
9+
set = mockRedisSet
10+
get = mockRedisGet
11+
del = mockRedisDel
12+
},
13+
}))
14+
15+
const mockLocalLock = vi.fn()
16+
vi.mock('@atproto/oauth-client-node', () => ({
17+
requestLocalLock: mockLocalLock,
18+
}))
19+
20+
const mockConfig = {
21+
upstash: {
22+
redisRestUrl: '',
23+
redisRestToken: '',
24+
},
25+
}
26+
vi.stubGlobal('useRuntimeConfig', () => mockConfig)
27+
28+
const LOCK_UUID = '00000000-0000-0000-0000-000000000000'
29+
vi.spyOn(crypto, 'randomUUID').mockReturnValue(LOCK_UUID)
30+
31+
const { getOAuthLock } = await import('../../../../../server/utils/atproto/lock')
32+
33+
function getUpstashLock() {
34+
mockConfig.upstash.redisRestUrl = 'https://redis.example.com'
35+
mockConfig.upstash.redisRestToken = 'token-123'
36+
return getOAuthLock()
37+
}
38+
39+
describe('lock', () => {
40+
beforeEach(() => {
41+
vi.clearAllMocks()
42+
mockConfig.upstash.redisRestUrl = ''
43+
mockConfig.upstash.redisRestToken = ''
44+
})
45+
46+
it('returns local lock when upstash is not configured', () => {
47+
const lock = getOAuthLock()
48+
expect(lock).toBe(mockLocalLock)
49+
})
50+
51+
it('returns local lock when only redisRestUrl is set', () => {
52+
mockConfig.upstash.redisRestUrl = 'https://totally-a-redis-server.com'
53+
const lock = getOAuthLock()
54+
expect(lock).toBe(mockLocalLock)
55+
})
56+
57+
it('returns local lock when only redisRestToken is set', () => {
58+
mockConfig.upstash.redisRestToken = 'super-fancy-secret-token'
59+
const lock = getOAuthLock()
60+
expect(lock).toBe(mockLocalLock)
61+
})
62+
63+
it('returns upstash lock when both url and token are configured', () => {
64+
mockConfig.upstash.redisRestUrl = 'https://redis.redis.redis'
65+
mockConfig.upstash.redisRestToken = 'token-123'
66+
const lock = getOAuthLock()
67+
expect(lock).not.toBe(mockLocalLock)
68+
expect(typeof lock).toBe('function')
69+
})
70+
71+
it('acquires lock, runs fn, and releases lock', async () => {
72+
mockRedisSet.mockResolvedValueOnce('OK')
73+
mockRedisGet.mockResolvedValueOnce(LOCK_UUID)
74+
mockRedisDel.mockResolvedValueOnce(1)
75+
76+
const lock = getUpstashLock()
77+
const result = await lock('test-key', () => 'hello')
78+
79+
expect(result).toBe('hello')
80+
expect(mockRedisSet).toHaveBeenCalledOnce()
81+
expect(mockRedisSet).toHaveBeenCalledWith(`oauth:lock:test-key`, LOCK_UUID, {
82+
nx: true,
83+
ex: 30,
84+
})
85+
expect(mockRedisDel).toHaveBeenCalledWith('oauth:lock:test-key')
86+
})
87+
88+
it('retries once if first acquire fails', async () => {
89+
mockRedisSet
90+
.mockResolvedValueOnce(null) // fail
91+
.mockResolvedValueOnce('OK') // success
92+
mockRedisGet.mockResolvedValueOnce(LOCK_UUID)
93+
mockRedisDel.mockResolvedValueOnce(1)
94+
95+
const lock = getUpstashLock()
96+
const result = await lock('retry-key', () => 42)
97+
98+
expect(result).toBe(42)
99+
expect(mockRedisSet).toHaveBeenCalledTimes(2)
100+
expect(mockRedisDel).toHaveBeenCalledWith('oauth:lock:retry-key')
101+
})
102+
103+
it('proceeds without lock if both acquire attempts fail', async () => {
104+
mockRedisSet.mockResolvedValueOnce(null).mockResolvedValueOnce(null)
105+
106+
const lock = getUpstashLock()
107+
const result = await lock('no-lock-key', () => 'fallback')
108+
109+
expect(result).toBe('fallback')
110+
expect(mockRedisSet).toHaveBeenCalledTimes(2)
111+
expect(mockRedisGet).not.toHaveBeenCalled()
112+
expect(mockRedisDel).not.toHaveBeenCalled()
113+
})
114+
115+
it('does not delete lock if another instance took ownership', async () => {
116+
mockRedisSet.mockResolvedValueOnce('OK')
117+
mockRedisGet.mockResolvedValueOnce('some-other-uuid')
118+
119+
const lock = getUpstashLock()
120+
await lock('stolen-key', () => 'done')
121+
122+
expect(mockRedisGet).toHaveBeenCalledWith('oauth:lock:stolen-key')
123+
expect(mockRedisDel).not.toHaveBeenCalled()
124+
})
125+
126+
it('releases lock even if fn throws', async () => {
127+
mockRedisSet.mockResolvedValueOnce('OK')
128+
mockRedisGet.mockResolvedValueOnce(LOCK_UUID)
129+
mockRedisDel.mockResolvedValueOnce(1)
130+
131+
const lock = getUpstashLock()
132+
await expect(
133+
lock('error-key', () => {
134+
throw new Error('boom')
135+
}),
136+
).rejects.toThrow('boom')
137+
138+
expect(mockRedisDel).toHaveBeenCalledWith('oauth:lock:error-key')
139+
})
140+
141+
it('works with async fn', async () => {
142+
mockRedisSet.mockResolvedValueOnce('OK')
143+
mockRedisGet.mockResolvedValueOnce(LOCK_UUID)
144+
mockRedisDel.mockResolvedValueOnce(1)
145+
146+
const lock = getUpstashLock()
147+
const result = await lock('async-key', async () => {
148+
await new Promise(resolve => setTimeout(resolve, 10))
149+
return 'async-result'
150+
})
151+
152+
expect(result).toBe('async-result')
153+
})
154+
})

0 commit comments

Comments
 (0)