-
Notifications
You must be signed in to change notification settings - Fork 7.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
test(Schedule Trigger Node): Add tests and extract trigger test helpe…
…r (no-changelog) (#10625)
- Loading branch information
Showing
3 changed files
with
250 additions
and
84 deletions.
There are no files selected for viewing
156 changes: 156 additions & 0 deletions
156
packages/nodes-base/nodes/Schedule/test/ScheduleTrigger.node.test.ts
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,156 @@ | ||
import * as n8nWorkflow from 'n8n-workflow'; | ||
|
||
import { createTestTriggerNode } from '@test/nodes/TriggerHelpers'; | ||
|
||
import { ScheduleTrigger } from '../ScheduleTrigger.node'; | ||
|
||
describe('ScheduleTrigger', () => { | ||
Object.defineProperty(n8nWorkflow, 'randomInt', { | ||
value: (min: number, max: number) => Math.floor((min + max) / 2), | ||
}); | ||
|
||
const HOUR = 60 * 60 * 1000; | ||
const mockDate = new Date('2023-12-28 12:34:56.789Z'); | ||
const timezone = 'Europe/Berlin'; | ||
|
||
beforeEach(() => { | ||
jest.clearAllMocks(); | ||
jest.useFakeTimers(); | ||
jest.setSystemTime(mockDate); | ||
}); | ||
|
||
describe('trigger', () => { | ||
it('should emit on defined schedule', async () => { | ||
const { emit } = await createTestTriggerNode(ScheduleTrigger).trigger({ | ||
timezone, | ||
node: { parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 3 }] } } }, | ||
workflowStaticData: { recurrenceRules: [] }, | ||
}); | ||
|
||
expect(emit).not.toHaveBeenCalled(); | ||
|
||
jest.advanceTimersByTime(HOUR); | ||
expect(emit).not.toHaveBeenCalled(); | ||
|
||
jest.advanceTimersByTime(2 * HOUR); | ||
expect(emit).toHaveBeenCalledTimes(1); | ||
|
||
const firstTriggerData = emit.mock.calls[0][0][0][0]; | ||
expect(firstTriggerData.json).toEqual({ | ||
'Day of month': '28', | ||
'Day of week': 'Thursday', | ||
Hour: '15', | ||
Minute: '30', | ||
Month: 'December', | ||
'Readable date': 'December 28th 2023, 3:30:30 pm', | ||
'Readable time': '3:30:30 pm', | ||
Second: '30', | ||
Timezone: 'Europe/Berlin (UTC+01:00)', | ||
Year: '2023', | ||
timestamp: '2023-12-28T15:30:30.000+01:00', | ||
}); | ||
|
||
jest.setSystemTime(new Date(firstTriggerData.json.timestamp as string)); | ||
|
||
jest.advanceTimersByTime(2 * HOUR); | ||
expect(emit).toHaveBeenCalledTimes(1); | ||
|
||
jest.advanceTimersByTime(HOUR); | ||
expect(emit).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('should emit on schedule defined as a cron expression', async () => { | ||
const { emit } = await createTestTriggerNode(ScheduleTrigger).trigger({ | ||
timezone, | ||
node: { | ||
parameters: { | ||
rule: { | ||
interval: [ | ||
{ | ||
field: 'cronExpression', | ||
expression: '0 */2 * * *', // every 2 hours | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
workflowStaticData: {}, | ||
}); | ||
|
||
expect(emit).not.toHaveBeenCalled(); | ||
|
||
jest.advanceTimersByTime(2 * HOUR); | ||
expect(emit).toHaveBeenCalledTimes(1); | ||
|
||
jest.advanceTimersByTime(2 * HOUR); | ||
expect(emit).toHaveBeenCalledTimes(2); | ||
}); | ||
|
||
it('should throw on invalid cron expressions', async () => { | ||
await expect( | ||
createTestTriggerNode(ScheduleTrigger).trigger({ | ||
timezone, | ||
node: { | ||
parameters: { | ||
rule: { | ||
interval: [ | ||
{ | ||
field: 'cronExpression', | ||
expression: '100 * * * *', // minute should be 0-59 -> invalid | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
workflowStaticData: {}, | ||
}), | ||
).rejects.toBeInstanceOf(n8nWorkflow.NodeOperationError); | ||
}); | ||
|
||
it('should emit when manually executed', async () => { | ||
const { emit } = await createTestTriggerNode(ScheduleTrigger).triggerManual({ | ||
timezone, | ||
node: { parameters: { rule: { interval: [{ field: 'hours', hoursInterval: 3 }] } } }, | ||
workflowStaticData: { recurrenceRules: [] }, | ||
}); | ||
|
||
expect(emit).toHaveBeenCalledTimes(1); | ||
|
||
const firstTriggerData = emit.mock.calls[0][0][0][0]; | ||
expect(firstTriggerData.json).toEqual({ | ||
'Day of month': '28', | ||
'Day of week': 'Thursday', | ||
Hour: '13', | ||
Minute: '34', | ||
Month: 'December', | ||
'Readable date': 'December 28th 2023, 1:34:56 pm', | ||
'Readable time': '1:34:56 pm', | ||
Second: '56', | ||
Timezone: 'Europe/Berlin (UTC+01:00)', | ||
Year: '2023', | ||
timestamp: '2023-12-28T13:34:56.789+01:00', | ||
}); | ||
}); | ||
|
||
it('should throw on invalid cron expressions in manual mode', async () => { | ||
await expect( | ||
createTestTriggerNode(ScheduleTrigger).triggerManual({ | ||
timezone, | ||
node: { | ||
parameters: { | ||
rule: { | ||
interval: [ | ||
{ | ||
field: 'cronExpression', | ||
expression: '@daily *', // adding extra fields to shorthand not allowed -> invalid | ||
}, | ||
], | ||
}, | ||
}, | ||
}, | ||
workflowStaticData: {}, | ||
}), | ||
).rejects.toBeInstanceOf(n8nWorkflow.NodeOperationError); | ||
}); | ||
}); | ||
}); |
84 changes: 0 additions & 84 deletions
84
packages/nodes-base/nodes/Schedule/tests/ScheduleTrigger.node.test.ts
This file was deleted.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
import { mock } from 'jest-mock-extended'; | ||
import merge from 'lodash/merge'; | ||
import { returnJsonArray, type InstanceSettings } from 'n8n-core'; | ||
import { ScheduledTaskManager } from 'n8n-core/dist/ScheduledTaskManager'; | ||
import type { | ||
IDataObject, | ||
INode, | ||
INodeType, | ||
ITriggerFunctions, | ||
Workflow, | ||
WorkflowExecuteMode, | ||
} from 'n8n-workflow'; | ||
|
||
type MockDeepPartial<T> = Parameters<typeof mock<T>>[0]; | ||
|
||
type TestTriggerNodeOptions = { | ||
node?: MockDeepPartial<INode>; | ||
timezone?: string; | ||
workflowStaticData?: IDataObject; | ||
}; | ||
|
||
type TriggerNodeTypeClass = new () => INodeType & Required<Pick<INodeType, 'trigger'>>; | ||
|
||
export const createTestTriggerNode = (Trigger: TriggerNodeTypeClass) => { | ||
const trigger = new Trigger(); | ||
|
||
const emit: jest.MockedFunction<ITriggerFunctions['emit']> = jest.fn(); | ||
|
||
const setupTriggerFunctions = ( | ||
mode: WorkflowExecuteMode, | ||
options: TestTriggerNodeOptions = {}, | ||
) => { | ||
const timezone = options.timezone ?? 'Europe/Berlin'; | ||
const version = trigger.description.version; | ||
const node = merge( | ||
{ | ||
type: trigger.description.name, | ||
name: trigger.description.defaults.name ?? `Test Node (${trigger.description.name})`, | ||
typeVersion: typeof version === 'number' ? version : version.at(-1), | ||
} satisfies Partial<INode>, | ||
options.node, | ||
) as INode; | ||
const workflow = mock<Workflow>({ timezone: options.timezone ?? 'Europe/Berlin' }); | ||
|
||
const scheduledTaskManager = new ScheduledTaskManager(mock<InstanceSettings>()); | ||
const helpers = mock<ITriggerFunctions['helpers']>({ | ||
returnJsonArray, | ||
registerCron: (cronExpression, onTick) => | ||
scheduledTaskManager.registerCron(workflow, cronExpression, onTick), | ||
}); | ||
|
||
const triggerFunctions = mock<ITriggerFunctions>({ | ||
helpers, | ||
emit, | ||
getTimezone: () => timezone, | ||
getNode: () => node, | ||
getMode: () => mode, | ||
getWorkflowStaticData: () => options.workflowStaticData ?? {}, | ||
getNodeParameter: (parameterName, fallback) => node.parameters[parameterName] ?? fallback, | ||
}); | ||
|
||
return triggerFunctions; | ||
}; | ||
|
||
return { | ||
trigger: async (options: TestTriggerNodeOptions = {}) => { | ||
const triggerFunctions = setupTriggerFunctions('trigger', options); | ||
|
||
const response = await trigger.trigger.call(triggerFunctions); | ||
|
||
expect(response?.manualTriggerFunction).toBeUndefined(); | ||
|
||
return { | ||
close: jest.fn(response?.closeFunction), | ||
emit, | ||
}; | ||
}, | ||
|
||
triggerManual: async (options: TestTriggerNodeOptions = {}) => { | ||
const triggerFunctions = setupTriggerFunctions('manual', options); | ||
|
||
const response = await trigger.trigger.call(triggerFunctions); | ||
|
||
expect(response?.manualTriggerFunction).toBeInstanceOf(Function); | ||
|
||
await response?.manualTriggerFunction?.(); | ||
|
||
return { | ||
close: jest.fn(response?.closeFunction), | ||
emit, | ||
}; | ||
}, | ||
}; | ||
}; |