From c55df63abc234ace6ac8e54ed094d10797671264 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 17 Sep 2024 15:09:35 +0200 Subject: [PATCH] fix(RSS Feed Trigger Node): Handle empty items gracefully (#10855) --- .../RssFeedRead/RssFeedReadTrigger.node.ts | 24 ++++--- .../RssFeedRead/test/RssFeedRead.test.ts | 64 +++++++++++++++++++ .../RssFeedRead/test/node/RssFeedRead.test.ts | 2 +- 3 files changed, 80 insertions(+), 10 deletions(-) create mode 100644 packages/nodes-base/nodes/RssFeedRead/test/RssFeedRead.test.ts diff --git a/packages/nodes-base/nodes/RssFeedRead/RssFeedReadTrigger.node.ts b/packages/nodes-base/nodes/RssFeedRead/RssFeedReadTrigger.node.ts index b956a4e18d..a0607f3b86 100644 --- a/packages/nodes-base/nodes/RssFeedRead/RssFeedReadTrigger.node.ts +++ b/packages/nodes-base/nodes/RssFeedRead/RssFeedReadTrigger.node.ts @@ -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', @@ -39,12 +44,12 @@ export class RssFeedReadTrigger implements INodeType { }; async poll(this: IPollFunctions): Promise { - 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!'); @@ -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) { diff --git a/packages/nodes-base/nodes/RssFeedRead/test/RssFeedRead.test.ts b/packages/nodes-base/nodes/RssFeedRead/test/RssFeedRead.test.ts new file mode 100644 index 0000000000..0f7b13fbe9 --- /dev/null +++ b/packages/nodes-base/nodes/RssFeedRead/test/RssFeedRead.test.ts @@ -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({ + 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); + }); + }); +}); diff --git a/packages/nodes-base/nodes/RssFeedRead/test/node/RssFeedRead.test.ts b/packages/nodes-base/nodes/RssFeedRead/test/node/RssFeedRead.test.ts index 62806e448c..7238dcfa00 100644 --- a/packages/nodes-base/nodes/RssFeedRead/test/node/RssFeedRead.test.ts +++ b/packages/nodes-base/nodes/RssFeedRead/test/node/RssFeedRead.test.ts @@ -4,7 +4,7 @@ import { setup, equalityTest, workflowToTests, getWorkflowFilenames } from '@tes // eslint-disable-next-line n8n-local-rules/no-unneeded-backticks const feed = `<![CDATA[Lorem ipsum feed for an interval of 1 minutes with 3 item(s)]]>http://example.com/RSS for NodeThu, 09 Feb 2023 13:40:32 GMTThu, 09 Feb 2023 13:40:00 GMT1<![CDATA[Lorem ipsum 2023-02-09T13:40:00Z]]>http://example.com/test/1675950000http://example.com/test/1675950000Thu, 09 Feb 2023 13:40:00 GMT<![CDATA[Lorem ipsum 2023-02-09T13:39:00Z]]>http://example.com/test/1675949940http://example.com/test/1675949940Thu, 09 Feb 2023 13:39:00 GMT<![CDATA[Lorem ipsum 2023-02-09T13:38:00Z]]>http://example.com/test/1675949880http://example.com/test/1675949880Thu, 09 Feb 2023 13:38:00 GMT`; -describe('Test HTTP Request Node', () => { +describe('Test RSS Feed Trigger Node', () => { const workflows = getWorkflowFilenames(__dirname); const tests = workflowToTests(workflows);