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