Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(RSS Feed Trigger Node): Handle empty items gracefully #10855

Merged
merged 2 commits into from
Sep 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 15 additions & 9 deletions packages/nodes-base/nodes/RssFeedRead/RssFeedReadTrigger.node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
import Parser from 'rss-parser';
import moment from 'moment-timezone';

interface PollData {
lastItemDate?: string;
lastTimeChecked?: string;
}

export class RssFeedReadTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'RSS Feed Trigger',
Expand Down Expand Up @@ -39,12 +44,12 @@ export class RssFeedReadTrigger implements INodeType {
};

async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const pollData = this.getWorkflowStaticData('node');
const pollData = this.getWorkflowStaticData('node') as PollData;
const feedUrl = this.getNodeParameter('feedUrl') as string;

const now = moment().utc().format();
const dateToCheck =
(pollData.lastItemDate as string) || (pollData.lastTimeChecked as string) || now;
const dateToCheck = Date.parse(
pollData.lastItemDate ?? pollData.lastTimeChecked ?? moment().utc().format(),
);

if (!feedUrl) {
throw new NodeOperationError(this.getNode(), 'The parameter "URL" has to be set!');
Expand Down Expand Up @@ -73,14 +78,15 @@ export class RssFeedReadTrigger implements INodeType {
return [this.helpers.returnJsonArray(feed.items[0])];
}
feed.items.forEach((item) => {
if (Date.parse(item.isoDate as string) > Date.parse(dateToCheck)) {
if (Date.parse(item.isoDate as string) > dateToCheck) {
returnData.push(item);
}
});
const maxIsoDate = feed.items.reduce((a, b) =>
new Date(a.isoDate as string) > new Date(b.isoDate as string) ? a : b,
).isoDate;
pollData.lastItemDate = maxIsoDate;

if (feed.items.length) {
const maxIsoDate = Math.max(...feed.items.map(({ isoDate }) => Date.parse(isoDate!)));
pollData.lastItemDate = new Date(maxIsoDate).toISOString();
}
}

if (Array.isArray(returnData) && returnData.length !== 0) {
Expand Down
64 changes: 64 additions & 0 deletions packages/nodes-base/nodes/RssFeedRead/test/RssFeedRead.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { mock } from 'jest-mock-extended';
import type { IPollFunctions } from 'n8n-workflow';
import Parser from 'rss-parser';
import { returnJsonArray } from 'n8n-core';
import { RssFeedReadTrigger } from '../RssFeedReadTrigger.node';

jest.mock('rss-parser');

const now = new Date('2024-02-01T01:23:45.678Z');
jest.useFakeTimers({ now });

describe('RssFeedReadTrigger', () => {
describe('poll', () => {
const feedUrl = 'https://example.com/feed';
const lastItemDate = '2022-01-01T00:00:00.000Z';
const newItemDate = '2022-01-02T00:00:00.000Z';

const node = new RssFeedReadTrigger();
const pollFunctions = mock<IPollFunctions>({
helpers: mock({ returnJsonArray }),
});

it('should throw an error if the feed URL is empty', async () => {
pollFunctions.getNodeParameter.mockReturnValue('');

await expect(node.poll.call(pollFunctions)).rejects.toThrowError();

expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).not.toHaveBeenCalled();
});

it('should return new items from the feed', async () => {
const pollData = mock({ lastItemDate });
pollFunctions.getNodeParameter.mockReturnValue(feedUrl);
pollFunctions.getWorkflowStaticData.mockReturnValue(pollData);
(Parser.prototype.parseURL as jest.Mock).mockResolvedValue({
items: [{ isoDate: lastItemDate }, { isoDate: newItemDate }],
});

const result = await node.poll.call(pollFunctions);

expect(result).toEqual([[{ json: { isoDate: newItemDate } }]]);
expect(pollFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).toHaveBeenCalledWith(feedUrl);
expect(pollData.lastItemDate).toEqual(newItemDate);
});

it('should return null if the feed is empty', async () => {
const pollData = mock({ lastItemDate });
pollFunctions.getNodeParameter.mockReturnValue(feedUrl);
pollFunctions.getWorkflowStaticData.mockReturnValue(pollData);
(Parser.prototype.parseURL as jest.Mock).mockResolvedValue({ items: [] });

const result = await node.poll.call(pollFunctions);

expect(result).toEqual(null);
expect(pollFunctions.getWorkflowStaticData).toHaveBeenCalledWith('node');
expect(pollFunctions.getNodeParameter).toHaveBeenCalledWith('feedUrl');
expect(Parser.prototype.parseURL).toHaveBeenCalledWith(feedUrl);
expect(pollData.lastItemDate).toEqual(lastItemDate);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { setup, equalityTest, workflowToTests, getWorkflowFilenames } from '@tes
// eslint-disable-next-line n8n-local-rules/no-unneeded-backticks
const feed = `<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Lorem ipsum feed for an interval of 1 minutes with 3 item(s)]]></title><description><![CDATA[This is a constantly updating lorem ipsum feed]]></description><link>http://example.com/</link><generator>RSS for Node</generator><lastBuildDate>Thu, 09 Feb 2023 13:40:32 GMT</lastBuildDate><pubDate>Thu, 09 Feb 2023 13:40:00 GMT</pubDate><copyright><![CDATA[Michael Bertolacci, licensed under a Creative Commons Attribution 3.0 Unported License.]]></copyright><ttl>1</ttl><item><title><![CDATA[Lorem ipsum 2023-02-09T13:40:00Z]]></title><description><![CDATA[Fugiat excepteur exercitation tempor ut aute sunt pariatur veniam pariatur dolor.]]></description><link>http://example.com/test/1675950000</link><guid isPermaLink="true">http://example.com/test/1675950000</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:40:00 GMT</pubDate></item><item><title><![CDATA[Lorem ipsum 2023-02-09T13:39:00Z]]></title><description><![CDATA[Laboris quis nulla tempor eu ullamco est esse qui aute commodo aliqua occaecat.]]></description><link>http://example.com/test/1675949940</link><guid isPermaLink="true">http://example.com/test/1675949940</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:39:00 GMT</pubDate></item><item><title><![CDATA[Lorem ipsum 2023-02-09T13:38:00Z]]></title><description><![CDATA[Irure labore dolor dolore sint aliquip eu anim aute anim et nulla adipisicing nostrud.]]></description><link>http://example.com/test/1675949880</link><guid isPermaLink="true">http://example.com/test/1675949880</guid><dc:creator><![CDATA[John Smith]]></dc:creator><pubDate>Thu, 09 Feb 2023 13:38:00 GMT</pubDate></item></channel></rss>`;

describe('Test HTTP Request Node', () => {
describe('Test RSS Feed Trigger Node', () => {
const workflows = getWorkflowFilenames(__dirname);
const tests = workflowToTests(workflows);

Expand Down
Loading