mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
ci: Refactor e2e tests to delete boilerplate code (no-changelog) (#6524)
This commit is contained in:
parent
abe7f71627
commit
0e071724ee
|
@ -10,3 +10,5 @@ packages/**/.turbo
|
|||
.git
|
||||
.github
|
||||
*.tsbuildinfo
|
||||
packages/cli/dist/**/e2e.*
|
||||
packages/cli/dist/ReloadNodesAndCredentials.*
|
||||
|
|
16
.github/workflows/ci-pull-requests.yml
vendored
16
.github/workflows/ci-pull-requests.yml
vendored
|
@ -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
|
||||
|
|
|
@ -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' }
|
||||
})
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -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"';
|
||||
|
|
|
@ -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 });
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
};
|
||||
}
|
|
@ -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'),
|
||||
};
|
||||
}
|
|
@ -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');
|
||||
},
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
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')) {
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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<Loggable & Timeoutable & Withinable & Shadow> | 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<Interception>;
|
||||
resetAll(): void;
|
||||
enableFeature(feature: string): void;
|
||||
disableFeature(feature: string): void;
|
||||
waitForLoad(waitForIntercepts?: boolean): void;
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
dist/ReloadNodesAndCredentials.*
|
|
@ -59,7 +59,9 @@
|
|||
"bin",
|
||||
"templates",
|
||||
"dist",
|
||||
"oclif.manifest.json"
|
||||
"oclif.manifest.json",
|
||||
"!dist/**/e2e.*",
|
||||
"!dist/ReloadNodesAndCredentials.*"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@apidevtools/swagger-cli": "4.0.0",
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<string[]>) {
|
||||
private async registerControllers(ignoredEndpoints: Readonly<string[]>) {
|
||||
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);
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
);
|
|
@ -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/');
|
||||
|
|
154
packages/cli/src/controllers/e2e.controller.ts
Normal file
154
packages/cli/src/controllers/e2e.controller.ts
Normal file
|
@ -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, boolean> = {
|
||||
[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);
|
||||
}
|
||||
}
|
|
@ -181,22 +181,19 @@ export declare namespace MeRequest {
|
|||
export type SurveyAnswers = AuthenticatedRequest<{}, {}, Record<string, string> | {}>;
|
||||
}
|
||||
|
||||
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, {}>;
|
||||
}
|
||||
|
||||
// ----------------------------------
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in a new issue