import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { z } from 'astro/zod'; import rss, { getRssString } from '../dist/index.js'; import { rssSchema } from '../dist/schema.js'; import { description, parseXmlString, phpFeedItem, phpFeedItemWithContent, phpFeedItemWithCustomData, site, title, web1FeedItem, web1FeedItemWithAllData, web1FeedItemWithContent, } from './test-utils.js'; // note: I spent 30 minutes looking for a nice node-based snapshot tool // ...and I gave up. Enjoy big strings! // biome-ignore format: keep in one line const validXmlResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItem.title}]]>${site}${web1FeedItem.link}/${site}${web1FeedItem.link}/${new Date(web1FeedItem.pubDate).toUTCString()}`; // biome-ignore format: keep in one line const validXmlWithContentResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithContent.title}]]>${site}${phpFeedItemWithContent.link}/${site}${phpFeedItemWithContent.link}/${new Date(phpFeedItemWithContent.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // biome-ignore format: keep in one line const validXmlResultWithAllData = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItem.title}]]>${site}${phpFeedItem.link}/${site}${phpFeedItem.link}/${new Date(phpFeedItem.pubDate).toUTCString()}<![CDATA[${web1FeedItemWithAllData.title}]]>${site}${web1FeedItemWithAllData.link}/${site}${web1FeedItemWithAllData.link}/${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}${web1FeedItemWithAllData.categories[0]}${web1FeedItemWithAllData.categories[1]}${web1FeedItemWithAllData.author}${web1FeedItemWithAllData.commentsUrl}${web1FeedItemWithAllData.source.title}`; // biome-ignore format: keep in one line const validXmlWithCustomDataResult = `<![CDATA[${title}]]>${site}/<![CDATA[${phpFeedItemWithCustomData.title}]]>${site}${phpFeedItemWithCustomData.link}/${site}${phpFeedItemWithCustomData.link}/${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}${phpFeedItemWithCustomData.customData}<![CDATA[${web1FeedItemWithContent.title}]]>${site}${web1FeedItemWithContent.link}/${site}${web1FeedItemWithContent.link}/${new Date(web1FeedItemWithContent.pubDate).toUTCString()}`; // biome-ignore format: keep in one line const validXmlWithStylesheet = `<![CDATA[${title}]]>${site}/`; // biome-ignore format: keep in one line const validXmlWithXSLStylesheet = `<![CDATA[${title}]]>${site}/`; function assertXmlDeepEqual(a, b) { const parsedA = parseXmlString(a); const parsedB = parseXmlString(b); assert.equal(parsedA.err, null); assert.equal(parsedB.err, null); assert.deepEqual(parsedA.result, parsedB.result); } describe('rss', () => { it('should return a response', async () => { const response = await rss({ title, description, items: [phpFeedItem, web1FeedItem], site, }); const str = await response.text(); // NOTE: Chai used the below parser to perform the tests, but I have omitted it for now. // parser = new xml2js.Parser({ trim: flag(this, 'deep') }); assertXmlDeepEqual(str, validXmlResult); const contentType = response.headers.get('Content-Type'); assert.equal(contentType, 'application/xml'); }); it('should be the same string as getRssString', async () => { const options = { title, description, items: [phpFeedItem, web1FeedItem], site, }; const response = await rss(options); const str1 = await response.text(); const str2 = await getRssString(options); assert.equal(str1, str2); }); }); describe('getRssString', () => { it('should generate on valid RSSFeedItem array', async () => { const str = await getRssString({ title, description, items: [phpFeedItem, web1FeedItem], site, }); assertXmlDeepEqual(str, validXmlResult); }); it('should generate on valid RSSFeedItem array with HTML content included', async () => { const str = await getRssString({ title, description, items: [phpFeedItemWithContent, web1FeedItemWithContent], site, }); assertXmlDeepEqual(str, validXmlWithContentResult); }); it('should generate on valid RSSFeedItem array with all RSS content included', async () => { const str = await getRssString({ title, description, items: [phpFeedItem, web1FeedItemWithAllData], site, }); assertXmlDeepEqual(str, validXmlResultWithAllData); }); it('should generate on valid RSSFeedItem array with custom data included', async () => { const str = await getRssString({ xmlns: { dc: 'http://purl.org/dc/elements/1.1/', }, title, description, items: [phpFeedItemWithCustomData, web1FeedItemWithContent], site, }); assertXmlDeepEqual(str, validXmlWithCustomDataResult); }); it('should include xml-stylesheet instruction when stylesheet is defined', async () => { const str = await getRssString({ title, description, items: [], site, stylesheet: '/feedstylesheet.css', }); assertXmlDeepEqual(str, validXmlWithStylesheet); }); it('should include xml-stylesheet instruction with xsl type when stylesheet is set to xsl file', async () => { const str = await getRssString({ title, description, items: [], site, stylesheet: '/feedstylesheet.xsl', }); assertXmlDeepEqual(str, validXmlWithXSLStylesheet); }); it('should preserve self-closing tags on `customData`', async () => { const customData = ''; const str = await getRssString({ title, description, items: [], site, xmlns: { atom: 'http://www.w3.org/2005/Atom', }, customData, }); assert.ok(str.includes(customData)); }); it('should not append trailing slash to URLs with the given option', async () => { const str = await getRssString({ title, description, items: [phpFeedItem], site, trailingSlash: false, }); assert.ok(str.includes('https://example.com/<')); assert.ok(str.includes('https://example.com/php<')); }); it('Deprecated import.meta.glob mapping still works', async () => { const globResult = { './posts/php.md': () => new Promise((resolve) => resolve({ url: phpFeedItem.link, frontmatter: { title: phpFeedItem.title, pubDate: phpFeedItem.pubDate, description: phpFeedItem.description, }, }) ), './posts/nested/web1.md': () => new Promise((resolve) => resolve({ url: web1FeedItem.link, frontmatter: { title: web1FeedItem.title, pubDate: web1FeedItem.pubDate, description: web1FeedItem.description, }, }) ), }; const str = await getRssString({ title, description, items: globResult, site, }); assertXmlDeepEqual(str, validXmlResult); }); it('should fail when an invalid date string is provided', async () => { const res = rssSchema.safeParse({ title: phpFeedItem.title, pubDate: 'invalid date', description: phpFeedItem.description, link: phpFeedItem.link, }); assert.equal(res.success, false); assert.equal(res.error.issues[0].path[0], 'pubDate'); }); it('should be extendable', () => { let error = null; try { rssSchema.extend({ category: z.string().optional(), }); } catch (e) { error = e.message; } assert.equal(error, null); }); it('should not fail when an enclosure has a length of 0', async () => { let error = null; try { await getRssString({ title, description, items: [ { title: 'Title', pubDate: new Date().toISOString(), description: 'Description', link: '/link', enclosure: { url: '/enclosure', length: 0, type: 'audio/mpeg', }, }, ], site, }); } catch (e) { error = e.message; } assert.equal(error, null); }); });