mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-25 20:54:07 -08:00
test(editor): Add user management e2e tests (#5438)
* ✅ Added initial UM test using new commands * ✅ Added rest of the UM tests
This commit is contained in:
parent
b8980f6118
commit
d9a4c2c66d
|
@ -1,53 +0,0 @@
|
||||||
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* User A - Instance owner
|
|
||||||
* User B - User, owns C1, W1, W2
|
|
||||||
* User C - User, owns C2
|
|
||||||
*
|
|
||||||
* W1 - Workflow owned by User B, shared with User C
|
|
||||||
* W2 - Workflow owned by User B
|
|
||||||
*
|
|
||||||
* C1 - Credential owned by User B
|
|
||||||
* 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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
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 A and UserB to instance`, () => {
|
|
||||||
cy.inviteUsers({ instanceOwner, users });
|
|
||||||
});
|
|
||||||
});
|
|
96
cypress/e2e/18-user-management.cy.ts
Normal file
96
cypress/e2e/18-user-management.cy.ts
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
import { MainSidebar } from './../pages/sidebar/main-sidebar';
|
||||||
|
import { DEFAULT_USER_EMAIL, DEFAULT_USER_PASSWORD } from '../constants';
|
||||||
|
import { SettingsSidebar, SettingsUsersPage, WorkflowPage, WorkflowsPage } from '../pages';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* User A - Instance owner
|
||||||
|
* User B - User, owns C1, W1, W2
|
||||||
|
* User C - User, owns C2
|
||||||
|
*
|
||||||
|
* W1 - Workflow owned by User B, shared with User C
|
||||||
|
* W2 - Workflow owned by User B
|
||||||
|
*
|
||||||
|
* C1 - Credential owned by User B
|
||||||
|
* 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 usersSettingsPage = new SettingsUsersPage();
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
|
||||||
|
describe('User Management', () => {
|
||||||
|
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 B and User C to instance`, () => {
|
||||||
|
cy.inviteUsers({ instanceOwner, users });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent non-owners to access UM settings', () => {
|
||||||
|
usersSettingsPage.actions.loginAndVisit(users[0].email, users[0].password, false)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow instance owner to access UM settings', () => {
|
||||||
|
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly render UM settings page for instance owners', () => {
|
||||||
|
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.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).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');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete user and their data', () => {
|
||||||
|
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true);
|
||||||
|
usersSettingsPage.actions.opedDeleteDialog(users[0].email);
|
||||||
|
usersSettingsPage.getters.deleteDataRadioButton().realClick();
|
||||||
|
usersSettingsPage.getters.deleteDataInput().type('delete all data');
|
||||||
|
usersSettingsPage.getters.deleteUserButton().realClick();
|
||||||
|
workflowPage.getters.successToast().should('contain', 'User deleted');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete user and transfer their data', () => {
|
||||||
|
usersSettingsPage.actions.loginAndVisit(instanceOwner.email, instanceOwner.password, true);
|
||||||
|
usersSettingsPage.actions.opedDeleteDialog(users[1].email);
|
||||||
|
usersSettingsPage.getters.transferDataRadioButton().realClick();
|
||||||
|
usersSettingsPage.getters.userSelectDropDown().realClick();
|
||||||
|
usersSettingsPage.getters.userSelectOptions().first().realClick();
|
||||||
|
usersSettingsPage.getters.deleteUserButton().realClick();
|
||||||
|
workflowPage.getters.successToast().should('contain', 'User deleted');
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,12 @@
|
||||||
|
import { SettingsSidebar } from './sidebar/settings-sidebar';
|
||||||
|
import { MainSidebar } from './sidebar/main-sidebar';
|
||||||
|
import { WorkflowsPage } from './workflows';
|
||||||
import { BasePage } from './base';
|
import { BasePage } from './base';
|
||||||
|
|
||||||
|
const workflowsPage = new WorkflowsPage();
|
||||||
|
const mainSidebar = new MainSidebar();
|
||||||
|
const settingsSidebar = new SettingsSidebar();
|
||||||
|
|
||||||
export class SettingsUsersPage extends BasePage {
|
export class SettingsUsersPage extends BasePage {
|
||||||
url = '/settings/users';
|
url = '/settings/users';
|
||||||
getters = {
|
getters = {
|
||||||
|
@ -7,8 +14,38 @@ export class SettingsUsersPage extends BasePage {
|
||||||
inviteButton: () => cy.getByTestId('settings-users-invite-button').last(),
|
inviteButton: () => cy.getByTestId('settings-users-invite-button').last(),
|
||||||
inviteUsersModal: () => cy.getByTestId('inviteUser-modal').last(),
|
inviteUsersModal: () => cy.getByTestId('inviteUser-modal').last(),
|
||||||
inviteUsersModalEmailsInput: () => cy.getByTestId('emails').find('input').first(),
|
inviteUsersModalEmailsInput: () => cy.getByTestId('emails').find('input').first(),
|
||||||
|
userListItems: () => cy.get('[data-test-id^="user-list-item"]'),
|
||||||
|
userItem: (email: string) => cy.getByTestId(`user-list-item-${email.toLowerCase()}`),
|
||||||
|
userActionsToggle: (email: string) => this.getters.userItem(email).find('[data-test-id="action-toggle"]'),
|
||||||
|
deleteUserAction: () => cy.getByTestId('action-toggle-dropdown').find('li:contains("Delete"):visible'),
|
||||||
|
confirmDeleteModal: () => cy.getByTestId('deleteUser-modal').last(),
|
||||||
|
transferDataRadioButton: () => this.getters.confirmDeleteModal().find('[role="radio"]').first(),
|
||||||
|
deleteDataRadioButton: () => this.getters.confirmDeleteModal().find('[role="radio"]').last(),
|
||||||
|
userSelectDropDown: () => this.getters.confirmDeleteModal().find('.n8n-select'),
|
||||||
|
userSelectOptions: () => cy.get('.el-select-dropdown:visible .el-select-dropdown__item'),
|
||||||
|
deleteUserButton: () => this.getters.confirmDeleteModal().find('button:contains("Delete")'),
|
||||||
|
deleteDataInput: () => cy.getByTestId('delete-data-input').find('input').first(),
|
||||||
};
|
};
|
||||||
actions = {
|
actions = {
|
||||||
goToOwnerSetup: () => this.getters.setUpOwnerButton().click(),
|
goToOwnerSetup: () => this.getters.setUpOwnerButton().click(),
|
||||||
|
loginAndVisit: (email: string, password: string, isOwner: boolean) => {
|
||||||
|
cy.signin({ email, password });
|
||||||
|
cy.visit(workflowsPage.url);
|
||||||
|
mainSidebar.actions.goToSettings();
|
||||||
|
if (isOwner) {
|
||||||
|
settingsSidebar.getters.menuItem('Users').click();
|
||||||
|
cy.url().should('match', new RegExp(this.url));
|
||||||
|
} else {
|
||||||
|
settingsSidebar.getters.menuItem('Users').should('not.exist');
|
||||||
|
// Should be redirected to workflows page if trying to access UM url
|
||||||
|
cy.visit('/settings/users');
|
||||||
|
cy.url().should('match', new RegExp(workflowsPage.url));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
opedDeleteDialog: (email: string) => {
|
||||||
|
this.getters.userActionsToggle(email).click();
|
||||||
|
this.getters.deleteUserAction().realClick();
|
||||||
|
this.getters.confirmDeleteModal().should('be.visible');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="$style.container">
|
<span :class="$style.container" data-test-id="action-toggle">
|
||||||
<el-dropdown
|
<el-dropdown
|
||||||
:placement="placement"
|
:placement="placement"
|
||||||
:size="size"
|
:size="size"
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
:key="user.id"
|
:key="user.id"
|
||||||
class="ph-no-capture"
|
class="ph-no-capture"
|
||||||
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
|
:class="i === sortedUsers.length - 1 ? $style.itemContainer : $style.itemWithBorder"
|
||||||
|
:data-test-id="`user-list-item-${user.email}`"
|
||||||
>
|
>
|
||||||
<n8n-user-info v-bind="user" :isCurrentUser="currentUserId === user.id" />
|
<n8n-user-info v-bind="user" :isCurrentUser="currentUserId === user.id" />
|
||||||
<div :class="$style.badgeContainer">
|
<div :class="$style.badgeContainer">
|
||||||
|
|
|
@ -41,7 +41,11 @@
|
||||||
$locale.baseText('settings.users.deleteWorkflowsAndCredentials')
|
$locale.baseText('settings.users.deleteWorkflowsAndCredentials')
|
||||||
}}</n8n-text>
|
}}</n8n-text>
|
||||||
</el-radio>
|
</el-radio>
|
||||||
<div :class="$style.optionInput" v-if="operation === 'delete'">
|
<div
|
||||||
|
:class="$style.optionInput"
|
||||||
|
v-if="operation === 'delete'"
|
||||||
|
data-test-id="delete-data-input"
|
||||||
|
>
|
||||||
<n8n-input-label :label="$locale.baseText('settings.users.deleteConfirmationMessage')">
|
<n8n-input-label :label="$locale.baseText('settings.users.deleteConfirmationMessage')">
|
||||||
<n8n-input
|
<n8n-input
|
||||||
:value="deleteConfirmText"
|
:value="deleteConfirmText"
|
||||||
|
|
Loading…
Reference in a new issue