diff --git a/packages/nodes-base/nodes/Twitter/V2/GenericFunctions.ts b/packages/nodes-base/nodes/Twitter/V2/GenericFunctions.ts index 33f238add..117ca8738 100644 --- a/packages/nodes-base/nodes/Twitter/V2/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Twitter/V2/GenericFunctions.ts @@ -83,12 +83,19 @@ export function returnId(tweetId: INodeParameterResourceLocator) { if (tweetId.mode === 'id') { return tweetId.value as string; } else if (tweetId.mode === 'url') { - const value = tweetId.value as string; - const tweetIdMatch = value.includes('lists') - ? value.match(/^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/list(s)?\/(\d+)$/) - : value.match(/^https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(es)?\/(\d+)$/); - - return tweetIdMatch?.[3] as string; + try { + const url = new URL(tweetId.value as string); + if (!/(twitter|x).com$/.test(url.hostname)) { + throw new ApplicationError('Invalid domain'); + } + const parts = url.pathname.split('/'); + if (parts.length !== 4 || parts[2] !== 'status' || !/^\d+$/.test(parts[3])) { + throw new ApplicationError('Invalid path'); + } + return parts[3]; + } catch (error) { + throw new ApplicationError('Not a valid tweet url', { level: 'warning', cause: error }); + } } else { throw new ApplicationError(`The mode ${tweetId.mode} is not valid!`, { level: 'warning' }); } diff --git a/packages/nodes-base/nodes/Twitter/test/Twitter.test.ts b/packages/nodes-base/nodes/Twitter/test/Twitter.test.ts index ff0c8fa1e..fc7486ce5 100644 --- a/packages/nodes-base/nodes/Twitter/test/Twitter.test.ts +++ b/packages/nodes-base/nodes/Twitter/test/Twitter.test.ts @@ -1,4 +1,6 @@ import nock from 'nock'; +import type { INodeParameterResourceLocator } from 'n8n-workflow'; +import { returnId } from '../V2/GenericFunctions'; import { getWorkflowFilenames, testWorkflows } from '@test/nodes/Helpers'; const searchResult = { @@ -66,6 +68,7 @@ const searchResult = { const meResult = { data: { id: '1285192200213626880', name: 'Integration-n8n', username: 'IntegrationN8n' }, }; + describe('Test Twitter Request Node', () => { beforeAll(() => { const baseUrl = 'https://api.twitter.com/2'; @@ -85,3 +88,60 @@ describe('Test Twitter Request Node', () => { const workflows = getWorkflowFilenames(__dirname); testWorkflows(workflows); }); + +describe('X / Twitter Node unit tests', () => { + describe('returnId', () => { + it('should return the id when mode is id', () => { + const tweetId: INodeParameterResourceLocator = { + __rl: true, + mode: 'id', + value: '12345', + }; + expect(returnId(tweetId)).toBe('12345'); + }); + + it('should extract the tweetId from url when the domain is twitter.com', () => { + const tweetId: INodeParameterResourceLocator = { + __rl: true, + mode: 'url', + value: 'https://twitter.com/user/status/12345?utm=6789', + }; + expect(returnId(tweetId)).toBe('12345'); + }); + + it('should extract the tweetId from url when the domain is x.com', () => { + const tweetId: INodeParameterResourceLocator = { + __rl: true, + mode: 'url', + value: 'https://x.com/user/status/12345?utm=6789', + }; + expect(returnId(tweetId)).toBe('12345'); + }); + + it('should throw an error when mode is not valid', () => { + const tweetId: INodeParameterResourceLocator = { + __rl: true, + mode: 'invalid', + value: 'https://twitter.com/user/status/12345', + }; + expect(() => returnId(tweetId)).toThrow(); + }); + + describe('should throw an error when the URL is not valid', () => { + test.each([ + 'https://twitter.com/user/', + 'https://twitter.com/user/status/', + 'https://twitter.com/user/profile/12345', + 'https://twitter.com/search?param=12345', + ])('%s', (value) => { + expect(() => + returnId({ + __rl: true, + mode: 'url', + value, + }), + ).toThrow(); + }); + }); + }); +});