import type { Request, Response } from 'express'; import { type MockProxy, mock } from 'jest-mock-extended'; import type { IExecuteFunctions, INodeProperties, IWebhookFunctions, IWorkflowSettings, } from 'n8n-workflow'; import { NodeOperationError, WAIT_INDEFINITELY } from 'n8n-workflow'; import { configureWaitTillDate } from '../configureWaitTillDate.util'; import { getSendAndWaitProperties, getSendAndWaitConfig, createEmail, sendAndWaitWebhook, } from '../utils'; describe('Send and Wait utils tests', () => { let mockExecuteFunctions: MockProxy; let mockWebhookFunctions: MockProxy; beforeEach(() => { mockExecuteFunctions = mock(); mockWebhookFunctions = mock(); mockWebhookFunctions.getWorkflowSettings.mockReturnValue(mock({})); }); describe('getSendAndWaitProperties', () => { it('should return properties with correct display options', () => { const targetProperties: INodeProperties[] = [ { displayName: 'Test Property', name: 'testProperty', type: 'string', default: '', }, ]; const extraOptions: INodeProperties[] = [ { displayName: 'Extra Property', name: 'extraProperty', type: 'string', default: '', }, ]; const result = getSendAndWaitProperties(targetProperties, undefined, undefined, { extraOptions, }); expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ name: 'options', options: expect.arrayContaining([ expect.objectContaining({ name: 'extraProperty', }), ]), }), ]), ); }); it('should include extra options when provided', () => { const targetProperties: INodeProperties[] = [ { displayName: 'Test Property', name: 'testProperty', type: 'string', default: '', }, ]; const extraOptions: INodeProperties[] = [ { displayName: 'Extra Property', name: 'extraProperty', type: 'string', default: '', }, ]; const result = getSendAndWaitProperties(targetProperties, undefined, undefined, { extraOptions, }); expect(result).toEqual( expect.arrayContaining([ expect.objectContaining({ displayOptions: { show: { resource: ['message'], operation: ['sendAndWait'], }, }, }), ]), ); }); }); describe('getSendAndWaitConfig', () => { it('should return correct config for single approval', () => { mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { message: 'Test message', subject: 'Test subject', 'approvalOptions.values': { approvalType: 'single', approveLabel: 'Approve', buttonApprovalStyle: 'primary', }, }; return params[parameterName]; }); mockExecuteFunctions.getSignedResumeUrl.mockReturnValue( 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc', ); const config = getSendAndWaitConfig(mockExecuteFunctions); expect(config).toEqual({ appendAttribution: undefined, title: 'Test subject', message: 'Test message', options: [ { label: 'Approve', style: 'primary', url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc', }, ], }); }); it('should return correct config for double approval', () => { mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { message: 'Test message', subject: 'Test subject', 'approvalOptions.values': { approvalType: 'double', approveLabel: 'Approve', buttonApprovalStyle: 'primary', disapproveLabel: 'Reject', buttonDisapprovalStyle: 'secondary', }, }; return params[parameterName]; }); mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce( 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc', ); mockExecuteFunctions.getSignedResumeUrl.mockReturnValueOnce( 'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc', ); const config = getSendAndWaitConfig(mockExecuteFunctions); expect(config.options).toHaveLength(2); expect(config.options).toEqual( expect.arrayContaining([ { label: 'Reject', style: 'secondary', url: 'http://localhost/waiting-webhook/nodeID?approved=false&signature=abc', }, { label: 'Approve', style: 'primary', url: 'http://localhost/waiting-webhook/nodeID?approved=true&signature=abc', }, ]), ); }); }); describe('createEmail', () => { beforeEach(() => { mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { sendTo: 'test@example.com', message: 'Test message', subject: 'Test subject', 'approvalOptions.values': { approvalType: 'single', approveLabel: 'Approve', buttonApprovalStyle: 'primary', }, }; return params[parameterName]; }); mockExecuteFunctions.getSignedResumeUrl.mockReturnValue('http://localhost/testNodeId'); }); it('should create a valid email object', () => { const email = createEmail(mockExecuteFunctions); expect(email).toEqual({ to: 'test@example.com', subject: 'Test subject', body: '', htmlBody: expect.stringContaining('Test message'), }); }); it('should throw NodeOperationError for invalid email address', () => { mockExecuteFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { sendTo: 'invalid@@email.com', message: 'Test message', subject: 'Test subject', 'approvalOptions.values': { approvalType: 'single', }, }; return params[parameterName]; }); expect(() => createEmail(mockExecuteFunctions)).toThrow(NodeOperationError); }); }); describe('sendAndWaitWebhook', () => { it('should handle approved webhook', async () => { mockWebhookFunctions.getRequestObject.mockReturnValue({ query: { approved: 'true' }, } as any); const result = await sendAndWaitWebhook.call(mockWebhookFunctions); expect(result).toEqual({ webhookResponse: expect.any(String), workflowData: [[{ json: { data: { approved: true } } }]], }); }); it('should handle disapproved webhook', async () => { mockWebhookFunctions.getRequestObject.mockReturnValue({ query: { approved: 'false' }, } as any); const result = await sendAndWaitWebhook.call(mockWebhookFunctions); expect(result).toEqual({ webhookResponse: expect.any(String), workflowData: [[{ json: { data: { approved: false } } }]], }); }); it('should handle freeText GET webhook', async () => { const mockRender = jest.fn(); const mockSetHeader = jest.fn(); mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'GET', } as any); mockWebhookFunctions.getResponseObject.mockReturnValue({ render: mockRender, setHeader: mockSetHeader, } as any); mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { responseType: 'freeText', message: 'Test message', options: {}, }; return params[parameterName]; }); const result = await sendAndWaitWebhook.call(mockWebhookFunctions); expect(result).toEqual({ noWebhookResponse: true, }); expect(mockSetHeader).toHaveBeenCalledWith( 'Content-Security-Policy', 'sandbox allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols', ); expect(mockRender).toHaveBeenCalledWith('form-trigger', { testRun: false, formTitle: '', formDescription: 'Test message', formDescriptionMetadata: 'Test message', formSubmittedHeader: 'Got it, thanks', formSubmittedText: 'This page can be closed now', n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger', formFields: [ { id: 'field-0', errorId: 'error-field-0', label: 'Response', inputRequired: 'form-required', defaultValue: '', isTextarea: true, }, ], appendAttribution: true, buttonLabel: 'Submit', }); }); it('should handle freeText POST webhook', async () => { mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'POST', } as any); mockWebhookFunctions.getBodyData.mockReturnValue({ data: { 'field-0': 'test value', }, } as any); mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { responseType: 'freeText', }; return params[parameterName]; }); const result = await sendAndWaitWebhook.call(mockWebhookFunctions); expect(result.workflowData).toEqual([[{ json: { data: { text: 'test value' } } }]]); }); it('should handle customForm GET webhook', async () => { const mockRender = jest.fn(); const mockSetHeader = jest.fn(); mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'GET', } as any); mockWebhookFunctions.getResponseObject.mockReturnValue({ render: mockRender, setHeader: mockSetHeader, } as any); mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { responseType: 'customForm', message: 'Test message', defineForm: 'fields', 'formFields.values': [{ label: 'Field 1', fieldType: 'text', requiredField: true }], options: { responseFormTitle: 'Test title', responseFormDescription: 'Test description', responseFormButtonLabel: 'Test button', responseFormCustomCss: 'body { background-color: red; }', }, }; return params[parameterName]; }); const result = await sendAndWaitWebhook.call(mockWebhookFunctions); expect(result).toEqual({ noWebhookResponse: true, }); expect(mockSetHeader).toHaveBeenCalledWith( 'Content-Security-Policy', 'sandbox allow-downloads allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-scripts allow-top-navigation-by-user-activation allow-top-navigation-to-custom-protocols', ); expect(mockRender).toHaveBeenCalledWith('form-trigger', { testRun: false, formTitle: 'Test title', formDescription: 'Test description', formDescriptionMetadata: 'Test description', formSubmittedHeader: 'Got it, thanks', formSubmittedText: 'This page can be closed now', n8nWebsiteLink: 'https://n8n.io/?utm_source=n8n-internal&utm_medium=form-trigger', formFields: [ { id: 'field-0', errorId: 'error-field-0', inputRequired: 'form-required', defaultValue: '', isInput: true, type: 'text', }, ], appendAttribution: true, buttonLabel: 'Test button', dangerousCustomCss: 'body { background-color: red; }', }); }); it('should resolve expressions in HTML fields for customForm GET webhook', async () => { const mockRender = jest.fn(); const mockSetHeader = jest.fn(); mockWebhookFunctions.getRequestObject.mockReturnValue({ method: 'GET', } as any); mockWebhookFunctions.getResponseObject.mockReturnValue({ render: mockRender, setHeader: mockSetHeader, } as any); // Mock evaluateExpression to resolve the expression mockWebhookFunctions.evaluateExpression.mockImplementation((expression) => { if (expression === '{{ $json.videoUrl }}') { return 'https://example.com/video.mp4'; } return expression; }); mockWebhookFunctions.getNodeParameter.mockImplementation((parameterName: string) => { const params: { [key: string]: any } = { responseType: 'customForm', message: 'Test message', defineForm: 'fields', 'formFields.values': [ { fieldLabel: 'Custom HTML', fieldType: 'html', // Use tag inside