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