From 0e071724eec1993b6e55ac985eb4ab9e89189a69 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: Fri, 23 Jun 2023 00:38:12 +0200 Subject: [PATCH] ci: Refactor e2e tests to delete boilerplate code (no-changelog) (#6524) --- .dockerignore | 2 + .github/workflows/ci-pull-requests.yml | 16 +- cypress.config.js | 29 +--- cypress/constants.ts | 29 +++- cypress/e2e/0-smoke.cy.ts | 28 ---- cypress/e2e/1-workflows.cy.ts | 12 -- cypress/e2e/10-settings-log-streaming.cy.ts | 16 +- cypress/e2e/10-undo-redo.cy.ts | 13 +- cypress/e2e/11-inline-expression-editor.cy.ts | 12 -- cypress/e2e/12-canvas-actions.cy.ts | 13 -- cypress/e2e/12-canvas.cy.ts | 15 -- cypress/e2e/13-pinning.cy.ts | 13 -- .../14-data-transformation-expressions.cy.ts | 12 -- cypress/e2e/14-mapping.cy.ts | 13 -- cypress/e2e/15-scheduler-node.cy.ts | 12 -- cypress/e2e/16-webhook-node.cy.ts | 12 -- cypress/e2e/17-sharing.cy.ts | 59 ++----- cypress/e2e/17-workflow-tags.cy.ts | 12 -- cypress/e2e/18-user-management.cy.ts | 86 +++------- cypress/e2e/19-execution.cy.ts | 13 -- cypress/e2e/2-credentials.cy.ts | 15 -- cypress/e2e/20-workflow-executions.cy.ts | 12 -- cypress/e2e/21-community-nodes.cy.ts | 12 +- cypress/e2e/23-variables.cy.ts | 17 +- cypress/e2e/24-ndv-paired-item.cy.ts | 15 +- cypress/e2e/25-stickies.cy.ts | 12 -- cypress/e2e/26-resource-locator.cy.ts | 12 -- cypress/e2e/4-node-creator.cy.ts | 12 -- cypress/e2e/5-ndv.cy.ts | 12 -- cypress/e2e/6-code-node.cy.ts | 12 -- cypress/e2e/7-workflow-actions.cy.ts | 13 -- cypress/e2e/8-http-request-node.cy.ts | 11 +- cypress/e2e/9-expression-editor-modal.cy.ts | 12 -- cypress/pages/index.ts | 2 - cypress/pages/sidebar/main-sidebar.ts | 4 - cypress/pages/signin.ts | 11 -- cypress/pages/signup.ts | 15 -- cypress/support/commands.ts | 153 ++--------------- cypress/support/e2e.ts | 27 +-- cypress/support/index.ts | 26 +-- package.json | 2 - packages/cli/.npmignore | 1 - packages/cli/package.json | 4 +- packages/cli/src/License.ts | 3 +- packages/cli/src/Server.ts | 15 +- packages/cli/src/api/e2e.api.ts | 158 ------------------ packages/cli/src/config/index.ts | 1 - .../cli/src/controllers/e2e.controller.ts | 154 +++++++++++++++++ packages/cli/src/requests.ts | 19 +-- pnpm-lock.yaml | 15 -- 50 files changed, 281 insertions(+), 913 deletions(-) delete mode 100644 cypress/e2e/0-smoke.cy.ts delete mode 100644 cypress/pages/signin.ts delete mode 100644 cypress/pages/signup.ts delete mode 100644 packages/cli/.npmignore delete mode 100644 packages/cli/src/api/e2e.api.ts create mode 100644 packages/cli/src/controllers/e2e.controller.ts diff --git a/.dockerignore b/.dockerignore index d92acf83ba..b907ce51f2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,5 @@ packages/**/.turbo .git .github *.tsbuildinfo +packages/cli/dist/**/e2e.* +packages/cli/dist/ReloadNodesAndCredentials.* diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index b58567227d..99ce69d02e 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -1,4 +1,4 @@ -name: Build, unit/smoke test and lint branch +name: Build, unit test and lint branch on: [pull_request] @@ -108,20 +108,6 @@ jobs: ESLINT_PLUGIN_DIFF_COMMIT: ${{ github.event.pull_request.base.ref }} run: pnpm lint - smoke-test: - name: E2E [Electron/Node 18] - uses: ./.github/workflows/e2e-reusable.yml - with: - branch: ${{ github.event.pull_request.base.ref }} - user: ${{ github.event.inputs.user || 'PR User' }} - spec: ${{ github.event.inputs.spec || 'e2e/0-smoke.cy.ts' }} - record: false - parallel: false - pr_number: ${{ github.event.number }} - containers: '[1]' - secrets: - CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} - checklist_job: runs-on: ubuntu-latest name: Checklist job diff --git a/cypress.config.js b/cypress.config.js index b6cea71083..cdcae02e65 100644 --- a/cypress.config.js +++ b/cypress.config.js @@ -1,10 +1,9 @@ -const fetch = require('node-fetch'); const { defineConfig } = require('cypress'); const BASE_URL = 'http://localhost:5678'; module.exports = defineConfig({ - projectId: "5hbsdn", + projectId: '5hbsdn', retries: { openMode: 0, runMode: 2, @@ -19,31 +18,5 @@ module.exports = defineConfig({ screenshotOnRunFailure: true, experimentalInteractiveRunEvents: true, experimentalSessionAndOrigin: true, - - setupNodeEvents(on, config) { - on('task', { - reset: () => fetch(BASE_URL + '/e2e/db/reset', { method: 'POST' }), - 'setup-owner': (payload) => { - try { - return fetch(BASE_URL + '/e2e/db/setup-owner', { - method: 'POST', - body: JSON.stringify(payload), - headers: { 'Content-Type': 'application/json' }, - }) - } catch (error) { - console.error("setup-owner failed with: ", error) - return null - } - }, - 'set-feature': ({ feature, enabled }) => { - return fetch(BASE_URL + `/e2e/feature/${feature}`, { - method: 'PATCH', - body: JSON.stringify({ enabled }), - headers: { 'Content-Type': 'application/json' } - }) - }, - }); - }, }, }); - diff --git a/cypress/constants.ts b/cypress/constants.ts index a7e2966577..dcb96b5ffd 100644 --- a/cypress/constants.ts +++ b/cypress/constants.ts @@ -1,9 +1,32 @@ -export const BACKEND_BASE_URL = 'http://localhost:5678'; +import { randFirstName, randLastName } from '@ngneat/falso'; +export const BASE_URL = 'http://localhost:5678'; +export const BACKEND_BASE_URL = 'http://localhost:5678'; export const N8N_AUTH_COOKIE = 'n8n-auth'; -export const DEFAULT_USER_EMAIL = 'nathan@n8n.io'; -export const DEFAULT_USER_PASSWORD = 'CypressTest123'; +const DEFAULT_USER_PASSWORD = 'CypressTest123'; + +export const INSTANCE_OWNER = { + email: 'nathan@n8n.io', + password: DEFAULT_USER_PASSWORD, + firstName: randFirstName(), + lastName: randLastName(), +}; + +export const INSTANCE_MEMBERS = [ + { + email: 'rebecca@n8n.io', + password: DEFAULT_USER_PASSWORD, + firstName: randFirstName(), + lastName: randLastName(), + }, + { + email: 'mustafa@n8n.io', + password: DEFAULT_USER_PASSWORD, + firstName: randFirstName(), + lastName: randLastName(), + }, +]; export const MANUAL_TRIGGER_NODE_NAME = 'Manual Trigger'; export const MANUAL_TRIGGER_NODE_DISPLAY_NAME = 'When clicking "Execute Workflow"'; diff --git a/cypress/e2e/0-smoke.cy.ts b/cypress/e2e/0-smoke.cy.ts deleted file mode 100644 index 09d9842922..0000000000 --- a/cypress/e2e/0-smoke.cy.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; -import { randFirstName, randLastName } from '@ngneat/falso'; - -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - -describe('Authentication', () => { - beforeEach(() => { - cy.resetAll(); - }); - - it('should setup owner', () => { - cy.setup({ email, firstName, lastName, password }); - }); - - it('should sign user in', () => { - cy.setupOwner({ email, password, firstName, lastName }); - cy.on('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('Not logged in'); - - return false; - }); - - cy.signin({ email, password }); - }); -}); diff --git a/cypress/e2e/1-workflows.cy.ts b/cypress/e2e/1-workflows.cy.ts index 0dc68cbae9..25f4f3cb0a 100644 --- a/cypress/e2e/1-workflows.cy.ts +++ b/cypress/e2e/1-workflows.cy.ts @@ -1,26 +1,14 @@ import { WorkflowsPage as WorkflowsPageClass } from '../pages/workflows'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { v4 as uuid } from 'uuid'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; const WorkflowsPage = new WorkflowsPageClass(); const WorkflowPage = new WorkflowPageClass(); const multipleWorkflowsCount = 5; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Workflows', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); cy.visit(WorkflowsPage.url); }); diff --git a/cypress/e2e/10-settings-log-streaming.cy.ts b/cypress/e2e/10-settings-log-streaming.cy.ts index 10b1d4d79f..1261940df2 100644 --- a/cypress/e2e/10-settings-log-streaming.cy.ts +++ b/cypress/e2e/10-settings-log-streaming.cy.ts @@ -1,22 +1,8 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { SettingsLogStreamingPage } from '../pages'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); const settingsLogStreamingPage = new SettingsLogStreamingPage(); describe('Log Streaming Settings', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - - beforeEach(() => { - cy.signin({ email, password }); - }); - it('should show the unlicensed view when the feature is disabled', () => { cy.visit('/settings/log-streaming'); settingsLogStreamingPage.getters.getActionBoxUnlicensed().should('be.visible'); @@ -25,7 +11,7 @@ describe('Log Streaming Settings', () => { }); it('should show the licensed view when the feature is enabled', () => { - cy.enableFeature('feat:logStreaming'); + cy.enableFeature('logStreaming'); cy.visit('/settings/log-streaming'); settingsLogStreamingPage.getters.getActionBoxLicensed().should('be.visible'); settingsLogStreamingPage.getters.getAddFirstDestinationButton().should('be.visible'); diff --git a/cypress/e2e/10-undo-redo.cy.ts b/cypress/e2e/10-undo-redo.cy.ts index b8c4113fd7..b197196726 100644 --- a/cypress/e2e/10-undo-redo.cy.ts +++ b/cypress/e2e/10-undo-redo.cy.ts @@ -1,8 +1,7 @@ -import { CODE_NODE_NAME, DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD, SET_NODE_NAME } from './../constants'; +import { CODE_NODE_NAME, SET_NODE_NAME } from './../constants'; import { SCHEDULE_TRIGGER_NODE_NAME } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { NDV } from '../pages/ndv'; -import { randFirstName, randLastName } from '@ngneat/falso'; // Suite-specific constants const CODE_NODE_NEW_NAME = 'Something else'; @@ -10,18 +9,8 @@ const CODE_NODE_NEW_NAME = 'Something else'; const WorkflowPage = new WorkflowPageClass(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Undo/Redo', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 4a4e3794ef..702dd2eac9 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -1,21 +1,9 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - const WorkflowPage = new WorkflowPageClass(); describe('Inline expression editor', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addNodeToCanvas('Hacker News'); diff --git a/cypress/e2e/12-canvas-actions.cy.ts b/cypress/e2e/12-canvas-actions.cy.ts index b485868f05..0ea993f906 100644 --- a/cypress/e2e/12-canvas-actions.cy.ts +++ b/cypress/e2e/12-canvas-actions.cy.ts @@ -6,25 +6,12 @@ import { SET_NODE_NAME, IF_NODE_NAME, HTTP_REQUEST_NODE_NAME, - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, } from './../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; -import { randFirstName, randLastName } from '@ngneat/falso'; - -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); const WorkflowPage = new WorkflowPageClass(); describe('Canvas Actions', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/12-canvas.cy.ts b/cypress/e2e/12-canvas.cy.ts index f7a69e7a55..ced589b33f 100644 --- a/cypress/e2e/12-canvas.cy.ts +++ b/cypress/e2e/12-canvas.cy.ts @@ -5,14 +5,9 @@ import { SCHEDULE_TRIGGER_NODE_NAME, SET_NODE_NAME, SWITCH_NODE_NAME, - IF_NODE_NAME, MERGE_NODE_NAME, - HTTP_REQUEST_NODE_NAME, - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, } from './../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; -import { randFirstName, randLastName } from '@ngneat/falso'; const WorkflowPage = new WorkflowPageClass(); @@ -23,18 +18,8 @@ const ZOOM_OUT_X1_FACTOR = 0.8; const ZOOM_OUT_X2_FACTOR = 0.64; const RENAME_NODE_NAME = 'Something else'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Canvas Node Manipulation and Navigation', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/13-pinning.cy.ts b/cypress/e2e/13-pinning.cy.ts index d94cf3a5fa..7f445b52d2 100644 --- a/cypress/e2e/13-pinning.cy.ts +++ b/cypress/e2e/13-pinning.cy.ts @@ -1,7 +1,4 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; import { - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, HTTP_REQUEST_NODE_NAME, MANUAL_TRIGGER_NODE_NAME, PIPEDRIVE_NODE_NAME, @@ -12,18 +9,8 @@ import { WorkflowPage, NDV } from '../pages'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Data pinning', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); }); diff --git a/cypress/e2e/14-data-transformation-expressions.cy.ts b/cypress/e2e/14-data-transformation-expressions.cy.ts index cbc69e5533..099e79ae7d 100644 --- a/cypress/e2e/14-data-transformation-expressions.cy.ts +++ b/cypress/e2e/14-data-transformation-expressions.cy.ts @@ -1,22 +1,10 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, NDV } from '../pages'; const wf = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Data transformation expressions', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); wf.actions.visit(); cy.window().then( diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 1610500448..951ff9d702 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -2,27 +2,14 @@ import { MANUAL_TRIGGER_NODE_NAME, MANUAL_TRIGGER_NODE_DISPLAY_NAME, SCHEDULE_TRIGGER_NODE_NAME, - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, } from './../constants'; import { WorkflowPage, NDV } from '../pages'; -import { randFirstName, randLastName } from '@ngneat/falso'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Data mapping', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); cy.window().then((win) => { diff --git a/cypress/e2e/15-scheduler-node.cy.ts b/cypress/e2e/15-scheduler-node.cy.ts index 8ad0b8ad95..d58a541652 100644 --- a/cypress/e2e/15-scheduler-node.cy.ts +++ b/cypress/e2e/15-scheduler-node.cy.ts @@ -1,5 +1,3 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, WorkflowsPage, NDV } from '../pages'; import { BACKEND_BASE_URL } from '../constants'; @@ -7,18 +5,8 @@ const workflowsPage = new WorkflowsPage(); const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Schedule Trigger node', async () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); }); diff --git a/cypress/e2e/16-webhook-node.cy.ts b/cypress/e2e/16-webhook-node.cy.ts index 1c17119b31..2ba59a8cfb 100644 --- a/cypress/e2e/16-webhook-node.cy.ts +++ b/cypress/e2e/16-webhook-node.cy.ts @@ -2,13 +2,6 @@ import { WorkflowPage, NDV, CredentialsModal } from '../pages'; import { v4 as uuid } from 'uuid'; import { cowBase64 } from '../support/binaryTestFiles'; import { BACKEND_BASE_URL } from '../constants'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; - -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); const workflowPage = new WorkflowPage(); const ndv = new NDV(); @@ -99,12 +92,7 @@ const simpleWebhookCall = (options: SimpleWebhookCallOptions) => { }; describe('Webhook Trigger node', async () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); cy.window().then((win) => { diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts index 6cfb226005..9af0bb5fe2 100644 --- a/cypress/e2e/17-sharing.cy.ts +++ b/cypress/e2e/17-sharing.cy.ts @@ -1,4 +1,4 @@ -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; +import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import { CredentialsModal, CredentialsPage, @@ -28,47 +28,12 @@ const workflowPage = new WorkflowPage(); const workflowSharingModal = new WorkflowSharingModal(); const ndv = new NDV(); -const instanceOwner = { - email: `${DEFAULT_USER_EMAIL}one`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'U1', -}; - -const users = [ - { - email: `${DEFAULT_USER_EMAIL}two`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'U2', - }, - { - email: `${DEFAULT_USER_EMAIL}three`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'U3', - }, -]; - -describe('Sharing', () => { - before(() => { - cy.setupOwner(instanceOwner); - }); - - beforeEach(() => { - cy.on('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('Not logged in'); - return false; - }); - }); - - it('should invite User U2 and User U3 to instance', () => { - cy.inviteUsers({ instanceOwner, users }); - }); +describe('Sharing', { disableAutoLogin: true }, () => { + before(() => cy.enableFeature('sharing', true)); let workflowW2Url = ''; it('should create C1, W1, W2, share W1 with U3, as U2', () => { - cy.signin(users[0]); + cy.signin(INSTANCE_MEMBERS[0]); cy.visit(credentialsPage.url); credentialsPage.getters.emptyListCreateCredentialButton().click(); @@ -87,7 +52,7 @@ describe('Sharing', () => { ndv.actions.close(); workflowPage.actions.openShareModal(); - workflowSharingModal.actions.addUser(users[1].email); + workflowSharingModal.actions.addUser(INSTANCE_MEMBERS[1].email); workflowSharingModal.actions.save(); workflowPage.actions.saveWorkflowOnButtonClick(); @@ -100,7 +65,7 @@ describe('Sharing', () => { }); it('should create C2, share C2 with U1 and U2, as U3', () => { - cy.signin(users[1]); + cy.signin(INSTANCE_MEMBERS[1]); cy.visit(credentialsPage.url); credentialsPage.getters.emptyListCreateCredentialButton().click(); @@ -109,14 +74,14 @@ describe('Sharing', () => { credentialsModal.getters.connectionParameter('API Key').type('1234567890'); credentialsModal.actions.setName('Credential C2'); credentialsModal.actions.changeTab('Sharing'); - credentialsModal.actions.addUser(instanceOwner.email); - credentialsModal.actions.addUser(users[0].email); + credentialsModal.actions.addUser(INSTANCE_OWNER.email); + credentialsModal.actions.addUser(INSTANCE_MEMBERS[0].email); credentialsModal.actions.save(); credentialsModal.actions.close(); }); it('should open W1, add node using C2 as U3', () => { - cy.signin(users[1]); + cy.signin(INSTANCE_MEMBERS[1]); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 1); @@ -136,7 +101,7 @@ describe('Sharing', () => { }); it('should not have access to W2, as U3', () => { - cy.signin(users[1]); + cy.signin(INSTANCE_MEMBERS[1]); cy.visit(workflowW2Url); cy.waitForLoad(); @@ -145,7 +110,7 @@ describe('Sharing', () => { }); it('should have access to W1, W2, as U1', () => { - cy.signin(instanceOwner); + cy.signin(INSTANCE_OWNER); cy.visit(workflowsPage.url); workflowsPage.getters.workflowCards().should('have.length', 2); @@ -165,7 +130,7 @@ describe('Sharing', () => { }); it('should automatically test C2 when opened by U2 sharee', () => { - cy.signin(users[0]); + cy.signin(INSTANCE_MEMBERS[0]); cy.visit(credentialsPage.url); credentialsPage.getters.credentialCard('Credential C2').click(); diff --git a/cypress/e2e/17-workflow-tags.cy.ts b/cypress/e2e/17-workflow-tags.cy.ts index 8b58657747..56b548747d 100644 --- a/cypress/e2e/17-workflow-tags.cy.ts +++ b/cypress/e2e/17-workflow-tags.cy.ts @@ -1,23 +1,11 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage } from '../pages'; const wf = new WorkflowPage(); const TEST_TAGS = ['Tag 1', 'Tag 2', 'Tag 3', 'Tag 4', 'Tag 5']; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Workflow tags', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); wf.actions.visit(); }); diff --git a/cypress/e2e/18-user-management.cy.ts b/cypress/e2e/18-user-management.cy.ts index 9f51f561d9..6af5ba6b60 100644 --- a/cypress/e2e/18-user-management.cy.ts +++ b/cypress/e2e/18-user-management.cy.ts @@ -1,6 +1,5 @@ -import { MainSidebar } from './../pages/sidebar/main-sidebar'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; -import { SettingsSidebar, SettingsUsersPage, WorkflowPage, WorkflowsPage } from '../pages'; +import { INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; +import { SettingsUsersPage, WorkflowPage } from '../pages'; import { PersonalSettingsPage } from '../pages/settings-personal'; /** @@ -15,28 +14,6 @@ import { PersonalSettingsPage } from '../pages/settings-personal'; * C2 - Credential owned by User C, shared with User A and User B */ -const instanceOwner = { - email: `${DEFAULT_USER_EMAIL}A`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'A', -}; - -const users = [ - { - email: `${DEFAULT_USER_EMAIL}B`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'B', - }, - { - email: `${DEFAULT_USER_EMAIL}C`, - password: DEFAULT_USER_PASSWORD, - firstName: 'User', - lastName: 'C', - }, -]; - const updatedPersonalData = { newFirstName: 'Something', newLastName: 'Else', @@ -49,47 +26,38 @@ const usersSettingsPage = new SettingsUsersPage(); const workflowPage = new WorkflowPage(); const personalSettingsPage = new PersonalSettingsPage(); -describe('User Management', () => { - before(() => { - cy.setupOwner(instanceOwner); - }); - - beforeEach(() => { - cy.on('uncaught:exception', (err, runnable) => { - expect(err.message).to.include('Not logged in'); - return false; - }); - }); - - it(`should invite User B and User C to instance`, () => { - cy.inviteUsers({ instanceOwner, users }); - }); +describe('User Management', { disableAutoLogin: true }, () => { + before(() => cy.enableFeature('sharing')); it('should prevent non-owners to access UM settings', () => { - usersSettingsPage.actions.loginAndVisit(users[0].email, users[0].password, false); + usersSettingsPage.actions.loginAndVisit( + INSTANCE_MEMBERS[0].email, + INSTANCE_MEMBERS[0].password, + false, + ); }); it('should allow instance owner to access UM settings', () => { - usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); + usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); }); it('should properly render UM settings page for instance owners', () => { - usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); + usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); // All items in user list should be there usersSettingsPage.getters.userListItems().should('have.length', 3); // List item for current user should have the `Owner` badge usersSettingsPage.getters - .userItem(instanceOwner.email) + .userItem(INSTANCE_OWNER.email) .find('.n8n-badge:contains("Owner")') .should('exist'); // Other users list items should contain action pop-up list - usersSettingsPage.getters.userActionsToggle(users[0].email).should('exist'); - usersSettingsPage.getters.userActionsToggle(users[1].email).should('exist'); + usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[0].email).should('exist'); + usersSettingsPage.getters.userActionsToggle(INSTANCE_MEMBERS[1].email).should('exist'); }); it('should delete user and their data', () => { - usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); - usersSettingsPage.actions.opedDeleteDialog(users[0].email); + usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); + usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[0].email); usersSettingsPage.getters.deleteDataRadioButton().realClick(); usersSettingsPage.getters.deleteDataInput().type('delete all data'); usersSettingsPage.getters.deleteUserButton().realClick(); @@ -97,8 +65,8 @@ describe('User Management', () => { }); it('should delete user and transfer their data', () => { - usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true); - usersSettingsPage.actions.opedDeleteDialog(users[1].email); + usersSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password, true); + usersSettingsPage.actions.opedDeleteDialog(INSTANCE_MEMBERS[1].email); usersSettingsPage.getters.transferDataRadioButton().realClick(); usersSettingsPage.getters.userSelectDropDown().realClick(); usersSettingsPage.getters.userSelectOptions().first().realClick(); @@ -107,7 +75,7 @@ describe('User Management', () => { }); it(`should allow user to change their personal data`, () => { - personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); + personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.updateFirstAndLastName( updatedPersonalData.newFirstName, updatedPersonalData.newLastName, @@ -119,14 +87,14 @@ describe('User Management', () => { }); it(`shouldn't allow user to set weak password`, () => { - personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); + personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); for (let weakPass of updatedPersonalData.invalidPasswords) { - personalSettingsPage.actions.tryToSetWeakPassword(instanceOwner.password, weakPass); + personalSettingsPage.actions.tryToSetWeakPassword(INSTANCE_OWNER.password, weakPass); } }); it(`shouldn't allow user to change password if old password is wrong`, () => { - personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); + personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.updatePassword('iCannotRemember', updatedPersonalData.newPassword); workflowPage.getters .errorToast() @@ -135,21 +103,21 @@ describe('User Management', () => { }); it(`should change current user password`, () => { - personalSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password); + personalSettingsPage.actions.loginAndVisit(INSTANCE_OWNER.email, INSTANCE_OWNER.password); personalSettingsPage.actions.updatePassword( - instanceOwner.password, + INSTANCE_OWNER.password, updatedPersonalData.newPassword, ); workflowPage.getters.successToast().should('contain', 'Password updated'); personalSettingsPage.actions.loginWithNewData( - instanceOwner.email, + INSTANCE_OWNER.email, updatedPersonalData.newPassword, ); }); it(`shouldn't allow users to set invalid email`, () => { personalSettingsPage.actions.loginAndVisit( - instanceOwner.email, + INSTANCE_OWNER.email, updatedPersonalData.newPassword, ); // try without @ part @@ -160,7 +128,7 @@ describe('User Management', () => { it(`should change user email`, () => { personalSettingsPage.actions.loginAndVisit( - instanceOwner.email, + INSTANCE_OWNER.email, updatedPersonalData.newPassword, ); personalSettingsPage.actions.updateEmail(updatedPersonalData.newEmail); diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index f91c2eb6cd..a6d635de9b 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -1,24 +1,11 @@ import { v4 as uuid } from 'uuid'; import { NDV, WorkflowPage as WorkflowPageClass, WorkflowsPage } from '../pages'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; -const workflowsPage = new WorkflowsPage(); const workflowPage = new WorkflowPageClass(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Execution', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); }); diff --git a/cypress/e2e/2-credentials.cy.ts b/cypress/e2e/2-credentials.cy.ts index 850ba06eb2..7d4f743a98 100644 --- a/cypress/e2e/2-credentials.cy.ts +++ b/cypress/e2e/2-credentials.cy.ts @@ -6,24 +6,14 @@ import { NEW_QUERY_AUTH_ACCOUNT_NAME, } from './../constants'; import { - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, GMAIL_NODE_NAME, NEW_GOOGLE_ACCOUNT_NAME, NEW_TRELLO_ACCOUNT_NAME, SCHEDULE_TRIGGER_NODE_NAME, TRELLO_NODE_NAME, } from '../constants'; -import { randFirstName, randLastName } from '@ngneat/falso'; import { CredentialsPage, CredentialsModal, WorkflowPage, NDV } from '../pages'; -import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json'; -import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; -import CustomCredential from '../fixtures/Custom_credential.json'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); const credentialsPage = new CredentialsPage(); const credentialsModal = new CredentialsModal(); const workflowPage = new WorkflowPage(); @@ -32,12 +22,7 @@ const nodeDetailsView = new NDV(); const NEW_CREDENTIAL_NAME = 'Something else'; describe('Credentials', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); cy.visit(credentialsPage.url); }); diff --git a/cypress/e2e/20-workflow-executions.cy.ts b/cypress/e2e/20-workflow-executions.cy.ts index 4b3ae7e1a7..f4249df062 100644 --- a/cypress/e2e/20-workflow-executions.cy.ts +++ b/cypress/e2e/20-workflow-executions.cy.ts @@ -1,24 +1,12 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage } from '../pages'; import { WorkflowExecutionsTab } from '../pages/workflow-executions-tab'; const workflowPage = new WorkflowPage(); const executionsTab = new WorkflowExecutionsTab(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - // Test suite for executions tab describe('Current Workflow Executions', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); cy.createFixtureWorkflow('Test_workflow_4_executions_view.json', `My test workflow`); createMockExecutions(); diff --git a/cypress/e2e/21-community-nodes.cy.ts b/cypress/e2e/21-community-nodes.cy.ts index bc976330ce..cfc76e46e0 100644 --- a/cypress/e2e/21-community-nodes.cy.ts +++ b/cypress/e2e/21-community-nodes.cy.ts @@ -4,25 +4,15 @@ import { CredentialsModal, WorkflowPage } from '../pages'; import CustomNodeWithN8nCredentialFixture from '../fixtures/Custom_node_n8n_credential.json'; import CustomNodeWithCustomCredentialFixture from '../fixtures/Custom_node_custom_credential.json'; import CustomCredential from '../fixtures/Custom_credential.json'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; const credentialsModal = new CredentialsModal(); const nodeCreatorFeature = new NodeCreator(); const workflowPage = new WorkflowPage(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - // We separate-out the custom nodes because they require injecting nodes and credentials // so the /nodes and /credentials endpoints are intercepted and non-cached. // We want to keep the other tests as fast as possible so we don't want to break the cache in those. describe('Community Nodes', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }) beforeEach(() => { cy.intercept('/types/nodes.json', { middleware: true }, (req) => { req.headers['cache-control'] = 'no-cache, no-store'; @@ -43,7 +33,7 @@ describe('Community Nodes', () => { credentials.push(CustomCredential); }) }) - cy.signin({ email, password }); + workflowPage.actions.visit(); }); diff --git a/cypress/e2e/23-variables.cy.ts b/cypress/e2e/23-variables.cy.ts index 90ccfedae2..ce6a49fb99 100644 --- a/cypress/e2e/23-variables.cy.ts +++ b/cypress/e2e/23-variables.cy.ts @@ -1,22 +1,10 @@ import { VariablesPage } from '../pages/variables'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; -import { randFirstName, randLastName } from '@ngneat/falso'; const variablesPage = new VariablesPage(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Variables', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - it('should show the unlicensed action box when the feature is disabled', () => { - cy.disableFeature('feat:variables'); - cy.signin({ email, password }); + cy.disableFeature('variables', false); cy.visit(variablesPage.url); variablesPage.getters.unavailableResourcesList().should('be.visible'); @@ -25,11 +13,10 @@ describe('Variables', () => { describe('licensed', () => { before(() => { - cy.enableFeature('feat:variables'); + cy.enableFeature('variables'); }); beforeEach(() => { - cy.signin({ email, password }); cy.intercept('GET', '/rest/variables').as('loadVariables'); cy.visit(variablesPage.url); diff --git a/cypress/e2e/24-ndv-paired-item.cy.ts b/cypress/e2e/24-ndv-paired-item.cy.ts index 3fd53cd9f6..3bbd2f0b23 100644 --- a/cypress/e2e/24-ndv-paired-item.cy.ts +++ b/cypress/e2e/24-ndv-paired-item.cy.ts @@ -1,23 +1,11 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, NDV } from '../pages'; import { v4 as uuid } from 'uuid'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('NDV', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); @@ -311,6 +299,9 @@ describe('NDV', () => { .realHover(); ndv.actions.changeOutputRunSelector('1 of 2 (2 items)') + ndv.getters.inputTableRow(1) + .should('have.text', '8888') + .realHover(); ndv.getters.outputHoveringItem().should('have.text', '8888'); // todo there's a bug here need to fix ADO-534 // ndv.getters.outputHoveringItem().should('not.exist'); diff --git a/cypress/e2e/25-stickies.cy.ts b/cypress/e2e/25-stickies.cy.ts index e755d8e134..ad0fe60c35 100644 --- a/cypress/e2e/25-stickies.cy.ts +++ b/cypress/e2e/25-stickies.cy.ts @@ -1,14 +1,7 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; const workflowPage = new WorkflowPageClass(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - function checkStickiesStyle( top: number, left: number, height: number, width: number, zIndex?: number) { workflowPage.getters.stickies().should(($el) => { expect($el).to.have.css('top', `${top}px`); @@ -22,12 +15,7 @@ function checkStickiesStyle( top: number, left: number, height: number, width: n } describe('Canvas Actions', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); cy.window().then( diff --git a/cypress/e2e/26-resource-locator.cy.ts b/cypress/e2e/26-resource-locator.cy.ts index 5b6cb4d3c7..cedcfd628e 100644 --- a/cypress/e2e/26-resource-locator.cy.ts +++ b/cypress/e2e/26-resource-locator.cy.ts @@ -1,5 +1,3 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, NDV, CredentialsModal } from '../pages'; const workflowPage = new WorkflowPage(); @@ -9,18 +7,8 @@ const credentialsModal = new CredentialsModal(); const NO_CREDENTIALS_MESSAGE = 'Please add your credential'; const INVALID_CREDENTIALS_MESSAGE = 'Please check your credential'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Resource Locator', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); }); diff --git a/cypress/e2e/4-node-creator.cy.ts b/cypress/e2e/4-node-creator.cy.ts index bc74d21ed0..35a508f7d7 100644 --- a/cypress/e2e/4-node-creator.cy.ts +++ b/cypress/e2e/4-node-creator.cy.ts @@ -1,25 +1,13 @@ import { NodeCreator } from '../pages/features/node-creator'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { NDV } from '../pages/ndv'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; const nodeCreatorFeature = new NodeCreator(); const WorkflowPage = new WorkflowPageClass(); const NDVModal = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Node Creator', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index f63a7a089e..0b6ded4635 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -1,23 +1,11 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, NDV } from '../pages'; import { v4 as uuid } from 'uuid'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('NDV', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); workflowPage.actions.visit(); workflowPage.actions.renameWorkflow(uuid()); workflowPage.actions.saveWorkflowOnButtonClick(); diff --git a/cypress/e2e/6-code-node.cy.ts b/cypress/e2e/6-code-node.cy.ts index c9c7cdf331..4a987dd6cd 100644 --- a/cypress/e2e/6-code-node.cy.ts +++ b/cypress/e2e/6-code-node.cy.ts @@ -1,23 +1,11 @@ import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; import { NDV } from '../pages/ndv'; -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; const WorkflowPage = new WorkflowPageClass(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Code node', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 007f6fab3a..4e5b2887b5 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -1,8 +1,5 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; import { CODE_NODE_NAME, - DEFAULT_USER_EMAIL, - DEFAULT_USER_PASSWORD, MANUAL_TRIGGER_NODE_NAME, META_KEY, SCHEDULE_TRIGGER_NODE_NAME, @@ -14,20 +11,10 @@ const IMPORT_WORKFLOW_URL = 'https://gist.githubusercontent.com/OlegIvaniv/010bd const DUPLICATE_WORKFLOW_NAME = 'Duplicated workflow'; const DUPLICATE_WORKFLOW_TAG = 'Duplicate'; -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - const WorkflowPage = new WorkflowPageClass(); describe('Workflow Actions', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); }); diff --git a/cypress/e2e/8-http-request-node.cy.ts b/cypress/e2e/8-http-request-node.cy.ts index a905c7f087..c7f44e3494 100644 --- a/cypress/e2e/8-http-request-node.cy.ts +++ b/cypress/e2e/8-http-request-node.cy.ts @@ -1,18 +1,11 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage, NDV } from '../pages'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('HTTP Request node', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); + beforeEach(() => { + workflowPage.actions.visit(); }); it('should make a request with a URL and receive a response', () => { diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index c50309ae12..46affa0d62 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -1,21 +1,9 @@ -import { randFirstName, randLastName } from '@ngneat/falso'; -import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants'; import { WorkflowPage as WorkflowPageClass } from '../pages/workflow'; const WorkflowPage = new WorkflowPageClass(); -const email = DEFAULT_USER_EMAIL; -const password = DEFAULT_USER_PASSWORD; -const firstName = randFirstName(); -const lastName = randLastName(); - describe('Expression editor modal', () => { - before(() => { - cy.setup({ email, firstName, lastName, password }); - }); - beforeEach(() => { - cy.signin({ email, password }); WorkflowPage.actions.visit(); WorkflowPage.actions.addInitialNodeToCanvas('Manual'); WorkflowPage.actions.addNodeToCanvas('Hacker News'); diff --git a/cypress/pages/index.ts b/cypress/pages/index.ts index 35ef30d5ec..33ddcda6e5 100644 --- a/cypress/pages/index.ts +++ b/cypress/pages/index.ts @@ -1,7 +1,5 @@ export * from './base'; export * from './credentials'; -export * from './signin'; -export * from './signup'; export * from './workflows'; export * from './workflow'; export * from './modals'; diff --git a/cypress/pages/sidebar/main-sidebar.ts b/cypress/pages/sidebar/main-sidebar.ts index b86ed326f1..fc9d8557a2 100644 --- a/cypress/pages/sidebar/main-sidebar.ts +++ b/cypress/pages/sidebar/main-sidebar.ts @@ -26,9 +26,5 @@ export class MainSidebar extends BasePage { openUserMenu: () => { this.getters.userMenu().find('[role="button"]').last().click(); }, - signout: () => { - this.actions.openUserMenu(); - cy.getByTestId('workflow-menu-item-logout').click(); - }, }; } diff --git a/cypress/pages/signin.ts b/cypress/pages/signin.ts deleted file mode 100644 index b54a30173f..0000000000 --- a/cypress/pages/signin.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { BasePage } from './base'; - -export class SigninPage extends BasePage { - url = '/signin'; - getters = { - form: () => cy.getByTestId('auth-form'), - email: () => cy.getByTestId('email'), - password: () => cy.getByTestId('password'), - submit: () => cy.get('button'), - }; -} diff --git a/cypress/pages/signup.ts b/cypress/pages/signup.ts deleted file mode 100644 index f647720ce4..0000000000 --- a/cypress/pages/signup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { BasePage } from './base'; - -// todo rename to setup -export class SignupPage extends BasePage { - url = '/setup'; - getters = { - form: () => cy.getByTestId('auth-form'), - email: () => cy.getByTestId('email'), - firstName: () => cy.getByTestId('firstName'), - lastName: () => cy.getByTestId('lastName'), - password: () => cy.getByTestId('password'), - submit: () => cy.get('button'), - skip: () => cy.get('a'), - }; -} diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index f8092fde33..bfd00b0718 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -1,32 +1,6 @@ -// *********************************************** -// This example commands.js shows you how to -// create various custom commands and overwrite -// existing commands. -// -// For more comprehensive examples of custom -// commands please read more here: -// https://on.cypress.io/custom-commands -// *********************************************** -// -// -// -- This is a parent command -- -// Cypress.Commands.add('login', (email, password) => { ... }) -// -// -// -- This is a child command -- -// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) -// -// -// -- This is a dual command -- -// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) -// -// -// -- This will overwrite an existing command -- -// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) import 'cypress-real-events'; -import { WorkflowsPage, SigninPage, SignupPage, SettingsUsersPage, WorkflowPage } from '../pages'; -import { N8N_AUTH_COOKIE } from '../constants'; -import { MessageBox } from '../pages/modals/message-box'; +import { WorkflowPage } from '../pages'; +import { BASE_URL, N8N_AUTH_COOKIE } from '../constants'; Cypress.Commands.add('getByTestId', (selector, ...args) => { return cy.get(`[data-test-id="${selector}"]`, ...args); @@ -59,136 +33,35 @@ Cypress.Commands.add('waitForLoad', (waitForIntercepts = true) => { // we can't set them up here because at this point it would be too late // and the requests would already have been made if (waitForIntercepts) { - cy.wait(['@loadSettings', '@loadLogin']); + cy.wait(['@loadSettings']); } cy.getByTestId('node-view-loader', { timeout: 20000 }).should('not.exist'); cy.get('.el-loading-mask', { timeout: 20000 }).should('not.exist'); }); Cypress.Commands.add('signin', ({ email, password }) => { - const signinPage = new SigninPage(); - const workflowsPage = new WorkflowsPage(); - - cy.session( - [email, password], - () => { - cy.visit(signinPage.url); - - signinPage.getters.form().within(() => { - signinPage.getters.email().type(email); - signinPage.getters.password().type(password); - signinPage.getters.submit().click(); - }); - - // we should be redirected to /workflows - cy.url().should('include', workflowsPage.url); + Cypress.session.clearAllSavedSessions(); + cy.session([email, password], () => cy.request('POST', '/rest/login', { email, password }), { + validate() { + cy.getCookie(N8N_AUTH_COOKIE).should('exist'); }, - { - validate() { - cy.getCookie(N8N_AUTH_COOKIE).should('exist'); - }, - }, - ); + }); }); Cypress.Commands.add('signout', () => { - cy.visit('/signout'); - cy.waitForLoad(); - cy.url().should('include', '/signin'); + cy.request('POST', '/rest/logout'); cy.getCookie(N8N_AUTH_COOKIE).should('not.exist'); }); -Cypress.Commands.add('signup', ({ firstName, lastName, password, url }) => { - const signupPage = new SignupPage(); - - cy.visit(url); - - signupPage.getters.form().within(() => { - cy.url().then((url) => { - cy.intercept('/rest/users/*').as('userSignup') - signupPage.getters.firstName().type(firstName); - signupPage.getters.lastName().type(lastName); - signupPage.getters.password().type(password); - signupPage.getters.submit().click(); - cy.wait('@userSignup'); - }); - }); -}); - -Cypress.Commands.add('setup', ({ email, firstName, lastName, password }, skipIntercept = false) => { - const signupPage = new SignupPage(); - - cy.intercept('GET', signupPage.url).as('setupPage'); - cy.visit(signupPage.url); - cy.wait('@setupPage'); - - signupPage.getters.form().within(() => { - cy.url().then((url) => { - if (url.includes(signupPage.url)) { - signupPage.getters.email().type(email); - signupPage.getters.firstName().type(firstName); - signupPage.getters.lastName().type(lastName); - signupPage.getters.password().type(password); - - cy.intercept('POST', '/rest/owner/setup').as('setupRequest'); - signupPage.getters.submit().click(); - - if(!skipIntercept) { - cy.wait('@setupRequest'); - } - } else { - cy.log('User already signed up'); - } - }); - }); -}); - Cypress.Commands.add('interceptREST', (method, url) => { cy.intercept(method, `http://localhost:5678/rest${url}`); }); -Cypress.Commands.add('inviteUsers', ({ instanceOwner, users }) => { - const settingsUsersPage = new SettingsUsersPage(); +const setFeature = (feature: string, enabled: boolean) => + cy.request('PATCH', `${BASE_URL}/rest/e2e/feature`, { feature: `feat:${feature}`, enabled }); - cy.signin(instanceOwner); - - users.forEach((user) => { - cy.signin(instanceOwner); - cy.visit(settingsUsersPage.url); - - cy.interceptREST('POST', '/users').as('inviteUser'); - - settingsUsersPage.getters.inviteButton().click(); - settingsUsersPage.getters.inviteUsersModal().within((modal) => { - settingsUsersPage.getters.inviteUsersModalEmailsInput().type(user.email).type('{enter}'); - }); - - cy.wait('@inviteUser').then((interception) => { - const inviteLink = interception.response!.body.data[0].user.inviteAcceptUrl; - cy.log(JSON.stringify(interception.response!.body.data[0].user)); - cy.log(inviteLink); - cy.signout(); - cy.signup({ ...user, url: inviteLink }); - }); - }); -}); - -Cypress.Commands.add('resetAll', () => { - cy.task('reset'); - Cypress.session.clearAllSavedSessions(); -}); - -Cypress.Commands.add('setupOwner', (payload) => { - cy.task('setup-owner', payload); -}); - -Cypress.Commands.add('enableFeature', (feature) => { - cy.task('set-feature', { feature, enabled: true }); -}); - -Cypress.Commands.add('disableFeature', (feature) => { - cy.task('set-feature', { feature, enabled: false }); -}); +Cypress.Commands.add('enableFeature', (feature: string) => setFeature(feature, true)); +Cypress.Commands.add('disableFeature', (feature): string => setFeature(feature, false)); Cypress.Commands.add('grantBrowserPermissions', (...permissions: string[]) => { if (Cypress.isBrowser('chrome')) { diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts index 1df0199296..456ba9efd9 100644 --- a/cypress/support/e2e.ts +++ b/cypress/support/e2e.ts @@ -1,28 +1,19 @@ -// *********************************************************** -// This example support/e2e.js is processed and -// loaded automatically before your test files. -// -// This is a great place to put global configuration and -// behavior that modifies Cypress. -// -// You can change the location of this file or turn off -// automatically serving support files with the -// 'supportFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/configuration -// *********************************************************** - +import { BASE_URL, INSTANCE_MEMBERS, INSTANCE_OWNER } from '../constants'; import './commands'; before(() => { - cy.resetAll(); + cy.request('POST', `${BASE_URL}/rest/e2e/reset`, { + owner: INSTANCE_OWNER, + members: INSTANCE_MEMBERS, + }); }); -// Load custom nodes and credentials fixtures beforeEach(() => { + if (!cy.config('disableAutoLogin')) { + cy.signin({ email: INSTANCE_OWNER.email, password: INSTANCE_OWNER.password }); + } + cy.intercept('GET', '/rest/settings').as('loadSettings'); - cy.intercept('GET', '/rest/login').as('loadLogin'); // Always intercept the request to test credentials and return a success cy.intercept('POST', '/rest/credentials/test', { diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 608d4095be..196a14d9ec 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -8,25 +8,14 @@ interface SigninPayload { password: string; } -interface SetupPayload { - email: string; - password: string; - firstName: string; - lastName: string; -} - -interface SignupPayload extends SetupPayload { - url: string; -} - -interface InviteUsersPayload { - instanceOwner: SigninPayload; - users: SetupPayload[]; -} - declare global { namespace Cypress { + interface SuiteConfigOverrides { + disableAutoLogin: boolean; + } + interface Chainable { + config(key: keyof SuiteConfigOverrides): boolean; getByTestId( selector: string, ...args: (Partial | undefined)[] @@ -35,12 +24,7 @@ declare global { createFixtureWorkflow(fixtureKey: string, workflowName: string): void; signin(payload: SigninPayload): void; signout(): void; - signup(payload: SignupPayload): void; - setup(payload: SetupPayload, skipIntercept?: boolean): void; - setupOwner(payload: SetupPayload): void; - inviteUsers(payload: InviteUsersPayload): void; interceptREST(method: string, url: string): Chainable; - resetAll(): void; enableFeature(feature: string): void; disableFeature(feature: string): void; waitForLoad(waitForIntercepts?: boolean): void; diff --git a/package.json b/package.json index 5d6c77cf6c..7b989b9c2a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,6 @@ "cypress:open": "CYPRESS_BASE_URL=http://localhost:8080 cypress open", "test:e2e:ui": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress open'", "test:e2e:dev": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first CYPRESS_BASE_URL=http://localhost:8080 start-server-and-test dev http://localhost:8080/favicon.ico 'cypress open'", - "test:e2e:smoke": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless --spec \"cypress/e2e/0-smoke.cy.ts\"'", "test:e2e:all": "cross-env E2E_TESTS=true NODE_OPTIONS=--dns-result-order=ipv4first start-server-and-test start http://localhost:5678/favicon.ico 'cypress run --headless'" }, "dependencies": { @@ -53,7 +52,6 @@ "jest-mock": "^29.5.0", "jest-mock-extended": "^3.0.4", "nock": "^13.2.9", - "node-fetch": "^2.6.7", "p-limit": "^3.1.0", "prettier": "^2.8.3", "rimraf": "^3.0.2", diff --git a/packages/cli/.npmignore b/packages/cli/.npmignore deleted file mode 100644 index e0c1232042..0000000000 --- a/packages/cli/.npmignore +++ /dev/null @@ -1 +0,0 @@ -dist/ReloadNodesAndCredentials.* diff --git a/packages/cli/package.json b/packages/cli/package.json index 0ef84a15d5..16a0072d2f 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,7 +59,9 @@ "bin", "templates", "dist", - "oclif.manifest.json" + "oclif.manifest.json", + "!dist/**/e2e.*", + "!dist/ReloadNodesAndCredentials.*" ], "devDependencies": { "@apidevtools/swagger-cli": "4.0.0", diff --git a/packages/cli/src/License.ts b/packages/cli/src/License.ts index 3452bc75ca..016e616d4a 100644 --- a/packages/cli/src/License.ts +++ b/packages/cli/src/License.ts @@ -96,9 +96,8 @@ export class License { await this.manager.renew(); } - isFeatureEnabled(feature: string): boolean { + isFeatureEnabled(feature: LICENSE_FEATURES): boolean { if (!this.manager) { - getLogger().warn('License manager not initialized'); return false; } diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 333f4c1e95..b981b6d1c7 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -68,6 +68,7 @@ import { EDITOR_UI_DIST_DIR, GENERATED_STATIC_DIR, inDevelopment, + inE2ETests, N8N_VERSION, RESPONSE_ERROR_MESSAGES, TEMPLATES_DIR, @@ -338,10 +339,6 @@ export class Server extends AbstractServer { this.push = Container.get(Push); - if (process.env.E2E_TESTS === 'true') { - this.app.use('/e2e', require('./api/e2e.api').e2eController); - } - await super.start(); const cpus = os.cpus(); @@ -461,7 +458,7 @@ export class Server extends AbstractServer { return this.frontendSettings; } - private registerControllers(ignoredEndpoints: Readonly) { + private async registerControllers(ignoredEndpoints: Readonly) { const { app, externalHooks, activeWorkflowRunner, nodeTypes } = this; const repositories = Db.collections; setupAuthMiddlewares(app, ignoredEndpoints, this.restEndpoint); @@ -515,6 +512,12 @@ export class Server extends AbstractServer { ); } + if (inE2ETests) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { E2EController } = await import('./controllers/e2e.controller'); + controllers.push(Container.get(E2EController)); + } + controllers.forEach((controller) => registerController(app, config, controller)); } @@ -590,7 +593,7 @@ export class Server extends AbstractServer { await handleLdapInit(); - this.registerControllers(ignoredEndpoints); + await this.registerControllers(ignoredEndpoints); this.app.use(`/${this.restEndpoint}/credentials`, credentialsController); diff --git a/packages/cli/src/api/e2e.api.ts b/packages/cli/src/api/e2e.api.ts deleted file mode 100644 index b0e11e23ed..0000000000 --- a/packages/cli/src/api/e2e.api.ts +++ /dev/null @@ -1,158 +0,0 @@ -/* eslint-disable @typescript-eslint/no-unsafe-return */ -/* eslint-disable @typescript-eslint/no-unsafe-call */ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/no-unsafe-argument */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -/* eslint-disable @typescript-eslint/naming-convention */ -import { Router } from 'express'; -import type { Request } from 'express'; -import bodyParser from 'body-parser'; -import { v4 as uuid } from 'uuid'; -import { Container } from 'typedi'; -import config from '@/config'; -import * as Db from '@/Db'; -import type { Role } from '@db/entities/Role'; -import { RoleRepository } from '@db/repositories'; -import { hashPassword } from '@/UserManagement/UserManagementHelper'; -import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; -import { License } from '../License'; -import { LICENSE_FEATURES } from '@/constants'; - -if (process.env.E2E_TESTS !== 'true') { - console.error('E2E endpoints only allowed during E2E tests'); - process.exit(1); -} - -const enabledFeatures = { - [LICENSE_FEATURES.SHARING]: true, //default to true here instead of setting it in config/index.ts for e2e - [LICENSE_FEATURES.LDAP]: false, - [LICENSE_FEATURES.SAML]: false, - [LICENSE_FEATURES.LOG_STREAMING]: false, - [LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS]: false, - [LICENSE_FEATURES.SOURCE_CONTROL]: false, -}; - -type Feature = keyof typeof enabledFeatures; - -Container.get(License).isFeatureEnabled = (feature: Feature) => enabledFeatures[feature] ?? false; - -const tablesToTruncate = [ - 'auth_identity', - 'auth_provider_sync_history', - 'event_destinations', - 'shared_workflow', - 'shared_credentials', - 'webhook_entity', - 'workflows_tags', - 'credentials_entity', - 'tag_entity', - 'workflow_statistics', - 'workflow_entity', - 'execution_entity', - 'settings', - 'installed_packages', - 'installed_nodes', - 'user', - 'role', - 'variables', -]; - -const truncateAll = async () => { - const connection = Db.getConnection(); - - for (const table of tablesToTruncate) { - try { - await connection.query( - `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, - ); - } catch (error) { - console.warn('Dropping Table for E2E Reset error: ', error); - } - } -}; - -const setupUserManagement = async () => { - const connection = Db.getConnection(); - await connection.query('INSERT INTO role (name, scope) VALUES ("owner", "global");'); - const instanceOwnerRole = (await connection.query( - 'SELECT last_insert_rowid() as insertId', - )) as Array<{ insertId: number }>; - - const roles: Array<[Role['name'], Role['scope']]> = [ - ['member', 'global'], - ['owner', 'workflow'], - ['owner', 'credential'], - ['user', 'credential'], - ['editor', 'workflow'], - ]; - - await Promise.all( - roles.map(async ([name, scope]) => - connection.query(`INSERT INTO role (name, scope) VALUES ("${name}", "${scope}");`), - ), - ); - await connection.query( - `INSERT INTO user (id, globalRoleId) values ("${uuid()}", ${instanceOwnerRole[0].insertId})`, - ); - await connection.query( - "INSERT INTO \"settings\" (key, value, loadOnStartup) values ('userManagement.isInstanceOwnerSetUp', 'false', true)", - ); - - config.set('userManagement.isInstanceOwnerSetUp', false); -}; - -const resetLogStreaming = async () => { - enabledFeatures[LICENSE_FEATURES.LOG_STREAMING] = false; - for (const id in eventBus.destinations) { - await eventBus.removeDestination(id); - } -}; - -export const e2eController = Router(); - -e2eController.post('/db/reset', async (req, res) => { - await resetLogStreaming(); - await truncateAll(); - await setupUserManagement(); - - res.writeHead(204).end(); -}); - -e2eController.post('/db/setup-owner', bodyParser.json(), async (req, res) => { - if (config.get('userManagement.isInstanceOwnerSetUp')) { - res.writeHead(500).send({ error: 'Owner already setup' }); - return; - } - - const globalRole = await Container.get(RoleRepository).findGlobalOwnerRoleOrFail(); - - const owner = await Db.collections.User.findOneByOrFail({ globalRoleId: globalRole.id }); - - await Db.collections.User.update(owner.id, { - email: req.body.email, - password: await hashPassword(req.body.password), - firstName: req.body.firstName, - lastName: req.body.lastName, - }); - - await Db.collections.Settings.update( - { key: 'userManagement.isInstanceOwnerSetUp' }, - { value: 'true' }, - ); - - config.set('userManagement.isInstanceOwnerSetUp', true); - - res.writeHead(204).end(); -}); - -e2eController.patch( - '/feature/:feature', - bodyParser.json(), - async (req: Request<{ feature: Feature }>, res) => { - const { feature } = req.params; - const { enabled } = req.body; - - enabledFeatures[feature] = enabled === undefined || enabled === true; - res.writeHead(204).end(); - }, -); diff --git a/packages/cli/src/config/index.ts b/packages/cli/src/config/index.ts index e75db78b7a..5b250b572e 100644 --- a/packages/cli/src/config/index.ts +++ b/packages/cli/src/config/index.ts @@ -18,7 +18,6 @@ if (inE2ETests) { N8N_PUBLIC_API_DISABLED: 'true', EXTERNAL_FRONTEND_HOOKS_URLS: '', N8N_PERSONALIZATION_ENABLED: 'false', - NODE_FUNCTION_ALLOW_EXTERNAL: 'node-fetch', }; } else if (inTest) { const testsDir = join(tmpdir(), 'n8n-tests/'); diff --git a/packages/cli/src/controllers/e2e.controller.ts b/packages/cli/src/controllers/e2e.controller.ts new file mode 100644 index 0000000000..58156df4a8 --- /dev/null +++ b/packages/cli/src/controllers/e2e.controller.ts @@ -0,0 +1,154 @@ +import { Request } from 'express'; +import { Service } from 'typedi'; +import { v4 as uuid } from 'uuid'; +import config from '@/config'; +import type { Role } from '@db/entities/Role'; +import { RoleRepository, SettingsRepository, UserRepository } from '@db/repositories'; +import { hashPassword } from '@/UserManagement/UserManagementHelper'; +import { eventBus } from '@/eventbus/MessageEventBus/MessageEventBus'; +import { License } from '@/License'; +import { LICENSE_FEATURES, inE2ETests } from '@/constants'; +import { NoAuthRequired, Patch, Post, RestController } from '@/decorators'; +import type { UserSetupPayload } from '@/requests'; + +if (!inE2ETests) { + console.error('E2E endpoints only allowed during E2E tests'); + process.exit(1); +} + +const tablesToTruncate = [ + 'auth_identity', + 'auth_provider_sync_history', + 'event_destinations', + 'shared_workflow', + 'shared_credentials', + 'webhook_entity', + 'workflows_tags', + 'credentials_entity', + 'tag_entity', + 'workflow_statistics', + 'workflow_entity', + 'execution_entity', + 'settings', + 'installed_packages', + 'installed_nodes', + 'user', + 'role', + 'variables', +]; + +type ResetRequest = Request< + {}, + {}, + { + owner: UserSetupPayload; + members: UserSetupPayload[]; + } +>; + +@Service() +@NoAuthRequired() +@RestController('/e2e') +export class E2EController { + private enabledFeatures: Record = { + [LICENSE_FEATURES.SHARING]: false, + [LICENSE_FEATURES.LDAP]: false, + [LICENSE_FEATURES.SAML]: false, + [LICENSE_FEATURES.LOG_STREAMING]: false, + [LICENSE_FEATURES.ADVANCED_EXECUTION_FILTERS]: false, + [LICENSE_FEATURES.SOURCE_CONTROL]: false, + [LICENSE_FEATURES.VARIABLES]: false, + [LICENSE_FEATURES.API_DISABLED]: false, + }; + + constructor( + license: License, + private roleRepo: RoleRepository, + private settingsRepo: SettingsRepository, + private userRepo: UserRepository, + ) { + license.isFeatureEnabled = (feature: LICENSE_FEATURES) => + this.enabledFeatures[feature] ?? false; + } + + @Post('/reset') + async reset(req: ResetRequest) { + this.resetFeatures(); + await this.resetLogStreaming(); + await this.truncateAll(); + await this.setupUserManagement(req.body.owner, req.body.members); + } + + @Patch('/feature') + setFeature(req: Request<{}, {}, { feature: LICENSE_FEATURES; enabled: boolean }>) { + const { enabled, feature } = req.body; + this.enabledFeatures[feature] = enabled; + } + + private resetFeatures() { + for (const feature of Object.keys(this.enabledFeatures)) { + this.enabledFeatures[feature as LICENSE_FEATURES] = false; + } + } + + private async resetLogStreaming() { + for (const id in eventBus.destinations) { + await eventBus.removeDestination(id); + } + } + + private async truncateAll() { + for (const table of tablesToTruncate) { + try { + const { connection } = this.roleRepo.manager; + await connection.query( + `DELETE FROM ${table}; DELETE FROM sqlite_sequence WHERE name=${table};`, + ); + } catch (error) { + console.warn('Dropping Table for E2E Reset error: ', error); + } + } + } + + private async setupUserManagement(owner: UserSetupPayload, members: UserSetupPayload[]) { + const roles: Array<[Role['name'], Role['scope']]> = [ + ['owner', 'global'], + ['member', 'global'], + ['owner', 'workflow'], + ['owner', 'credential'], + ['user', 'credential'], + ['editor', 'workflow'], + ]; + + const [{ id: globalOwnerRoleId }, { id: globalMemberRoleId }] = await this.roleRepo.save( + roles.map(([name, scope], index) => ({ name, scope, id: index.toString() })), + ); + + const users = []; + users.push({ + id: uuid(), + ...owner, + password: await hashPassword(owner.password), + globalRoleId: globalOwnerRoleId, + }); + for (const { password, ...payload } of members) { + users.push( + this.userRepo.create({ + id: uuid(), + ...payload, + password: await hashPassword(password), + globalRoleId: globalMemberRoleId, + }), + ); + } + + await this.userRepo.insert(users); + + await this.settingsRepo.update( + { key: 'userManagement.isInstanceOwnerSetUp' }, + { value: 'true' }, + ); + + config.set('userManagement.isInstanceOwnerSetUp', true); + } +} diff --git a/packages/cli/src/requests.ts b/packages/cli/src/requests.ts index 9b4adf94ad..5b4bf5d51b 100644 --- a/packages/cli/src/requests.ts +++ b/packages/cli/src/requests.ts @@ -181,22 +181,19 @@ export declare namespace MeRequest { export type SurveyAnswers = AuthenticatedRequest<{}, {}, Record | {}>; } +export interface UserSetupPayload { + email: string; + password: string; + firstName: string; + lastName: string; +} + // ---------------------------------- // /owner // ---------------------------------- export declare namespace OwnerRequest { - type Post = AuthenticatedRequest< - {}, - {}, - Partial<{ - email: string; - password: string; - firstName: string; - lastName: string; - }>, - {} - >; + type Post = AuthenticatedRequest<{}, {}, UserSetupPayload, {}>; } // ---------------------------------- diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a34918c618..fad3582a7f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -98,9 +98,6 @@ importers: nock: specifier: ^13.2.9 version: 13.2.9 - node-fetch: - specifier: ^2.6.7 - version: 2.6.7 p-limit: specifier: ^3.1.0 version: 3.1.0 @@ -17055,18 +17052,6 @@ packages: resolution: {integrity: sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg==} dev: true - /node-fetch@2.6.7: - resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==} - engines: {node: 4.x || >=6.0.0} - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - dependencies: - whatwg-url: 5.0.0 - dev: true - /node-fetch@2.6.8: resolution: {integrity: sha512-RZ6dBYuj8dRSfxpUSu+NsdF1dpPpluJxwOp+6IoDp/sH2QNDSvurYsAa+F1WxY2RjA1iP93xhcsUoYbF2XBqVg==} engines: {node: 4.x || >=6.0.0}