rss.test.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. import assert from 'node:assert/strict';
  2. import { describe, it } from 'node:test';
  3. import { z } from 'astro/zod';
  4. import rss, { getRssString } from '../dist/index.js';
  5. import { rssSchema } from '../dist/schema.js';
  6. import {
  7. description,
  8. phpFeedItem,
  9. phpFeedItemWithContent,
  10. phpFeedItemWithCustomData,
  11. site,
  12. title,
  13. web1FeedItem,
  14. web1FeedItemWithAllData,
  15. web1FeedItemWithContent,
  16. parseXmlString,
  17. } from './test-utils.js';
  18. // note: I spent 30 minutes looking for a nice node-based snapshot tool
  19. // ...and I gave up. Enjoy big strings!
  20. // prettier-ignore
  21. 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>`;
  22. // prettier-ignore
  23. 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>`;
  24. // prettier-ignore
  25. 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>`;
  26. // prettier-ignore
  27. 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>`;
  28. // prettier-ignore
  29. 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>`;
  30. // prettier-ignore
  31. 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>`;
  32. function assertXmlDeepEqual(a, b) {
  33. const parsedA = parseXmlString(a);
  34. const parsedB = parseXmlString(b);
  35. assert.equal(parsedA.err, null);
  36. assert.equal(parsedB.err, null);
  37. assert.deepEqual(parsedA.result, parsedB.result);
  38. }
  39. describe('rss', () => {
  40. it('should return a response', async () => {
  41. const response = await rss({
  42. title,
  43. description,
  44. items: [phpFeedItem, web1FeedItem],
  45. site,
  46. });
  47. const str = await response.text();
  48. // NOTE: Chai used the below parser to perform the tests, but I have omitted it for now.
  49. // parser = new xml2js.Parser({ trim: flag(this, 'deep') });
  50. assertXmlDeepEqual(str, validXmlResult);
  51. const contentType = response.headers.get('Content-Type');
  52. assert.equal(contentType, 'application/xml');
  53. });
  54. it('should be the same string as getRssString', async () => {
  55. const options = {
  56. title,
  57. description,
  58. items: [phpFeedItem, web1FeedItem],
  59. site,
  60. };
  61. const response = await rss(options);
  62. const str1 = await response.text();
  63. const str2 = await getRssString(options);
  64. assert.equal(str1, str2);
  65. });
  66. });
  67. describe('getRssString', () => {
  68. it('should generate on valid RSSFeedItem array', async () => {
  69. const str = await getRssString({
  70. title,
  71. description,
  72. items: [phpFeedItem, web1FeedItem],
  73. site,
  74. });
  75. assertXmlDeepEqual(str, validXmlResult);
  76. });
  77. it('should generate on valid RSSFeedItem array with HTML content included', async () => {
  78. const str = await getRssString({
  79. title,
  80. description,
  81. items: [phpFeedItemWithContent, web1FeedItemWithContent],
  82. site,
  83. });
  84. assertXmlDeepEqual(str, validXmlWithContentResult);
  85. });
  86. it('should generate on valid RSSFeedItem array with all RSS content included', async () => {
  87. const str = await getRssString({
  88. title,
  89. description,
  90. items: [phpFeedItem, web1FeedItemWithAllData],
  91. site,
  92. });
  93. assertXmlDeepEqual(str, validXmlResultWithAllData);
  94. });
  95. it('should generate on valid RSSFeedItem array with custom data included', async () => {
  96. const str = await getRssString({
  97. xmlns: {
  98. dc: 'http://purl.org/dc/elements/1.1/',
  99. },
  100. title,
  101. description,
  102. items: [phpFeedItemWithCustomData, web1FeedItemWithContent],
  103. site,
  104. });
  105. assertXmlDeepEqual(str, validXmlWithCustomDataResult);
  106. });
  107. it('should include xml-stylesheet instruction when stylesheet is defined', async () => {
  108. const str = await getRssString({
  109. title,
  110. description,
  111. items: [],
  112. site,
  113. stylesheet: '/feedstylesheet.css',
  114. });
  115. assertXmlDeepEqual(str, validXmlWithStylesheet);
  116. });
  117. it('should include xml-stylesheet instruction with xsl type when stylesheet is set to xsl file', async () => {
  118. const str = await getRssString({
  119. title,
  120. description,
  121. items: [],
  122. site,
  123. stylesheet: '/feedstylesheet.xsl',
  124. });
  125. assertXmlDeepEqual(str, validXmlWithXSLStylesheet);
  126. });
  127. it('should preserve self-closing tags on `customData`', async () => {
  128. const customData =
  129. '<atom:link href="https://example.com/feed.xml" rel="self" type="application/rss+xml"/>';
  130. const str = await getRssString({
  131. title,
  132. description,
  133. items: [],
  134. site,
  135. xmlns: {
  136. atom: 'http://www.w3.org/2005/Atom',
  137. },
  138. customData,
  139. });
  140. assert.ok(str.includes(customData));
  141. });
  142. it('should not append trailing slash to URLs with the given option', async () => {
  143. const str = await getRssString({
  144. title,
  145. description,
  146. items: [phpFeedItem],
  147. site,
  148. trailingSlash: false,
  149. });
  150. assert.ok(str.includes('https://example.com/<'));
  151. assert.ok(str.includes('https://example.com/php<'));
  152. });
  153. it('Deprecated import.meta.glob mapping still works', async () => {
  154. const globResult = {
  155. './posts/php.md': () =>
  156. new Promise((resolve) =>
  157. resolve({
  158. url: phpFeedItem.link,
  159. frontmatter: {
  160. title: phpFeedItem.title,
  161. pubDate: phpFeedItem.pubDate,
  162. description: phpFeedItem.description,
  163. },
  164. })
  165. ),
  166. './posts/nested/web1.md': () =>
  167. new Promise((resolve) =>
  168. resolve({
  169. url: web1FeedItem.link,
  170. frontmatter: {
  171. title: web1FeedItem.title,
  172. pubDate: web1FeedItem.pubDate,
  173. description: web1FeedItem.description,
  174. },
  175. })
  176. ),
  177. };
  178. const str = await getRssString({
  179. title,
  180. description,
  181. items: globResult,
  182. site,
  183. });
  184. assertXmlDeepEqual(str, validXmlResult);
  185. });
  186. it('should fail when an invalid date string is provided', async () => {
  187. const res = rssSchema.safeParse({
  188. title: phpFeedItem.title,
  189. pubDate: 'invalid date',
  190. description: phpFeedItem.description,
  191. link: phpFeedItem.link,
  192. });
  193. assert.equal(res.success, false);
  194. assert.equal(res.error.issues[0].path[0], 'pubDate');
  195. });
  196. it('should be extendable', () => {
  197. let error = null;
  198. try {
  199. rssSchema.extend({
  200. category: z.string().optional(),
  201. });
  202. } catch (e) {
  203. error = e.message;
  204. }
  205. assert.equal(error, null);
  206. });
  207. it('should not fail when an enclosure has a length of 0', async () => {
  208. let error = null;
  209. try {
  210. await getRssString({
  211. title,
  212. description,
  213. items: [
  214. {
  215. title: 'Title',
  216. pubDate: new Date().toISOString(),
  217. description: 'Description',
  218. link: '/link',
  219. enclosure: {
  220. url: '/enclosure',
  221. length: 0,
  222. type: 'audio/mpeg',
  223. },
  224. },
  225. ],
  226. site,
  227. });
  228. } catch (e) {
  229. error = e.message;
  230. }
  231. assert.equal(error, null);
  232. });
  233. });