diff --git a/cypress/e2e/17-sharing.cy.ts b/cypress/e2e/17-sharing.cy.ts new file mode 100644 index 0000000000..9bb11293f4 --- /dev/null +++ b/cypress/e2e/17-sharing.cy.ts @@ -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(); + }); +}); diff --git a/cypress/pages/modals/credentials-modal.ts b/cypress/pages/modals/credentials-modal.ts index e6ae20079f..02b267bf9f 100644 --- a/cypress/pages/modals/credentials-modal.ts +++ b/cypress/pages/modals/credentials-modal.ts @@ -26,8 +26,19 @@ export class CredentialsModal extends BasePage { credentialAuthTypeRadioButtons: () => this.getters.credentialsAuthTypeSelector().find('label[role=radio]'), 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 = { + addUser: (email: string) => { + this.getters.usersSelect().click(); + this.getters + .usersSelect() + .get('.el-select-dropdown__item') + .contains(email.toLowerCase()) + .click(); + }, setName: (name: string) => { this.getters.name().click(); this.getters.nameInput().clear().type(name); @@ -64,5 +75,8 @@ export class CredentialsModal extends BasePage { this.getters.nameInput().type(newName); this.getters.nameInput().type('{enter}'); }, + changeTab: (tabName: string) => { + this.getters.menuItem(tabName).click(); + }, }; } diff --git a/cypress/pages/modals/index.ts b/cypress/pages/modals/index.ts index 24f5101aed..3d1981d027 100644 --- a/cypress/pages/modals/index.ts +++ b/cypress/pages/modals/index.ts @@ -1,2 +1,3 @@ export * from './credentials-modal'; export * from './message-box'; +export * from './workflow-sharing-modal'; diff --git a/cypress/pages/modals/workflow-sharing-modal.ts b/cypress/pages/modals/workflow-sharing-modal.ts new file mode 100644 index 0000000000..c013093286 --- /dev/null +++ b/cypress/pages/modals/workflow-sharing-modal.ts @@ -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(); + }, + }; +} diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 9d15a836c3..9cc0285a4f 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -5,6 +5,7 @@ export class NDV extends BasePage { container: () => cy.getByTestId('ndv'), backToCanvas: () => cy.getByTestId('back-to-canvas'), copyInput: () => cy.getByTestId('copy-input'), + credentialInput: (eq = 0) => cy.getByTestId('node-credentials-select').eq(eq), nodeExecuteButton: () => cy.getByTestId('node-execute-button'), inputSelect: () => cy.getByTestId('ndv-input-select'), inputOption: () => cy.getByTestId('ndv-input-option'), @@ -24,15 +25,22 @@ export class NDV extends BasePage { outputTableRows: () => this.getters.outputDataContainer().find('table tr'), outputTableHeaders: () => this.getters.outputDataContainer().find('table thead th'), 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'), inputTableHeaders: () => this.getters.inputDataContainer().find('table thead th'), 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'), nodeParameters: () => cy.getByTestId('node-parameters'), 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'), nodeRenameInput: () => cy.getByTestId('node-rename-input'), executePrevious: () => cy.getByTestId('execute-previous-node'), @@ -77,18 +85,11 @@ export class NDV extends BasePage { this.getters.parameterInput(parameterName).type(content); }, selectOptionInParameterDropdown: (parameterName: string, content: string) => { - this.getters - .parameterInput(parameterName) - .find('.option-headline') - .contains(content) - .click(); + this.getters.parameterInput(parameterName).find('.option-headline').contains(content).click(); }, rename: (newName: string) => { this.getters.nodeNameContainer().click(); - this.getters.nodeRenameInput() - .should('be.visible') - .type('{selectall}') - .type(newName); + this.getters.nodeRenameInput().should('be.visible').type('{selectall}').type(newName); cy.get('body').type('{enter}'); }, executePrevious: () => { @@ -104,10 +105,10 @@ export class NDV extends BasePage { cy.draganddrop('', droppable); }, 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') => { - this.getters.outputDisplayMode().find('label').contains(type).click({force: true}); + this.getters.outputDisplayMode().find('label').contains(type).click({ force: true }); }, selectInputNode: (nodeName: string) => { this.getters.inputSelect().find('.el-select').click(); diff --git a/cypress/pages/workflow.ts b/cypress/pages/workflow.ts index d895ccfab4..f09bda9f1f 100644 --- a/cypress/pages/workflow.ts +++ b/cypress/pages/workflow.ts @@ -26,7 +26,7 @@ export class WorkflowPage extends BasePage { canvasNodeByName: (nodeName: string) => this.getters.canvasNodes().filter(`:contains("${nodeName}")`), 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) => { return cy.get(this.getters.getEndpointSelector('input', nodeName, index)); @@ -79,7 +79,7 @@ export class WorkflowPage extends BasePage { workflowSettingsSaveButton: () => 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'), nodeViewBackground: () => cy.getByTestId('node-view-background'), @@ -155,11 +155,17 @@ export class WorkflowPage extends BasePage { saveWorkflowOnButtonClick: () => { this.getters.saveButton().should('contain', 'Save'); this.getters.saveButton().click(); - this.getters.saveButton().should('contain', 'Saved') + this.getters.saveButton().should('contain', 'Saved'); }, saveWorkflowUsingKeyboardShortcut: () => { 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: () => { this.getters.activatorSwitch().find('input').first().should('be.enabled'); this.getters.activatorSwitch().click(); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index ff64f69599..445645975c 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -40,10 +40,7 @@ Cypress.Commands.add('createFixtureWorkflow', (fixtureKey, workflowName) => { WorkflowPage.getters .workflowImportInput() .selectFile(`cypress/fixtures/${fixtureKey}`, { force: true }); - WorkflowPage.getters.workflowNameInput().should('be.disabled'); - WorkflowPage.getters.workflowNameInput().parent().click(); - WorkflowPage.getters.workflowNameInput().should('be.enabled'); - WorkflowPage.getters.workflowNameInput().clear().type(workflowName).type('{enter}'); + WorkflowPage.actions.setWorkflowName(workflowName); WorkflowPage.getters.saveButton().should('contain', 'Saved'); }); diff --git a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue index 2012311bdc..fc29c79688 100644 --- a/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue +++ b/packages/editor-ui/src/components/CredentialEdit/CredentialSharing.ee.vue @@ -61,6 +61,7 @@ :users="usersList" :currentUserId="usersStore.currentUser.id" :placeholder="$locale.baseText('credentialEdit.credentialSharing.select.placeholder')" + data-test-id="credential-sharing-modal-users-select" @input="onAddSharee" >