ci: Refactor e2e tests to delete boilerplate code (no-changelog) (#6524)

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-06-23 00:38:12 +02:00 committed by GitHub
parent abe7f71627
commit 0e071724ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
50 changed files with 281 additions and 913 deletions

View file

@ -10,3 +10,5 @@ packages/**/.turbo
.git
.github
*.tsbuildinfo
packages/cli/dist/**/e2e.*
packages/cli/dist/ReloadNodesAndCredentials.*

View file

@ -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

View file

@ -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' }
})
},
});
},
},
});

View file

@ -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"';

View file

@ -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 });
});
});

View file

@ -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);
});

View file

@ -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');

View file

@ -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();
});

View file

@ -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');

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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(

View file

@ -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) => {

View file

@ -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();
});

View file

@ -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) => {

View file

@ -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();

View file

@ -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();
});

View file

@ -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);

View file

@ -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();
});

View file

@ -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);
});

View file

@ -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();

View file

@ -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();
});

View file

@ -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);

View file

@ -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');

View file

@ -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(

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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();

View file

@ -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();
});

View file

@ -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();
});

View file

@ -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', () => {

View file

@ -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');

View file

@ -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';

View file

@ -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();
},
};
}

View file

@ -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'),
};
}

View file

@ -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'),
};
}

View file

@ -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')) {

View file

@ -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', {

View file

@ -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;

View file

@ -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",

View file

@ -1 +0,0 @@
dist/ReloadNodesAndCredentials.*

View file

@ -59,7 +59,9 @@
"bin",
"templates",
"dist",
"oclif.manifest.json"
"oclif.manifest.json",
"!dist/**/e2e.*",
"!dist/ReloadNodesAndCredentials.*"
],
"devDependencies": {
"@apidevtools/swagger-cli": "4.0.0",

View file

@ -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;
}

View file

@ -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);

View file

@ -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();
},
);

View file

@ -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/');

View 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);
}
}

View file

@ -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, {}>;
}
// ----------------------------------

View file

@ -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}