A calm place to write long-form, and publish it to the open social web. skypress.blog/
0

Configure Feed

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

at trunk 5.5 kB View raw
1import { describe, expect, it, vi } from 'vitest'; 2import { lookupActor, searchActors } from './actor-lookup'; 3 4const ok = ( body: unknown ) => 5 ( { ok: true, json: async () => body } ) as unknown as Response; 6const notOk = () => ( { ok: false, json: async () => ( {} ) } ) as unknown as Response; 7 8describe( 'lookupActor', () => { 9 it( 'returns a preview for a resolvable handle', async () => { 10 const fetchImpl = vi.fn().mockResolvedValue( 11 ok( { 12 did: 'did:plc:abc', 13 handle: 'alice.bsky.social', 14 displayName: 'Alice Rivers', 15 avatar: 'https://cdn.example/a.jpg', 16 } ) 17 ); 18 const preview = await lookupActor( '@Alice.bsky.social', fetchImpl as unknown as typeof fetch ); 19 expect( preview ).toEqual( { 20 did: 'did:plc:abc', 21 handle: 'alice.bsky.social', 22 displayName: 'Alice Rivers', 23 avatar: 'https://cdn.example/a.jpg', 24 } ); 25 expect( fetchImpl ).toHaveBeenCalledWith( 26 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=alice.bsky.social' 27 ); 28 } ); 29 30 it( 'passes a DID through untouched', async () => { 31 const fetchImpl = vi.fn().mockResolvedValue( ok( { did: 'did:plc:xyz', handle: 'x.test' } ) ); 32 await lookupActor( 'did:plc:xyz', fetchImpl as unknown as typeof fetch ); 33 expect( fetchImpl ).toHaveBeenCalledWith( 34 'https://public.api.bsky.app/xrpc/app.bsky.actor.getProfile?actor=did%3Aplc%3Axyz' 35 ); 36 } ); 37 38 it( 'returns null without fetching for syntactically invalid input', async () => { 39 const fetchImpl = vi.fn(); 40 expect( await lookupActor( 'notahandle', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 41 expect( await lookupActor( ' ', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 42 expect( fetchImpl ).not.toHaveBeenCalled(); 43 } ); 44 45 it( 'returns null on a non-ok response', async () => { 46 const fetchImpl = vi.fn().mockResolvedValue( notOk() ); 47 expect( await lookupActor( 'ghost.bsky.social', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 48 } ); 49 50 it( 'returns null when the network throws', async () => { 51 const fetchImpl = vi.fn().mockRejectedValue( new Error( 'offline' ) ); 52 expect( await lookupActor( 'alice.bsky.social', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 53 } ); 54 55 it( 'returns null when the body has no did', async () => { 56 const fetchImpl = vi.fn().mockResolvedValue( ok( { handle: 'x.test' } ) ); 57 expect( await lookupActor( 'x.test', fetchImpl as unknown as typeof fetch ) ).toBeNull(); 58 } ); 59} ); 60 61describe( 'searchActors', () => { 62 it( 'returns multiple previews for a partial query', async () => { 63 const fetchImpl = vi.fn().mockResolvedValue( 64 ok( { 65 actors: [ 66 { 67 did: 'did:plc:abc', 68 handle: 'riad.blog', 69 displayName: 'Riad', 70 avatar: 'https://cdn.example/r.jpg', 71 }, 72 { 73 did: 'did:plc:def', 74 handle: 'rianna.bsky.social', 75 displayName: 'Rianna', 76 avatar: null, 77 }, 78 ], 79 } ) 80 ); 81 const results = await searchActors( 'ria', fetchImpl as unknown as typeof fetch ); 82 expect( results ).toEqual( [ 83 { 84 did: 'did:plc:abc', 85 handle: 'riad.blog', 86 displayName: 'Riad', 87 avatar: 'https://cdn.example/r.jpg', 88 }, 89 { 90 did: 'did:plc:def', 91 handle: 'rianna.bsky.social', 92 displayName: 'Rianna', 93 avatar: null, 94 }, 95 ] ); 96 } ); 97 98 it( 'hits the typeahead endpoint with an encoded query and a limit', async () => { 99 const fetchImpl = vi.fn().mockResolvedValue( ok( { actors: [] } ) ); 100 await searchActors( 'a b', fetchImpl as unknown as typeof fetch, 6 ); 101 expect( fetchImpl ).toHaveBeenCalledWith( 102 'https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=a%20b&limit=6' 103 ); 104 } ); 105 106 it( 'maps missing displayName and avatar to null', async () => { 107 const fetchImpl = vi.fn().mockResolvedValue( 108 ok( { actors: [ { did: 'did:plc:abc', handle: 'riad.blog' } ] } ) 109 ); 110 const results = await searchActors( 'ria', fetchImpl as unknown as typeof fetch ); 111 expect( results ).toEqual( [ 112 { did: 'did:plc:abc', handle: 'riad.blog', displayName: null, avatar: null }, 113 ] ); 114 } ); 115 116 it( 'drops actors missing a did or handle', async () => { 117 const fetchImpl = vi.fn().mockResolvedValue( 118 ok( { 119 actors: [ 120 { did: 'did:plc:abc', handle: 'riad.blog' }, 121 { handle: 'nohandle-did.test' }, 122 { did: 'did:plc:nohandle' }, 123 ], 124 } ) 125 ); 126 const results = await searchActors( 'ria', fetchImpl as unknown as typeof fetch ); 127 expect( results ).toEqual( [ 128 { did: 'did:plc:abc', handle: 'riad.blog', displayName: null, avatar: null }, 129 ] ); 130 } ); 131 132 it( 'returns [] without fetching for an empty or whitespace query', async () => { 133 const fetchImpl = vi.fn(); 134 expect( await searchActors( '', fetchImpl as unknown as typeof fetch ) ).toEqual( [] ); 135 expect( await searchActors( ' ', fetchImpl as unknown as typeof fetch ) ).toEqual( [] ); 136 expect( fetchImpl ).not.toHaveBeenCalled(); 137 } ); 138 139 it( 'returns [] on a non-ok response', async () => { 140 const fetchImpl = vi.fn().mockResolvedValue( notOk() ); 141 expect( await searchActors( 'ria', fetchImpl as unknown as typeof fetch ) ).toEqual( [] ); 142 } ); 143 144 it( 'returns [] when the network throws', async () => { 145 const fetchImpl = vi.fn().mockRejectedValue( new Error( 'offline' ) ); 146 expect( await searchActors( 'ria', fetchImpl as unknown as typeof fetch ) ).toEqual( [] ); 147 } ); 148 149 it( 'returns [] when the body has no actors array', async () => { 150 const fetchImpl = vi.fn().mockResolvedValue( ok( {} ) ); 151 expect( await searchActors( 'ria', fetchImpl as unknown as typeof fetch ) ).toEqual( [] ); 152 } ); 153} );