Barazo AppView backend
barazo.forum
1import { describe, it, expect, vi, beforeEach } from 'vitest'
2import { createHandleResolver } from '../../../src/lib/handle-resolver.js'
3import type { Cache } from '../../../src/cache/index.js'
4import type { Database } from '../../../src/db/index.js'
5import type { Logger } from '../../../src/lib/logger.js'
6
7// ---------------------------------------------------------------------------
8// Mock functions
9// ---------------------------------------------------------------------------
10
11const cacheGetFn = vi.fn<(...args: unknown[]) => Promise<string | null>>()
12const cacheSetFn = vi.fn<(...args: unknown[]) => Promise<string>>()
13
14const mockCache = {
15 get: cacheGetFn,
16 set: cacheSetFn,
17} as unknown as Cache
18
19const dbSelectFn = vi.fn()
20const mockDb = {
21 select: dbSelectFn,
22} as unknown as Database
23
24const mockLogger = {
25 debug: vi.fn(),
26 info: vi.fn(),
27 warn: vi.fn(),
28 error: vi.fn(),
29} as unknown as Logger
30
31// ---------------------------------------------------------------------------
32// Constants
33// ---------------------------------------------------------------------------
34
35const TEST_DID = 'did:plc:test123456789'
36const TEST_HANDLE = 'jay.bsky.team'
37
38// ---------------------------------------------------------------------------
39// Test suite
40// ---------------------------------------------------------------------------
41
42describe('handle-resolver', () => {
43 beforeEach(() => {
44 vi.clearAllMocks()
45 vi.restoreAllMocks()
46 })
47
48 it('returns handle from Valkey cache when available', async () => {
49 cacheGetFn.mockResolvedValueOnce(TEST_HANDLE)
50
51 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
52 const handle = await resolver.resolve(TEST_DID)
53
54 expect(handle).toBe(TEST_HANDLE)
55 expect(cacheGetFn).toHaveBeenCalledWith(`barazo:handle:${TEST_DID}`)
56 expect(dbSelectFn).not.toHaveBeenCalled()
57 })
58
59 it('falls back to DB when cache misses', async () => {
60 cacheGetFn.mockResolvedValueOnce(null)
61
62 // Mock the Drizzle chain: db.select().from().where().limit()
63 const limitFn = vi.fn().mockResolvedValueOnce([{ handle: TEST_HANDLE }])
64 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
65 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
66 dbSelectFn.mockReturnValue({ from: fromFn })
67
68 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
69 const handle = await resolver.resolve(TEST_DID)
70
71 expect(handle).toBe(TEST_HANDLE)
72 // Should cache the result
73 expect(cacheSetFn).toHaveBeenCalledWith(`barazo:handle:${TEST_DID}`, TEST_HANDLE, 'EX', 3600)
74 })
75
76 it('skips DB result when handle equals DID (not yet resolved)', async () => {
77 cacheGetFn.mockResolvedValueOnce(null)
78
79 // DB has DID as handle (placeholder from before handle resolution)
80 const limitFn = vi.fn().mockResolvedValueOnce([{ handle: TEST_DID }])
81 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
82 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
83 dbSelectFn.mockReturnValue({ from: fromFn })
84
85 // Mock PLC directory fetch
86 const plcDoc = {
87 id: TEST_DID,
88 alsoKnownAs: [`at://${TEST_HANDLE}`],
89 }
90 vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
91 new Response(JSON.stringify(plcDoc), { status: 200 })
92 )
93
94 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
95 const handle = await resolver.resolve(TEST_DID)
96
97 expect(handle).toBe(TEST_HANDLE)
98 })
99
100 it('falls back to PLC directory when cache and DB miss', async () => {
101 cacheGetFn.mockResolvedValueOnce(null)
102
103 // DB returns no results
104 const limitFn = vi.fn().mockResolvedValueOnce([])
105 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
106 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
107 dbSelectFn.mockReturnValue({ from: fromFn })
108
109 // Mock PLC directory fetch
110 const plcDoc = {
111 id: TEST_DID,
112 alsoKnownAs: [`at://${TEST_HANDLE}`],
113 }
114 vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
115 new Response(JSON.stringify(plcDoc), { status: 200 })
116 )
117
118 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
119 const handle = await resolver.resolve(TEST_DID)
120
121 expect(handle).toBe(TEST_HANDLE)
122 // Should cache the result
123 expect(cacheSetFn).toHaveBeenCalledWith(`barazo:handle:${TEST_DID}`, TEST_HANDLE, 'EX', 3600)
124 })
125
126 it('returns DID as fallback when all resolution methods fail', async () => {
127 cacheGetFn.mockResolvedValueOnce(null)
128
129 // DB returns no results
130 const limitFn = vi.fn().mockResolvedValueOnce([])
131 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
132 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
133 dbSelectFn.mockReturnValue({ from: fromFn })
134
135 // PLC directory returns 404
136 vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(new Response('Not found', { status: 404 }))
137
138 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
139 const handle = await resolver.resolve(TEST_DID)
140
141 expect(handle).toBe(TEST_DID)
142 })
143
144 it('handles PLC directory network errors gracefully', async () => {
145 cacheGetFn.mockResolvedValueOnce(null)
146
147 // DB returns no results
148 const limitFn = vi.fn().mockResolvedValueOnce([])
149 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
150 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
151 dbSelectFn.mockReturnValue({ from: fromFn })
152
153 // PLC directory fetch throws
154 vi.spyOn(globalThis, 'fetch').mockRejectedValueOnce(new Error('Network error'))
155
156 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
157 const handle = await resolver.resolve(TEST_DID)
158
159 // Falls back to DID
160 expect(handle).toBe(TEST_DID)
161 })
162
163 it('skips PLC lookup for did:web DIDs', async () => {
164 const webDid = 'did:web:example.com'
165 cacheGetFn.mockResolvedValueOnce(null)
166
167 // DB returns no results
168 const limitFn = vi.fn().mockResolvedValueOnce([])
169 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
170 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
171 dbSelectFn.mockReturnValue({ from: fromFn })
172
173 const fetchSpy = vi.spyOn(globalThis, 'fetch')
174
175 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
176 const handle = await resolver.resolve(webDid)
177
178 // Should not call PLC directory for did:web
179 expect(fetchSpy).not.toHaveBeenCalled()
180 // Falls back to DID
181 expect(handle).toBe(webDid)
182 })
183
184 it('handles cache errors gracefully and continues resolution', async () => {
185 cacheGetFn.mockRejectedValueOnce(new Error('Valkey down'))
186
187 // DB has the handle
188 const limitFn = vi.fn().mockResolvedValueOnce([{ handle: TEST_HANDLE }])
189 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
190 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
191 dbSelectFn.mockReturnValue({ from: fromFn })
192
193 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
194 const handle = await resolver.resolve(TEST_DID)
195
196 expect(handle).toBe(TEST_HANDLE)
197 })
198
199 it('handles missing alsoKnownAs in PLC document', async () => {
200 cacheGetFn.mockResolvedValueOnce(null)
201
202 const limitFn = vi.fn().mockResolvedValueOnce([])
203 const whereFn = vi.fn().mockReturnValue({ limit: limitFn })
204 const fromFn = vi.fn().mockReturnValue({ where: whereFn })
205 dbSelectFn.mockReturnValue({ from: fromFn })
206
207 // PLC document without alsoKnownAs
208 vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
209 new Response(JSON.stringify({ id: TEST_DID }), { status: 200 })
210 )
211
212 const resolver = createHandleResolver(mockCache, mockDb, mockLogger)
213 const handle = await resolver.resolve(TEST_DID)
214
215 expect(handle).toBe(TEST_DID)
216 })
217})