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 8.2 kB View raw
1import { describe, expect, it, vi, beforeEach } from 'vitest'; 2import { 3 resolveAuthorContext, 4 resolvePublicationContext, 5 resolveArticleContext, 6} from './read-context'; 7import { resolveAuthor } from './identity'; 8import { getRecord, listRecords } from './records'; 9import { buildGetBlobUrl } from '../media/blob'; 10import { SITE_BASE } from '../publish/records'; 11 12vi.mock( './identity', () => ( { resolveAuthor: vi.fn() } ) ); 13vi.mock( './records', () => ( { getRecord: vi.fn(), listRecords: vi.fn() } ) ); 14 15const mockedResolveAuthor = resolveAuthor as unknown as ReturnType< typeof vi.fn >; 16const mockedGetRecord = getRecord as unknown as ReturnType< typeof vi.fn >; 17const mockedListRecords = listRecords as unknown as ReturnType< typeof vi.fn >; 18 19const PDS = 'https://pds.example'; 20const DID = 'did:plc:me'; 21const HANDLE = 'me.bsky.social'; 22const ICON = { $type: 'blob', ref: { $link: 'bafyicon' }, mimeType: 'image/png', size: 1 }; 23 24const PUB_URI = `at://${ DID }/site.standard.publication/a`; 25 26function pubRecord( rkey: string, url: string, extra: Record< string, unknown > = {} ) { 27 return { 28 uri: `at://${ DID }/site.standard.publication/${ rkey }`, 29 cid: 'c', 30 value: { url, name: `P${ rkey }`, ...extra }, 31 }; 32} 33 34function docRecord( rkey: string, site: string, extra: Record< string, unknown > = {} ) { 35 return { 36 uri: `at://${ DID }/site.standard.document/${ rkey }`, 37 cid: 'c', 38 value: { title: `T${ rkey }`, site, ...extra }, 39 }; 40} 41 42/** Default happy-path PDS: one resolvable writer, two publications, two documents. */ 43function wirePds( { 44 publications = [ 45 pubRecord( 'a', `${ SITE_BASE }/@${ HANDLE }/blog`, { icon: ICON } ), 46 pubRecord( 'b', `${ SITE_BASE }/@${ HANDLE }/notes` ), 47 pubRecord( 'f', 'https://leaflet.pub/x/y', { name: 'Leafy' } ), 48 ], 49 documents = [ 50 docRecord( 'd1', PUB_URI ), 51 docRecord( 'd2', `at://${ DID }/site.standard.publication/b` ), 52 ], 53 profile = { displayName: ' Me ', description: 'bio' } as Record< string, unknown > | null, 54 article = null as ReturnType< typeof docRecord > | null, 55} = {} ) { 56 mockedResolveAuthor.mockResolvedValue( { did: DID, pdsUrl: PDS } ); 57 mockedListRecords.mockImplementation( async ( _pds, _did, collection ) => 58 collection === 'site.standard.publication' ? publications : documents 59 ); 60 mockedGetRecord.mockImplementation( async ( _pds, _did, collection ) => { 61 if ( collection === 'app.bsky.actor.profile' ) { 62 return profile ? { uri: 'at://profile', cid: 'c', value: profile } : null; 63 } 64 return article; 65 } ); 66} 67 68beforeEach( () => { 69 mockedResolveAuthor.mockReset(); 70 mockedGetRecord.mockReset(); 71 mockedListRecords.mockReset(); 72} ); 73 74describe( 'resolveAuthorContext', () => { 75 it( 'maps a param without a leading @ to a generic 404', async () => { 76 const result = await resolveAuthorContext( HANDLE ); 77 expect( result.ok ).toBe( false ); 78 if ( ! result.ok ) { 79 expect( result.error.status ).toBe( 404 ); 80 } 81 expect( mockedResolveAuthor ).not.toHaveBeenCalled(); 82 } ); 83 84 it( 'maps a missing param to a generic 404', async () => { 85 const result = await resolveAuthorContext( undefined ); 86 expect( result.ok ).toBe( false ); 87 } ); 88 89 it( 'maps an unresolvable handle to writer-not-found', async () => { 90 mockedResolveAuthor.mockResolvedValue( null ); 91 const result = await resolveAuthorContext( `@${ HANDLE }` ); 92 expect( result.ok ).toBe( false ); 93 if ( ! result.ok ) { 94 expect( result.error.status ).toBe( 404 ); 95 expect( result.error.heading ).toContain( `@${ HANDLE }` ); 96 } 97 } ); 98 99 it( 'degrades to an empty context when the PDS has no profile or publications', async () => { 100 wirePds( { publications: [], documents: [], profile: null } ); 101 const result = await resolveAuthorContext( `@${ HANDLE }` ); 102 expect( result.ok ).toBe( true ); 103 if ( ! result.ok ) { 104 return; 105 } 106 expect( result.context.publications ).toEqual( [] ); 107 expect( result.context.foreign ).toEqual( [] ); 108 expect( result.context.profile.displayName ).toBeNull(); 109 } ); 110 111 it( 'returns the author, profile, and publications with prebuilt logo URLs', async () => { 112 wirePds(); 113 const result = await resolveAuthorContext( `@${ HANDLE }` ); 114 expect( result.ok ).toBe( true ); 115 if ( ! result.ok ) { 116 return; 117 } 118 const { author, profile, publications, foreign } = result.context; 119 expect( author ).toEqual( { handle: HANDLE, did: DID, pdsUrl: PDS } ); 120 expect( profile.displayName ).toBe( 'Me' ); 121 122 expect( publications ).toHaveLength( 2 ); 123 expect( publications[ 0 ] ).toMatchObject( { 124 slug: 'blog', 125 logoUrl: buildGetBlobUrl( PDS, DID, 'bafyicon' ), 126 } ); 127 // No icon → no logo URL; the caller never touches blob internals. 128 expect( publications[ 1 ].logoUrl ).toBeNull(); 129 130 expect( foreign ).toHaveLength( 1 ); 131 expect( foreign[ 0 ] ).toMatchObject( { name: 'Leafy', logoUrl: null } ); 132 } ); 133} ); 134 135describe( 'resolvePublicationContext', () => { 136 it( 'maps a missing slug to a generic 404', async () => { 137 wirePds(); 138 const result = await resolvePublicationContext( `@${ HANDLE }`, undefined ); 139 expect( result.ok ).toBe( false ); 140 if ( ! result.ok ) { 141 expect( result.error.status ).toBe( 404 ); 142 } 143 } ); 144 145 it( 'maps an unknown slug to publication-not-found', async () => { 146 wirePds(); 147 const result = await resolvePublicationContext( `@${ HANDLE }`, 'missing' ); 148 expect( result.ok ).toBe( false ); 149 if ( ! result.ok ) { 150 expect( result.error.status ).toBe( 404 ); 151 expect( result.error.subline ).toContain( 'missing' ); 152 } 153 } ); 154 155 it( 'returns the publication and only its own documents, newest-first order preserved', async () => { 156 wirePds(); 157 const result = await resolvePublicationContext( `@${ HANDLE }`, 'blog' ); 158 expect( result.ok ).toBe( true ); 159 if ( ! result.ok ) { 160 return; 161 } 162 const { publication, documents } = result.context; 163 expect( publication ).toMatchObject( { 164 uri: PUB_URI, 165 slug: 'blog', 166 logoUrl: buildGetBlobUrl( PDS, DID, 'bafyicon' ), 167 } ); 168 // The site-join (document.value.site === publication.uri) happens here, once. 169 expect( documents ).toHaveLength( 1 ); 170 expect( documents[ 0 ] ).toMatchObject( { rkey: 'd1', value: { title: 'Td1' } } ); 171 } ); 172} ); 173 174describe( 'resolveArticleContext', () => { 175 it( 'maps a missing rkey to a generic 404', async () => { 176 wirePds(); 177 const result = await resolveArticleContext( `@${ HANDLE }`, 'blog', undefined ); 178 expect( result.ok ).toBe( false ); 179 } ); 180 181 it( 'maps a missing record to article-not-found', async () => { 182 wirePds( { article: null } ); 183 const result = await resolveArticleContext( `@${ HANDLE }`, 'blog', 'gone' ); 184 expect( result.ok ).toBe( false ); 185 if ( ! result.ok ) { 186 expect( result.error.status ).toBe( 404 ); 187 expect( result.error.subline ).toContain( 'Pa' ); 188 } 189 } ); 190 191 it( 'rejects a document that belongs to another publication', async () => { 192 // The d2 document exists but its site points at the "notes" publication. 193 wirePds( { article: docRecord( 'd2', `at://${ DID }/site.standard.publication/b` ) } ); 194 const result = await resolveArticleContext( `@${ HANDLE }`, 'blog', 'd2' ); 195 expect( result.ok ).toBe( false ); 196 if ( ! result.ok ) { 197 expect( result.error.status ).toBe( 404 ); 198 } 199 } ); 200 201 it( 'returns the document with its publication and author profile', async () => { 202 wirePds( { 203 article: docRecord( 'd1', PUB_URI, { textContent: 'Hello world', publishedAt: '2026-06-01T00:00:00Z' } ), 204 } ); 205 const result = await resolveArticleContext( `@${ HANDLE }`, 'blog', 'd1' ); 206 expect( result.ok ).toBe( true ); 207 if ( ! result.ok ) { 208 return; 209 } 210 const { author, publication, document, profile } = result.context; 211 expect( author.handle ).toBe( HANDLE ); 212 expect( publication.slug ).toBe( 'blog' ); 213 expect( profile.displayName ).toBe( 'Me' ); 214 expect( document ).toMatchObject( { 215 rkey: 'd1', 216 uri: `at://${ DID }/site.standard.document/d1`, 217 value: { title: 'Td1', textContent: 'Hello world' }, 218 } ); 219 } ); 220 221 it( 'reports publication-not-found before article-not-found', async () => { 222 wirePds( { article: docRecord( 'd1', PUB_URI ) } ); 223 const result = await resolveArticleContext( `@${ HANDLE }`, 'nope', 'd1' ); 224 expect( result.ok ).toBe( false ); 225 if ( ! result.ok ) { 226 expect( result.error.subline ).toContain( 'nope' ); 227 } 228 } ); 229} );