1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-09-20 09:27:44 +03:00

feat: Add workflow and credential sharing access e2e tests (#5463)

feat: add workflow and credential sharing access e2e tests
This commit is contained in:
Alex Grozav 2023-02-14 16:13:00 +02:00 committed by GitHub
parent b25c10a0e1
commit 246189f6da
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 239 additions and 22 deletions

View File

@ -0,0 +1,168 @@
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
import {
CredentialsModal,
CredentialsPage,
NDV,
WorkflowPage,
WorkflowSharingModal,
WorkflowsPage,
} from '../pages';
/**
* User U1 - Instance owner
* User U2 - User, owns C1, W1, W2
* User U3 - User, owns C2
*
* W1 - Workflow owned by User U2, shared with User U3
* W2 - Workflow owned by User U2
*
* C1 - Credential owned by User U2
* C2 - Credential owned by User U3, shared with User U1 and User U2
*/
const credentialsPage = new CredentialsPage();
const credentialsModal = new CredentialsModal();
const workflowsPage = new WorkflowsPage();
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.resetAll();
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 });
});
let workflowW2Url = '';
it('should create C1, W1, W2, share W1 with U3, as U2', () => {
cy.signin(users[0]);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Notion API').click();
credentialsModal.getters.newCredentialTypeButton().click();
credentialsModal.getters.connectionParameter('API Key').type('1234567890');
credentialsModal.actions.setName('Credential C1');
credentialsModal.actions.save();
credentialsModal.actions.close();
cy.visit(workflowsPage.url);
workflowsPage.getters.newWorkflowButtonCard().click();
workflowPage.actions.setWorkflowName('Workflow W1');
workflowPage.actions.addInitialNodeToCanvas('Manual Trigger');
workflowPage.actions.addNodeToCanvas('Notion', true, true);
ndv.getters.credentialInput().should('contain', 'Credential C1');
ndv.actions.close();
workflowPage.actions.openShareModal();
workflowSharingModal.actions.addUser(users[1].email);
workflowSharingModal.actions.save();
workflowPage.actions.saveWorkflowOnButtonClick();
cy.visit(workflowsPage.url);
workflowsPage.getters.createWorkflowButton().click();
cy.createFixtureWorkflow('Test_workflow_1.json', 'Workflow W2');
cy.url().then((url) => {
workflowW2Url = url;
});
});
it('should create C2, share C2 with U1 and U2, as U3', () => {
cy.signin(users[1]);
cy.visit(credentialsPage.url);
credentialsPage.getters.emptyListCreateCredentialButton().click();
credentialsModal.getters.newCredentialTypeOption('Airtable API').click();
credentialsModal.getters.newCredentialTypeButton().click();
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.save();
credentialsModal.actions.close();
});
it('should open W1, add node using C2 as U3', () => {
cy.signin(users[1]);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 1);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.addNodeToCanvas('Airtable', true, true);
ndv.getters.credentialInput().should('contain', 'Credential C2');
ndv.actions.close();
workflowPage.actions.saveWorkflowOnButtonClick();
workflowPage.actions.openNode('Notion');
ndv.getters
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.disabled');
ndv.actions.close();
});
it('should not have access to W2, as U3', () => {
cy.signin(users[1]);
cy.visit(workflowW2Url);
cy.waitForLoad();
cy.wait(1000);
cy.get('.el-notification').contains('Could not find workflow').should('be.visible');
});
it('should have access to W1, W2, as U1', () => {
cy.signin(instanceOwner);
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCards().should('have.length', 2);
workflowsPage.getters.workflowCard('Workflow W1').click();
workflowPage.actions.openNode('Notion');
ndv.getters
.credentialInput()
.find('input')
.should('have.value', 'Credential C1')
.should('be.disabled');
ndv.actions.close();
cy.waitForLoad();
cy.visit(workflowsPage.url);
workflowsPage.getters.workflowCard('Workflow W2').click();
workflowPage.actions.executeWorkflow();
});
});

View File

@ -26,8 +26,19 @@ export class CredentialsModal extends BasePage {
credentialAuthTypeRadioButtons: () => credentialAuthTypeRadioButtons: () =>
this.getters.credentialsAuthTypeSelector().find('label[role=radio]'), this.getters.credentialsAuthTypeSelector().find('label[role=radio]'),
credentialInputs: () => cy.getByTestId('credential-connection-parameter'), credentialInputs: () => cy.getByTestId('credential-connection-parameter'),
menu: () => this.getters.editCredentialModal().get('.menu-container'),
menuItem: (name: string) => this.getters.menu().get('.n8n-menu-item').contains(name),
usersSelect: () => cy.getByTestId('credential-sharing-modal-users-select'),
}; };
actions = { actions = {
addUser: (email: string) => {
this.getters.usersSelect().click();
this.getters
.usersSelect()
.get('.el-select-dropdown__item')
.contains(email.toLowerCase())
.click();
},
setName: (name: string) => { setName: (name: string) => {
this.getters.name().click(); this.getters.name().click();
this.getters.nameInput().clear().type(name); this.getters.nameInput().clear().type(name);
@ -64,5 +75,8 @@ export class CredentialsModal extends BasePage {
this.getters.nameInput().type(newName); this.getters.nameInput().type(newName);
this.getters.nameInput().type('{enter}'); this.getters.nameInput().type('{enter}');
}, },
changeTab: (tabName: string) => {
this.getters.menuItem(tabName).click();
},
}; };
} }

View File

@ -1,2 +1,3 @@
export * from './credentials-modal'; export * from './credentials-modal';
export * from './message-box'; export * from './message-box';
export * from './workflow-sharing-modal';

View File

@ -0,0 +1,26 @@
import { BasePage } from '../base';
export class WorkflowSharingModal extends BasePage {
getters = {
modal: () => cy.getByTestId('workflowShare-modal', { timeout: 5000 }),
usersSelect: () => cy.getByTestId('workflow-sharing-modal-users-select'),
saveButton: () => cy.getByTestId('workflow-sharing-modal-save-button'),
closeButton: () => this.getters.modal().find('.el-dialog__close').first(),
};
actions = {
addUser: (email: string) => {
this.getters.usersSelect().click();
this.getters
.usersSelect()
.get('.el-select-dropdown__item')
.contains(email.toLowerCase())
.click();
},
save: () => {
this.getters.saveButton().click();
},
closeModal: () => {
this.getters.closeButton().click();
},
};
}

View File

@ -5,6 +5,7 @@ export class NDV extends BasePage {
container: () => cy.getByTestId('ndv'), container: () => cy.getByTestId('ndv'),
backToCanvas: () => cy.getByTestId('back-to-canvas'), backToCanvas: () => cy.getByTestId('back-to-canvas'),
copyInput: () => cy.getByTestId('copy-input'), copyInput: () => cy.getByTestId('copy-input'),
credentialInput: (eq = 0) => cy.getByTestId('node-credentials-select').eq(eq),
nodeExecuteButton: () => cy.getByTestId('node-execute-button'), nodeExecuteButton: () => cy.getByTestId('node-execute-button'),
inputSelect: () => cy.getByTestId('ndv-input-select'), inputSelect: () => cy.getByTestId('ndv-input-select'),
inputOption: () => cy.getByTestId('ndv-input-option'), inputOption: () => cy.getByTestId('ndv-input-option'),
@ -24,15 +25,22 @@ export class NDV extends BasePage {
outputTableRows: () => this.getters.outputDataContainer().find('table tr'), outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'), outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'),
outputTableRow: (row: number) => this.getters.outputTableRows().eq(row), outputTableRow: (row: number) => this.getters.outputTableRows().eq(row),
outputTbodyCell: (row: number, col: number) => this.getters.outputTableRow(row).find('td').eq(col), outputTbodyCell: (row: number, col: number) =>
this.getters.outputTableRow(row).find('td').eq(col),
inputTableRows: () => this.getters.inputDataContainer().find('table tr'), inputTableRows: () => this.getters.inputDataContainer().find('table tr'),
inputTableHeaders: () => this.getters.inputDataContainer().find('table thead th'), inputTableHeaders: () => this.getters.inputDataContainer().find('table thead th'),
inputTableRow: (row: number) => this.getters.inputTableRows().eq(row), inputTableRow: (row: number) => this.getters.inputTableRows().eq(row),
inputTbodyCell: (row: number, col: number) => this.getters.inputTableRow(row).find('td').eq(col), inputTbodyCell: (row: number, col: number) =>
this.getters.inputTableRow(row).find('td').eq(col),
inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'), inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'),
nodeParameters: () => cy.getByTestId('node-parameters'), nodeParameters: () => cy.getByTestId('node-parameters'),
parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`), parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`),
parameterExpressionPreview: (parameterName: string) => this.getters.nodeParameters().find(`[data-test-id="parameter-input-${parameterName}"] + [data-test-id="parameter-expression-preview"]`), parameterExpressionPreview: (parameterName: string) =>
this.getters
.nodeParameters()
.find(
`[data-test-id="parameter-input-${parameterName}"] + [data-test-id="parameter-expression-preview"]`,
),
nodeNameContainer: () => cy.getByTestId('node-title-container'), nodeNameContainer: () => cy.getByTestId('node-title-container'),
nodeRenameInput: () => cy.getByTestId('node-rename-input'), nodeRenameInput: () => cy.getByTestId('node-rename-input'),
executePrevious: () => cy.getByTestId('execute-previous-node'), executePrevious: () => cy.getByTestId('execute-previous-node'),
@ -77,18 +85,11 @@ export class NDV extends BasePage {
this.getters.parameterInput(parameterName).type(content); this.getters.parameterInput(parameterName).type(content);
}, },
selectOptionInParameterDropdown: (parameterName: string, content: string) => { selectOptionInParameterDropdown: (parameterName: string, content: string) => {
this.getters this.getters.parameterInput(parameterName).find('.option-headline').contains(content).click();
.parameterInput(parameterName)
.find('.option-headline')
.contains(content)
.click();
}, },
rename: (newName: string) => { rename: (newName: string) => {
this.getters.nodeNameContainer().click(); this.getters.nodeNameContainer().click();
this.getters.nodeRenameInput() this.getters.nodeRenameInput().should('be.visible').type('{selectall}').type(newName);
.should('be.visible')
.type('{selectall}')
.type(newName);
cy.get('body').type('{enter}'); cy.get('body').type('{enter}');
}, },
executePrevious: () => { executePrevious: () => {
@ -104,10 +105,10 @@ export class NDV extends BasePage {
cy.draganddrop('', droppable); cy.draganddrop('', droppable);
}, },
switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => { switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => {
this.getters.inputDisplayMode().find('label').contains(type).click({force: true}); this.getters.inputDisplayMode().find('label').contains(type).click({ force: true });
}, },
switchOutputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => { switchOutputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => {
this.getters.outputDisplayMode().find('label').contains(type).click({force: true}); this.getters.outputDisplayMode().find('label').contains(type).click({ force: true });
}, },
selectInputNode: (nodeName: string) => { selectInputNode: (nodeName: string) => {
this.getters.inputSelect().find('.el-select').click(); this.getters.inputSelect().find('.el-select').click();

View File

@ -26,7 +26,7 @@ export class WorkflowPage extends BasePage {
canvasNodeByName: (nodeName: string) => canvasNodeByName: (nodeName: string) =>
this.getters.canvasNodes().filter(`:contains("${nodeName}")`), this.getters.canvasNodes().filter(`:contains("${nodeName}")`),
getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => { getEndpointSelector: (type: 'input' | 'output' | 'plus', nodeName: string, index = 0) => {
return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']` return `[data-endpoint-name='${nodeName}'][data-endpoint-type='${type}'][data-input-index='${index}']`;
}, },
canvasNodeInputEndpointByName: (nodeName: string, index = 0) => { canvasNodeInputEndpointByName: (nodeName: string, index = 0) => {
return cy.get(this.getters.getEndpointSelector('input', nodeName, index)); return cy.get(this.getters.getEndpointSelector('input', nodeName, index));
@ -79,7 +79,7 @@ export class WorkflowPage extends BasePage {
workflowSettingsSaveButton: () => workflowSettingsSaveButton: () =>
cy.getByTestId('workflow-settings-save-button').find('button'), cy.getByTestId('workflow-settings-save-button').find('button'),
shareButton: () => cy.getByTestId('workflow-share-button').find('button'), shareButton: () => cy.getByTestId('workflow-share-button'),
duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'), duplicateWorkflowModal: () => cy.getByTestId('duplicate-modal'),
nodeViewBackground: () => cy.getByTestId('node-view-background'), nodeViewBackground: () => cy.getByTestId('node-view-background'),
@ -155,11 +155,17 @@ export class WorkflowPage extends BasePage {
saveWorkflowOnButtonClick: () => { saveWorkflowOnButtonClick: () => {
this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().should('contain', 'Save');
this.getters.saveButton().click(); this.getters.saveButton().click();
this.getters.saveButton().should('contain', 'Saved') this.getters.saveButton().should('contain', 'Saved');
}, },
saveWorkflowUsingKeyboardShortcut: () => { saveWorkflowUsingKeyboardShortcut: () => {
cy.get('body').type('{meta}', { release: false }).type('s'); cy.get('body').type('{meta}', { release: false }).type('s');
}, },
setWorkflowName: (name: string) => {
this.getters.workflowNameInput().should('be.disabled');
this.getters.workflowNameInput().parent().click();
this.getters.workflowNameInput().should('be.enabled');
this.getters.workflowNameInput().clear().type(name).type('{enter}');
},
activateWorkflow: () => { activateWorkflow: () => {
this.getters.activatorSwitch().find('input').first().should('be.enabled'); this.getters.activatorSwitch().find('input').first().should('be.enabled');
this.getters.activatorSwitch().click(); this.getters.activatorSwitch().click();

View File

@ -40,10 +40,7 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => {
WorkflowPage.getters WorkflowPage.getters
.workflowImportInput() .workflowImportInput()
.selectFile(`cypress/fixtures/${fixtureKey}`, { force: true }); .selectFile(`cypress/fixtures/${fixtureKey}`, { force: true });
WorkflowPage.getters.workflowNameInput().should('be.disabled'); WorkflowPage.actions.setWorkflowName(workflowName);
WorkflowPage.getters.workflowNameInput().parent().click();
WorkflowPage.getters.workflowNameInput().should('be.enabled');
WorkflowPage.getters.workflowNameInput().clear().type(workflowName).type('{enter}');
WorkflowPage.getters.saveButton().should('contain', 'Saved'); WorkflowPage.getters.saveButton().should('contain', 'Saved');
}); });

View File

@ -61,6 +61,7 @@
:users="usersList" :users="usersList"
:currentUserId="usersStore.currentUser.id" :currentUserId="usersStore.currentUser.id"
:placeholder="$locale.baseText('credentialEdit.credentialSharing.select.placeholder')" :placeholder="$locale.baseText('credentialEdit.credentialSharing.select.placeholder')"
data-test-id="credential-sharing-modal-users-select"
@input="onAddSharee" @input="onAddSharee"
> >
<template #prefix> <template #prefix>

View File

@ -20,6 +20,7 @@
:value="getSelectedName(credentialTypeDescription.name)" :value="getSelectedName(credentialTypeDescription.name)"
disabled disabled
size="small" size="small"
data-test-id="node-credentials-select"
/> />
</div> </div>
<div <div

View File

@ -38,6 +38,7 @@
:users="usersList" :users="usersList"
:currentUserId="currentUser.id" :currentUserId="currentUser.id"
:placeholder="$locale.baseText('workflows.shareModal.select.placeholder')" :placeholder="$locale.baseText('workflows.shareModal.select.placeholder')"
data-test-id="workflow-sharing-modal-users-select"
@input="onAddSharee" @input="onAddSharee"
> >
<template #prefix> <template #prefix>
@ -108,10 +109,11 @@
</n8n-text> </n8n-text>
<n8n-button <n8n-button
v-show="workflowPermissions.updateSharing" v-show="workflowPermissions.updateSharing"
@click="onSave"
:loading="loading" :loading="loading"
:disabled="!isDirty" :disabled="!isDirty"
size="medium" size="medium"
data-test-id="workflow-sharing-modal-save-button"
@click="onSave"
> >
{{ $locale.baseText('workflows.shareModal.save') }} {{ $locale.baseText('workflows.shareModal.save') }}
</n8n-button> </n8n-button>