[READ-ONLY] a fast, modern browser for the npm registry
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

test: add atproto lock tests (#1105)

authored by

James Garbutt and committed by
GitHub
8ce95611 de1425cc

+154
+154
test/unit/server/utils/atproto/lock.spec.ts
··· 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 + })