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 4.8 kB View raw
1import { describe, expect, it } from 'vitest'; 2import { 3 buildPublicationFeedXml, 4 FEED_ITEM_LIMIT, 5 type FeedDocumentValue, 6} from './publication-feed'; 7import type { RepoRecord } from '../reader/records'; 8 9const PUB_URI = 'at://did:plc:alice/site.standard.publication/pub1'; 10const OTHER_PUB_URI = 'at://did:plc:alice/site.standard.publication/pub2'; 11 12const publication = { 13 uri: PUB_URI, 14 name: 'My Publication', 15 description: 'Notes from the quiet hours', 16}; 17 18function doc( 19 rkey: string, 20 value: Partial< FeedDocumentValue > 21): RepoRecord< FeedDocumentValue > { 22 return { 23 uri: `at://did:plc:alice/site.standard.document/${ rkey }`, 24 cid: `cid-${ rkey }`, 25 value: { 26 site: PUB_URI, 27 title: `Doc ${ rkey }`, 28 publishedAt: '2026-06-01T00:00:00Z', 29 content: { blocks: [ { name: 'core/paragraph', attributes: { content: 'Body' } } ] }, 30 ...value, 31 }, 32 }; 33} 34 35function build( documents: RepoRecord< FeedDocumentValue >[] ): string { 36 return buildPublicationFeedXml( { 37 handle: 'alice.test', 38 slug: 'my-pub', 39 pdsUrl: 'https://pds.example', 40 did: 'did:plc:alice', 41 publication, 42 documents, 43 } ); 44} 45 46describe( 'buildPublicationFeedXml', () => { 47 it( 'includes only documents belonging to this publication', () => { 48 const xml = build( [ 49 doc( 'mine', { title: 'Mine', site: PUB_URI } ), 50 doc( 'theirs', { title: 'Theirs', site: OTHER_PUB_URI } ), 51 ] ); 52 expect( xml ).toContain( '<title>Mine</title>' ); 53 expect( xml ).not.toContain( '<title>Theirs</title>' ); 54 } ); 55 56 it( 'sorts items newest-first by publishedAt', () => { 57 const xml = build( [ 58 doc( 'older', { title: 'Older', publishedAt: '2026-01-01T00:00:00Z' } ), 59 doc( 'newer', { title: 'Newer', publishedAt: '2026-12-01T00:00:00Z' } ), 60 ] ); 61 expect( xml.indexOf( 'Newer' ) ).toBeLessThan( xml.indexOf( 'Older' ) ); 62 } ); 63 64 it( `caps the feed at the newest ${ FEED_ITEM_LIMIT } items`, () => { 65 const documents = Array.from( { length: FEED_ITEM_LIMIT + 5 }, ( _, i ) => 66 doc( `d${ i }`, { 67 title: `Doc ${ i }`, 68 // Higher index = newer, so the oldest 5 should be dropped. 69 publishedAt: `2026-06-${ String( i + 1 ).padStart( 2, '0' ) }T00:00:00Z`, 70 } ) 71 ); 72 const xml = build( documents ); 73 expect( ( xml.match( /<item>/g ) ?? [] ).length ).toBe( FEED_ITEM_LIMIT ); 74 expect( xml ).toContain( '<title>Doc 24</title>' ); // newest kept 75 expect( xml ).not.toContain( '<title>Doc 0</title>' ); // oldest dropped 76 } ); 77 78 it( 'drops documents with no publishedAt (cannot be ordered or dated)', () => { 79 const xml = build( [ 80 doc( 'dated', { title: 'Dated', publishedAt: '2026-06-01T00:00:00Z' } ), 81 doc( 'undated', { title: 'Undated', publishedAt: undefined } ), 82 ] ); 83 expect( xml ).toContain( '<title>Dated</title>' ); 84 expect( xml ).not.toContain( '<title>Undated</title>' ); 85 } ); 86 87 it( 'drops documents with an unparseable publishedAt rather than emitting "Invalid Date"', () => { 88 // publishedAt is untrusted PDS data — a truthy-but-bad value must not reach Date(). 89 const xml = build( [ 90 doc( 'good', { title: 'Good', publishedAt: '2026-06-01T00:00:00Z' } ), 91 doc( 'bad', { title: 'Bad', publishedAt: 'not-a-date' } ), 92 ] ); 93 expect( xml ).toContain( '<title>Good</title>' ); 94 expect( xml ).not.toContain( '<title>Bad</title>' ); 95 expect( xml ).not.toContain( 'Invalid Date' ); 96 } ); 97 98 it( 'renders full article HTML into content:encoded via the reader pipeline', () => { 99 const xml = build( [ 100 doc( 'a', { 101 content: { 102 blocks: [ 103 { name: 'core/heading', attributes: { level: 2, content: 'Section' } }, 104 { name: 'core/paragraph', attributes: { content: 'A paragraph.' } }, 105 ], 106 }, 107 } ), 108 ] ); 109 expect( xml ).toContain( '<content:encoded><![CDATA[' ); 110 expect( xml ).toContain( 'A paragraph.' ); 111 expect( xml ).toContain( 'Section' ); 112 } ); 113 114 it( 'links each item to its canonical article URL and self-references the feed', () => { 115 const xml = build( [ doc( 'abc', {} ) ] ); 116 expect( xml ).toContain( 117 '<link>https://skypress.blog/@alice.test/my-pub/abc</link>' 118 ); 119 expect( xml ).toContain( 120 'href="https://skypress.blog/@alice.test/my-pub/rss.xml"' 121 ); 122 expect( xml ).toContain( '<link>https://skypress.blog/@alice.test/my-pub</link>' ); 123 } ); 124 125 it( 'falls back to textContent for the item description when none is stored', () => { 126 const xml = build( [ 127 doc( 'a', { 128 description: undefined, 129 textContent: 'Plain summary text.', 130 content: { blocks: [] }, 131 } ), 132 ] ); 133 expect( xml ).toContain( '<description>Plain summary text.</description>' ); 134 } ); 135 136 it( 'produces a valid empty channel when the publication has no articles', () => { 137 const xml = build( [] ); 138 expect( xml ).toContain( '<title>My Publication</title>' ); 139 expect( xml ).not.toContain( '<item>' ); 140 } ); 141} );