summaryrefslogtreecommitdiff
path: root/packages/astro-rss/test
diff options
context:
space:
mode:
Diffstat (limited to 'packages/astro-rss/test')
-rw-r--r--packages/astro-rss/test/pagesGlobToRssItems.test.js122
-rw-r--r--packages/astro-rss/test/rss.test.js284
-rw-r--r--packages/astro-rss/test/test-utils.js69
3 files changed, 475 insertions, 0 deletions
diff --git a/packages/astro-rss/test/pagesGlobToRssItems.test.js b/packages/astro-rss/test/pagesGlobToRssItems.test.js
new file mode 100644
index 000000000..36613c96c
--- /dev/null
+++ b/packages/astro-rss/test/pagesGlobToRssItems.test.js
@@ -0,0 +1,122 @@
+import assert from 'node:assert/strict';
+import { describe, it } from 'node:test';
+
+import { pagesGlobToRssItems } from '../dist/index.js';
+import { phpFeedItem, web1FeedItem } from './test-utils.js';
+
+describe('pagesGlobToRssItems', () => {
+ it('should generate on valid result', 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 items = await pagesGlobToRssItems(globResult);
+ const expected = [
+ {
+ title: phpFeedItem.title,
+ link: phpFeedItem.link,
+ pubDate: new Date(phpFeedItem.pubDate),
+ description: phpFeedItem.description,
+ },
+ {
+ title: web1FeedItem.title,
+ link: web1FeedItem.link,
+ pubDate: new Date(web1FeedItem.pubDate),
+ description: web1FeedItem.description,
+ },
+ ];
+
+ assert.deepEqual(
+ items.sort((a, b) => a.pubDate - b.pubDate),
+ expected,
+ );
+ });
+
+ it('should fail on missing "url"', () => {
+ const globResult = {
+ './posts/php.md': () =>
+ new Promise((resolve) =>
+ resolve({
+ url: undefined,
+ frontmatter: {
+ pubDate: phpFeedItem.pubDate,
+ description: phpFeedItem.description,
+ },
+ }),
+ ),
+ };
+ return assert.rejects(pagesGlobToRssItems(globResult));
+ });
+
+ it('should fail on missing "title" key and "description"', () => {
+ const globResult = {
+ './posts/php.md': () =>
+ new Promise((resolve) =>
+ resolve({
+ url: phpFeedItem.link,
+ frontmatter: {
+ title: undefined,
+ pubDate: phpFeedItem.pubDate,
+ description: undefined,
+ },
+ }),
+ ),
+ };
+ return assert.rejects(pagesGlobToRssItems(globResult));
+ });
+
+ it('should not fail on missing "title" key if "description" is present', () => {
+ const globResult = {
+ './posts/php.md': () =>
+ new Promise((resolve) =>
+ resolve({
+ url: phpFeedItem.link,
+ frontmatter: {
+ title: undefined,
+ pubDate: phpFeedItem.pubDate,
+ description: phpFeedItem.description,
+ },
+ }),
+ ),
+ };
+ return assert.doesNotReject(pagesGlobToRssItems(globResult));
+ });
+
+ it('should not fail on missing "description" key if "title" is present', () => {
+ const globResult = {
+ './posts/php.md': () =>
+ new Promise((resolve) =>
+ resolve({
+ url: phpFeedItem.link,
+ frontmatter: {
+ title: phpFeedItem.title,
+ pubDate: phpFeedItem.pubDate,
+ description: undefined,
+ },
+ }),
+ ),
+ };
+ return assert.doesNotReject(pagesGlobToRssItems(globResult));
+ });
+});
diff --git a/packages/astro-rss/test/rss.test.js b/packages/astro-rss/test/rss.test.js
new file mode 100644
index 000000000..8014f87fe
--- /dev/null
+++ b/packages/astro-rss/test/rss.test.js
@@ -0,0 +1,284 @@
+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,
+ phpFeedItemWithoutDate,
+ 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 = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item><item><title><![CDATA[${web1FeedItem.title}]]></title><link>${site}${web1FeedItem.link}/</link><guid isPermaLink="true">${site}${web1FeedItem.link}/</guid><description><![CDATA[${web1FeedItem.description}]]></description><pubDate>${new Date(web1FeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlWithContentResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithContent.title}]]></title><link>${site}${phpFeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${phpFeedItemWithContent.link}/</guid><description><![CDATA[${phpFeedItemWithContent.description}]]></description><pubDate>${new Date(phpFeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${phpFeedItemWithContent.content}]]></content:encoded></item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlResultWithMissingDate = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithoutDate.title}]]></title><link>${site}${phpFeedItemWithoutDate.link}/</link><guid isPermaLink="true">${site}${phpFeedItemWithoutDate.link}/</guid><description><![CDATA[${phpFeedItemWithoutDate.description}]]></description></item><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlResultWithAllData = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItem.title}]]></title><link>${site}${phpFeedItem.link}/</link><guid isPermaLink="true">${site}${phpFeedItem.link}/</guid><description><![CDATA[${phpFeedItem.description}]]></description><pubDate>${new Date(phpFeedItem.pubDate).toUTCString()}</pubDate></item><item><title><![CDATA[${web1FeedItemWithAllData.title}]]></title><link>${site}${web1FeedItemWithAllData.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithAllData.link}/</guid><description><![CDATA[${web1FeedItemWithAllData.description}]]></description><pubDate>${new Date(web1FeedItemWithAllData.pubDate).toUTCString()}</pubDate><category>${web1FeedItemWithAllData.categories[0]}</category><category>${web1FeedItemWithAllData.categories[1]}</category><author>${web1FeedItemWithAllData.author}</author><comments>${web1FeedItemWithAllData.commentsUrl}</comments><source url="${web1FeedItemWithAllData.source.url}">${web1FeedItemWithAllData.source.title}</source><enclosure url="${site}${web1FeedItemWithAllData.enclosure.url}" length="${web1FeedItemWithAllData.enclosure.length}" type="${web1FeedItemWithAllData.enclosure.type}"/></item></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlWithCustomDataResult = `<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link><item><title><![CDATA[${phpFeedItemWithCustomData.title}]]></title><link>${site}${phpFeedItemWithCustomData.link}/</link><guid isPermaLink="true">${site}${phpFeedItemWithCustomData.link}/</guid><description><![CDATA[${phpFeedItemWithCustomData.description}]]></description><pubDate>${new Date(phpFeedItemWithCustomData.pubDate).toUTCString()}</pubDate>${phpFeedItemWithCustomData.customData}</item><item><title><![CDATA[${web1FeedItemWithContent.title}]]></title><link>${site}${web1FeedItemWithContent.link}/</link><guid isPermaLink="true">${site}${web1FeedItemWithContent.link}/</guid><description><![CDATA[${web1FeedItemWithContent.description}]]></description><pubDate>${new Date(web1FeedItemWithContent.pubDate).toUTCString()}</pubDate><content:encoded><![CDATA[${web1FeedItemWithContent.content}]]></content:encoded></item></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlWithStylesheet = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feedstylesheet.css"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link></channel></rss>`;
+// biome-ignore format: keep in one line
+const validXmlWithXSLStylesheet = `<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/feedstylesheet.xsl" type="text/xsl"?><rss version="2.0"><channel><title><![CDATA[${title}]]></title><description><![CDATA[${description}]]></description><link>${site}/</link></channel></rss>`;
+
+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 missing date', async () => {
+ const str = await getRssString({
+ title,
+ description,
+ items: [phpFeedItemWithoutDate, phpFeedItem],
+ site,
+ });
+
+ assertXmlDeepEqual(str, validXmlResultWithMissingDate);
+ });
+
+ 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 =
+ '<atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>';
+ 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);
+ });
+});
diff --git a/packages/astro-rss/test/test-utils.js b/packages/astro-rss/test/test-utils.js
new file mode 100644
index 000000000..d3ee8ca33
--- /dev/null
+++ b/packages/astro-rss/test/test-utils.js
@@ -0,0 +1,69 @@
+import xml2js from 'xml2js';
+
+export const title = 'My RSS feed';
+export const description = 'This sure is a nice RSS feed';
+export const site = 'https://example.com';
+
+export const phpFeedItemWithoutDate = {
+ link: '/php',
+ title: 'Remember PHP?',
+ description:
+ 'PHP is a general-purpose scripting language geared toward web development. It was originally created by Danish-Canadian programmer Rasmus Lerdorf in 1994.',
+};
+export const phpFeedItem = {
+ ...phpFeedItemWithoutDate,
+ pubDate: '1994-05-03',
+};
+export const phpFeedItemWithContent = {
+ ...phpFeedItem,
+ content: `<h1>${phpFeedItem.title}</h1><p>${phpFeedItem.description}</p>`,
+};
+export const phpFeedItemWithCustomData = {
+ ...phpFeedItem,
+ customData: '<dc:creator><![CDATA[Buster Bluth]]></dc:creator>',
+};
+
+export const web1FeedItem = {
+ // Should support empty string as a URL (possible for homepage route)
+ link: '',
+ title: 'Web 1.0',
+ pubDate: '1997-05-03',
+ description:
+ 'Web 1.0 is the term used for the earliest version of the Internet as it emerged from its origins with Defense Advanced Research Projects Agency (DARPA) and became, for the first time, a global network representing the future of digital communications.',
+};
+export const web1FeedItemWithContent = {
+ ...web1FeedItem,
+ content: `<h1>${web1FeedItem.title}</h1><p>${web1FeedItem.description}</p>`,
+};
+export const web1FeedItemWithAllData = {
+ ...web1FeedItem,
+ categories: ['web1', 'history'],
+ author: 'test@example.com',
+ commentsUrl: 'http://example.com/comments',
+ source: {
+ url: 'http://example.com/source',
+ title: 'The Web 1.0 blog',
+ },
+ enclosure: {
+ url: '/podcast.mp3',
+ length: 256,
+ type: 'audio/mpeg',
+ },
+};
+
+const parser = new xml2js.Parser({ trim: true });
+
+/**
+ *
+ * Utility function to parse an XML string into an object using `xml2js`.
+ *
+ * @param {string} xmlString - Stringified XML to parse.
+ * @return {{ err: Error, result: any }} Represents an option containing the parsed XML string or an Error.
+ */
+export function parseXmlString(xmlString) {
+ let res;
+ parser.parseString(xmlString, (err, result) => {
+ res = { err, result };
+ });
+ return res;
+}