Skip to content

Commit

Permalink
feat: added test for build-rss.js (#3101)
Browse files Browse the repository at this point in the history
Co-authored-by: Akshat Nema <[email protected]>%0ACo-authored-by: Ansh Goyal <[email protected]>%0ACo-authored-by: asyncapi-bot <[email protected]>
  • Loading branch information
vishvamsinh28 and asyncapi-bot authored Nov 9, 2024
1 parent 79b2491 commit 311886b
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 51 deletions.
29 changes: 29 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@
"remark-cli": "^12.0.1",
"remark-lint": "^10.0.0",
"remark-mdx": "^3.0.1",
"storybook": "^8.2.4"
"storybook": "^8.2.4",
"fast-xml-parser": "^4.5.0"
}
}
128 changes: 78 additions & 50 deletions scripts/build-rss.js
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) {
Expand All @@ -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}`);
}
};
147 changes: 147 additions & 0 deletions tests/build-rss.test.js
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');

});

});
Loading

0 comments on commit 311886b

Please sign in to comment.