Barazo AppView backend barazo.forum
at main 217 lines 7.7 kB view raw
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})