-
-
Notifications
You must be signed in to change notification settings - Fork 692
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: added test for build-rss.js (#3101)
Co-authored-by: Akshat Nema <[email protected]>%0ACo-authored-by: Ansh Goyal <[email protected]>%0ACo-authored-by: asyncapi-bot <[email protected]>
- Loading branch information
1 parent
79b2491
commit 311886b
Showing
5 changed files
with
349 additions
and
51 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,8 +1,8 @@ | ||
const fs = require('fs') | ||
const fs = require('fs').promises | ||
const json2xml = require('jgexml/json2xml') | ||
|
||
function getAllPosts() { | ||
return require('../config/posts.json') | ||
return require('../config/posts.json'); | ||
} | ||
|
||
function clean(s) { | ||
|
@@ -15,61 +15,89 @@ function clean(s) { | |
return s | ||
} | ||
|
||
module.exports = function rssFeed(type, title, desc, outputPath) { | ||
module.exports = async function rssFeed(type, title, desc, outputPath) { | ||
try { | ||
|
||
const posts = getAllPosts()[`${type}`] | ||
.sort((i1, i2) => { | ||
const i1Date = new Date(i1.date) | ||
const i2Date = new Date(i2.date) | ||
let posts = getAllPosts()[`${type}`] | ||
const missingDatePosts = posts.filter(post => !post.date); | ||
posts = posts.filter(post => post.date); | ||
posts.sort((i1, i2) => { | ||
const i1Date = new Date(i1.date); | ||
const i2Date = new Date(i2.date); | ||
if (i1.featured && !i2.featured) return -1; | ||
if (!i1.featured && i2.featured) return 1; | ||
return i2Date - i1Date; | ||
}); | ||
|
||
if (i1.featured && !i2.featured) return -1 | ||
if (!i1.featured && i2.featured) return 1 | ||
return i2Date - i1Date | ||
}) | ||
if (missingDatePosts.length > 0) { | ||
throw new Error(`Missing date in posts: ${missingDatePosts.map(p => p.title || p.slug).join(', ')}`); | ||
} | ||
|
||
const base = 'https://www.asyncapi.com' | ||
const tracking = '?utm_source=rss'; | ||
|
||
const feed = {} | ||
const rss = {} | ||
rss['@version'] = '2.0' | ||
rss["@xmlns:atom"] = 'http://www.w3.org/2005/Atom' | ||
rss.channel = {} | ||
rss.channel.title = title | ||
rss.channel.link = `${base}/${outputPath}` | ||
rss.channel["atom:link"] = {} | ||
rss.channel["atom:link"]["@rel"] = 'self' | ||
rss.channel["atom:link"]["@href"] = rss.channel.link | ||
rss.channel["atom:link"]["@type"] = 'application/rss+xml' | ||
rss.channel.description = desc | ||
rss.channel.language = 'en-gb'; | ||
rss.channel.copyright = 'Made with :love: by the AsyncAPI Initiative.'; | ||
rss.channel.webMaster = '[email protected] (AsyncAPI Initiative)' | ||
rss.channel.pubDate = new Date().toUTCString() | ||
rss.channel.generator = 'next.js' | ||
rss.channel.item = [] | ||
|
||
const base = 'https://www.asyncapi.com' | ||
const tracking = '?utm_source=rss'; | ||
const invalidPosts = posts.filter(post => | ||
!post.title || !post.slug || !post.excerpt || !post.date | ||
); | ||
|
||
const feed = {} | ||
const rss = {} | ||
rss['@version'] = '2.0' | ||
rss["@xmlns:atom"] = 'http://www.w3.org/2005/Atom' | ||
rss.channel = {} | ||
rss.channel.title = title | ||
rss.channel.link = `${base}/${outputPath}` | ||
rss.channel["atom:link"] = {} | ||
rss.channel["atom:link"]["@rel"] = 'self' | ||
rss.channel["atom:link"]["@href"] = rss.channel.link | ||
rss.channel["atom:link"]["@type"] = 'application/rss+xml' | ||
rss.channel.description = desc | ||
rss.channel.language = 'en-gb'; | ||
rss.channel.copyright = 'Made with :love: by the AsyncAPI Initiative.'; | ||
rss.channel.webMaster = '[email protected] (AsyncAPI Initiative)' | ||
rss.channel.pubDate = new Date().toUTCString() | ||
rss.channel.generator = 'next.js' | ||
rss.channel.item = [] | ||
if (invalidPosts.length > 0) { | ||
throw new Error(`Missing required fields in posts: ${invalidPosts.map(p => p.title || p.slug).join(', ')}`); | ||
} | ||
|
||
for (let post of posts) { | ||
const link = `${base}${post.slug}${tracking}`; | ||
const item = { title: post.title, description: clean(post.excerpt), link, category: type, guid: { '@isPermaLink': true, '': link }, pubDate: new Date(post.date).toUTCString() } | ||
if (post.cover) { | ||
const enclosure = {}; | ||
enclosure["@url"] = base+post.cover; | ||
enclosure["@length"] = 15026; // dummy value, anything works | ||
enclosure["@type"] = 'image/jpeg'; | ||
if (typeof enclosure["@url"] === 'string') { | ||
let tmp = enclosure["@url"].toLowerCase(); | ||
if (tmp.indexOf('.png')>=0) enclosure["@type"] = 'image/png'; | ||
if (tmp.indexOf('.svg')>=0) enclosure["@type"] = 'image/svg+xml'; | ||
if (tmp.indexOf('.webp')>=0) enclosure["@type"] = 'image/webp'; | ||
for (let post of posts) { | ||
const link = `${base}${post.slug}${tracking}`; | ||
const { title, excerpt, date } = post; | ||
const pubDate = new Date(date).toUTCString(); | ||
const description = clean(excerpt); | ||
const guid = { '@isPermaLink': true, '': link }; | ||
const item = { | ||
title, | ||
description, | ||
link, | ||
category: type, | ||
guid, | ||
pubDate | ||
}; | ||
if (post.cover) { | ||
const enclosure = {}; | ||
enclosure["@url"] = base + post.cover; | ||
enclosure["@length"] = 15026; // dummy value, anything works | ||
enclosure["@type"] = 'image/jpeg'; | ||
if (typeof enclosure["@url"] === 'string') { | ||
let tmp = enclosure["@url"].toLowerCase(); | ||
if (tmp.indexOf('.png') >= 0) enclosure["@type"] = 'image/png'; | ||
if (tmp.indexOf('.svg') >= 0) enclosure["@type"] = 'image/svg+xml'; | ||
if (tmp.indexOf('.webp') >= 0) enclosure["@type"] = 'image/webp'; | ||
} | ||
item.enclosure = enclosure; | ||
} | ||
item.enclosure = enclosure; | ||
rss.channel.item.push(item) | ||
} | ||
rss.channel.item.push(item) | ||
} | ||
|
||
feed.rss = rss | ||
feed.rss = rss | ||
|
||
const xml = json2xml.getXml(feed,'@','',2) | ||
fs.writeFileSync(`./public/${outputPath}`, xml, 'utf8') | ||
const xml = json2xml.getXml(feed, '@', '', 2); | ||
await fs.writeFile(`./public/${outputPath}`, xml, 'utf8'); | ||
} catch (err) { | ||
throw new Error(`Failed to generate RSS feed: ${err.message}`); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const rssFeed = require('../scripts/build-rss'); | ||
const { XMLParser } = require('fast-xml-parser'); | ||
const parser = new XMLParser({ ignoreAttributes: false }); | ||
const { mockRssData, title, type, desc, missingDateMockData, incompletePostMockData } = require('./fixtures/rssData'); | ||
|
||
describe('rssFeed', () => { | ||
const testOutputDir = path.join(__dirname, '..', 'public', 'test-output'); | ||
const outputPath = 'test-output/rss.xml'; | ||
|
||
beforeAll(async () => { | ||
try { | ||
await fs.promises.mkdir(testOutputDir, { recursive: true }); | ||
} catch (err) { | ||
throw new Error(`Error while creating temp dir: ${err.message}`); | ||
} | ||
}); | ||
|
||
afterAll(async () => { | ||
try { | ||
const files = await fs.promises.readdir(testOutputDir); | ||
await Promise.all(files.map(file => fs.promises.unlink(path.join(testOutputDir, file)))); | ||
await fs.promises.rmdir(testOutputDir); | ||
} catch (err) { | ||
throw new Error(`Error while deleting temp dir: ${err.message}`); | ||
} | ||
}); | ||
|
||
afterEach(() => { | ||
jest.resetModules(); | ||
}); | ||
|
||
it('should generate RSS feed and write to file', async () => { | ||
|
||
jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() | ||
|
||
const filePath = path.join(__dirname, '..', 'public', outputPath); | ||
expect(fs.existsSync(filePath)).toBe(true); | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
expect(fileContent).toContain('<rss version="2.0"'); | ||
}); | ||
|
||
it('should prioritize featured posts over non-featured ones', async () => { | ||
jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined(); | ||
|
||
const filePath = path.join(__dirname, '..', 'public', outputPath); | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
|
||
const parsedContent = parser.parse(fileContent); | ||
const itemTitles = parsedContent.rss.channel.item.map(item => item.title); | ||
|
||
expect(itemTitles[0]).toBe('Test Post 1'); | ||
expect(itemTitles[1]).toBe('Another Featured Post'); | ||
|
||
expect(itemTitles[2]).toBe('Post with Special Characters: & < > "'); | ||
expect(itemTitles[3]).toBe('Post with UTC Date Format'); | ||
expect(itemTitles[4]).toBe('Non-Featured Post 1'); | ||
expect(itemTitles[5]).toBe('Non-Featured Post 3'); | ||
}); | ||
|
||
it('should sort posts by date in descending order', async () => { | ||
jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined(); | ||
|
||
const filePath = path.join(__dirname, '..', 'public', outputPath); | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
|
||
const parsedContent = parser.parse(fileContent); | ||
const itemTitles = parsedContent.rss.channel.item.map(item => item.title); | ||
|
||
expect(itemTitles[0]).toBe('Test Post 1'); | ||
expect(itemTitles[1]).toBe('Another Featured Post') | ||
expect(itemTitles[2]).toBe('Post with Special Characters: & < > "'); | ||
expect(itemTitles[3]).toBe('Post with UTC Date Format'); | ||
expect(itemTitles[4]).toBe('Non-Featured Post 1'); | ||
expect(itemTitles[5]).toBe('Non-Featured Post 3'); | ||
}); | ||
|
||
it('should set correct enclosure type based on image extension', async () => { | ||
jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() | ||
|
||
const filePath = path.join(__dirname, '..', 'public', outputPath); | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
|
||
expect(fileContent).toContain('<enclosure url="https://www.asyncapi.com/img/test-cover.png"'); | ||
expect(fileContent).toContain('type="image/png"'); | ||
expect(fileContent).toContain('<enclosure url="https://www.asyncapi.com/img/test-cover.svg"'); | ||
expect(fileContent).toContain('type="image/svg+xml"'); | ||
expect(fileContent).toContain('<enclosure url="https://www.asyncapi.com/img/test-cover.webp"'); | ||
expect(fileContent).toContain('type="image/webp"'); | ||
}); | ||
|
||
it('should catch and handle errors when write operation fails', async () => { | ||
jest.doMock('../config/posts.json', () => mockRssData, { virtual: true }); | ||
|
||
const invalidOutputPath = "invalid/path"; | ||
|
||
await expect(rssFeed(type, title, desc, invalidOutputPath)).rejects.toThrow(/ENOENT|EACCES/); | ||
|
||
}); | ||
|
||
it('should throw an error when posts.json is malformed', async () => { | ||
jest.doMock('../config/posts.json', () => { | ||
return { invalidKey: [] }; | ||
}, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Failed to generate RSS feed'); | ||
|
||
}); | ||
|
||
it('should handle empty posts array', async () => { | ||
const emptyMockData = { blog: [] }; | ||
jest.doMock('../config/posts.json', () => emptyMockData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).resolves.toBeUndefined() | ||
|
||
const filePath = path.join(__dirname, '..', 'public', outputPath); | ||
const fileContent = fs.readFileSync(filePath, 'utf8'); | ||
expect(fileContent).toContain('<rss version="2.0"'); | ||
expect(fileContent).not.toContain('<item>'); | ||
}); | ||
|
||
it('should throw an error when post is missing required fields', async () => { | ||
|
||
jest.doMock('../config/posts.json', () => incompletePostMockData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Missing required fields'); | ||
|
||
}); | ||
|
||
it('should throw an error when a post is missing a date field during sorting', async () => { | ||
|
||
jest.doMock('../config/posts.json', () => missingDateMockData, { virtual: true }); | ||
|
||
await expect(rssFeed(type, title, desc, outputPath)).rejects.toThrow('Failed to generate RSS feed: Missing date in posts: Post without Date'); | ||
|
||
}); | ||
|
||
}); |
Oops, something went wrong.