From 9fb1fa1d79f9c09444245878f4faaacf89012275 Mon Sep 17 00:00:00 2001 From: Michael Auerswald Date: Tue, 7 Feb 2023 17:51:11 +0100 Subject: [PATCH] ci: Add E2E tests for Webhook node (#5402) * scheduler e2e test and switch to main mode * adjust e2e options * rolling back config changes * webhook e2e tests and disable cypress retries on openmode * cleanup --- cypress.config.js | 2 +- cypress/e2e/16-webhook-node.cy.ts | 344 +++++++++++++++++++++++++++++ cypress/support/binaryTestFiles.ts | 1 + 3 files changed, 346 insertions(+), 1 deletion(-) create mode 100644 cypress/e2e/16-webhook-node.cy.ts create mode 100644 cypress/support/binaryTestFiles.ts diff --git a/cypress.config.js b/cypress.config.js index 59128107ca..357875df43 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -5,7 +5,7 @@ const BASE_URL = 'http://localhost:5678'; module.exports = defineConfig({ retries: { - openMode: 1, + openMode: 0, runMode: 3, }, e2e: { diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts new file mode 100644 index 0000000000..119ebdd3b4 --- /dev/null +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -0,0 +1,344 @@ +import { WorkflowPage, WorkflowsPage, NDV, CredentialsModal } from '../pages'; +import { v4 as uuid } from 'uuid'; +import { cowBase64 } from '../support/binaryTestFiles'; + +const workflowsPage = new WorkflowsPage(); +const workflowPage = new WorkflowPage(); +const ndv = new NDV(); +const credentialsModal = new CredentialsModal(); + +const webhookWorkflowName = 'Webhook Workflow'; +const waitForWebhook = 500; + +interface SimpleWebhookCallOptions { + method: string; + webhookPath: string; + responseCode?: number; + respondWith?: string; + executeNow?: boolean; + responseData?: string; + authentication?: string; +} + +const simpleWebhookCall = (options: SimpleWebhookCallOptions) => { + const { + authentication, + method, + webhookPath, + responseCode, + respondWith, + responseData, + executeNow = true, + } = options; + + cy.visit(workflowsPage.url); + + workflowsPage.actions.createWorkflowFromCard(); + workflowPage.actions.renameWorkflow(webhookWorkflowName); + workflowPage.actions.addInitialNodeToCanvas('Webhook'); + workflowPage.actions.openNode('Webhook'); + + cy.getByTestId('parameter-input-httpMethod').click(); + cy.getByTestId('parameter-input-httpMethod') + .find('.el-select-dropdown') + .find('.option-headline') + .contains(method) + .click(); + cy.getByTestId('parameter-input-path') + .find('.parameter-input') + .find('input') + .clear() + .type(webhookPath); + + if (authentication) { + cy.getByTestId('parameter-input-authentication').click(); + cy.getByTestId('parameter-input-authentication') + .find('.el-select-dropdown') + .find('.option-headline') + .contains(authentication) + .click(); + } + + if (responseCode) { + cy.getByTestId('parameter-input-responseCode') + .find('.parameter-input') + .find('input') + .clear() + .type(responseCode.toString()); + } + + if (respondWith) { + cy.getByTestId('parameter-input-responseMode').click(); + cy.getByTestId('parameter-input-responseMode') + .find('.el-select-dropdown') + .find('.option-headline') + .contains(respondWith) + .click(); + } + + if (responseData) { + cy.getByTestId('parameter-input-responseData').click(); + cy.getByTestId('parameter-input-responseData') + .find('.el-select-dropdown') + .find('.option-headline') + .contains(responseData) + .click(); + } + + if (executeNow) { + ndv.actions.execute(); + cy.wait(waitForWebhook); + + cy.request(method, '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(200); + ndv.getters.outputPanel().contains('headers'); + }); + } +}; + +describe('Webhook Trigger node', async () => { + before(() => { + cy.resetAll(); + cy.skipSetup(); + }); + + afterEach(() => { + cy.visit(workflowsPage.url); + workflowsPage.actions.deleteWorkFlow(webhookWorkflowName); + }); + + it('should listen for a GET request', () => { + simpleWebhookCall({method: 'GET', webhookPath: uuid(), executeNow: true}); + }); + + it('should listen for a POST request', () => { + simpleWebhookCall({method: 'POST', webhookPath: uuid(), executeNow: true}); + }); + + it('should listen for a DELETE request', () => { + simpleWebhookCall({method: 'DELETE', webhookPath: uuid(), executeNow: true}); + }); + it('should listen for a HEAD request', () => { + simpleWebhookCall({method: 'HEAD', webhookPath: uuid(), executeNow: true}); + }); + it('should listen for a PATCH request', () => { + simpleWebhookCall({method: 'PATCH', webhookPath: uuid(), executeNow: true}); + }); + it('should listen for a PUT request', () => { + simpleWebhookCall({method: 'PUT', webhookPath: uuid(), executeNow: true}); + }); + + it('should listen for a GET request and respond with Respond to Webhook node', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + respondWith: 'Respond to Webhook', + }); + + ndv.getters.backToCanvas().click(); + + workflowPage.actions.addNodeToCanvas('Set'); + workflowPage.actions.openNode('Set'); + cy.get('.add-option').click(); + cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click(); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('MyValue'); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234'); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.addNodeToCanvas('Respond to Webhook'); + + workflowPage.actions.executeWorkflow(); + cy.wait(waitForWebhook); + + cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.eq(1234); + }); + }); + + it('should listen for a GET request and respond custom status code 201', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + responseCode: 201, + }); + + ndv.actions.execute(); + cy.wait(waitForWebhook); + + cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(201); + }); + }); + + it('should listen for a GET request and respond with last node', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + respondWith: 'Last Node', + }); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.addNodeToCanvas('Set'); + workflowPage.actions.openNode('Set'); + cy.get('.add-option').click(); + cy.get('.add-option').find('.el-select-dropdown__item').contains('Number').click(); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('MyValue'); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().type('1234'); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.executeWorkflow(); + cy.wait(waitForWebhook); + + cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.eq(1234); + }); + }); + + it('should listen for a GET request and respond with last node binary data', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + respondWith: 'Last Node', + responseData: 'First Entry Binary', + }); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.addNodeToCanvas('Set'); + workflowPage.actions.openNode('Set'); + cy.get('.add-option').click(); + cy.get('.add-option').find('.el-select-dropdown__item').contains('String').click(); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-name').clear().type('data'); + cy.get('.fixed-collection-parameter').getByTestId('parameter-input-value').clear().find('input').invoke('val', cowBase64).trigger('blur'); + ndv.getters.backToCanvas().click(); + + + workflowPage.actions.addNodeToCanvas('Move Binary Data'); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.openNode('Move Binary Data'); + cy.getByTestId('parameter-input-mode').click(); + cy.getByTestId('parameter-input-mode') + .find('.el-select-dropdown') + .find('.option-headline') + .contains('JSON to Binary') + .click(); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.executeWorkflow(); + cy.wait(waitForWebhook); + + cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(200); + expect(Object.keys(response.body).includes('data')).to.be.true; + }); + }); + + it('should listen for a GET request and respond with an empty body', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + respondWith: 'Last Node', + responseData: 'No Response Body', + }); + ndv.actions.execute(); + cy.wait(waitForWebhook); + cy.request('GET', '/webhook-test/'+ webhookPath).then((response) => { + expect(response.status).to.eq(200); + expect(response.body.MyValue).to.be.undefined; + }); + }); + + it('should listen for a GET request with Basic Authentication', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + authentication: 'Basic Auth', + }); + // add credentials + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.actions.fillCredentialsForm(); + + ndv.actions.execute(); + cy.wait(waitForWebhook); + cy.request({ + method: 'GET', + url: '/webhook-test/'+ webhookPath, + auth: { + 'user': 'username', + 'pass': 'password', + }, + failOnStatusCode: false, + }) + .then((response) => { + expect(response.status).to.eq(403); + }).then(() => { + cy.request({ + method: 'GET', + url: '/webhook-test/'+ webhookPath, + auth: { + 'user': 'test', + 'pass': 'test', + }, + failOnStatusCode: true, + }).then((response) => { + expect(response.status).to.eq(200); + }); + }); + }); + + it('should listen for a GET request with Header Authentication', () => { + const webhookPath = uuid(); + simpleWebhookCall({ + method: 'GET', + webhookPath, + executeNow: false, + authentication: 'Header Auth', + }); + // add credentials + workflowPage.getters.nodeCredentialsSelect().click(); + workflowPage.getters.nodeCredentialsSelect().find('li').last().click(); + credentialsModal.getters.credentialsEditModal().should('be.visible'); + credentialsModal.actions.fillCredentialsForm(); + + ndv.actions.execute(); + cy.wait(waitForWebhook); + cy.request({ + method: 'GET', + url: '/webhook-test/'+ webhookPath, + headers: { + test: 'wrong', + }, + failOnStatusCode: false, + }) + .then((response) => { + expect(response.status).to.eq(403); + }).then(() => { + cy.request({ + method: 'GET', + url: '/webhook-test/'+ webhookPath, + headers: { + test: 'test', + }, + failOnStatusCode: true, + }).then((response) => { + expect(response.status).to.eq(200); + }); + }); + }); +}); diff --git a/cypress/support/binaryTestFiles.ts b/cypress/support/binaryTestFiles.ts new file mode 100644 index 0000000000..bb42cd2101 --- /dev/null +++ b/cypress/support/binaryTestFiles.ts @@ -0,0 +1 @@ +export const cowBase64 = "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"