A calm place to write long-form, and publish it to the open social web.
skypress.blog/
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} );