From 1581b5ac0a70c13dee8c959ae7d1761860c0dc61 Mon Sep 17 00:00:00 2001 From: QAComet Date: Mon, 6 Nov 2023 02:35:20 -0700 Subject: [PATCH] test: write tests for role management (#1396) --- .../fixtures/admin/create-role-page.js | 106 ++++ .../fixtures/admin/delete-role-modal.js | 19 + .../fixtures/admin/edit-role-page.js | 9 + .../fixtures/admin/edit-user-page.js | 1 + packages/e2e-tests/fixtures/admin/index.js | 18 +- .../fixtures/admin/role-conditions-modal.js | 47 ++ .../e2e-tests/fixtures/admin/roles-page.js | 79 +++ .../e2e-tests/fixtures/admin/users-page.js | 16 + .../e2e-tests/fixtures/authenticated-page.js | 1 + packages/e2e-tests/fixtures/login-page.js | 18 +- .../tests/admin/manage-roles.spec.js | 483 ++++++++++++++++++ .../tests/admin/role-conditions.spec.js | 69 +++ packages/web/src/components/AppBar/index.tsx | 1 + .../ConditionalIconButton/index.tsx | 1 + .../components/ControlledCheckbox/index.tsx | 3 + .../components/DeleteRoleButton/index.ee.tsx | 2 + .../PermissionSettings.ee.tsx | 10 +- .../PermissionCatalogField/index.ee.tsx | 3 + .../web/src/components/RoleList/index.ee.tsx | 20 +- .../web/src/pages/CreateRole/index.ee.tsx | 3 + packages/web/src/pages/EditRole/index.ee.tsx | 3 + 21 files changed, 899 insertions(+), 13 deletions(-) create mode 100644 packages/e2e-tests/fixtures/admin/create-role-page.js create mode 100644 packages/e2e-tests/fixtures/admin/delete-role-modal.js create mode 100644 packages/e2e-tests/fixtures/admin/edit-role-page.js create mode 100644 packages/e2e-tests/fixtures/admin/role-conditions-modal.js create mode 100644 packages/e2e-tests/fixtures/admin/roles-page.js create mode 100644 packages/e2e-tests/tests/admin/manage-roles.spec.js create mode 100644 packages/e2e-tests/tests/admin/role-conditions.spec.js diff --git a/packages/e2e-tests/fixtures/admin/create-role-page.js b/packages/e2e-tests/fixtures/admin/create-role-page.js new file mode 100644 index 00000000..bd6ff23c --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/create-role-page.js @@ -0,0 +1,106 @@ +const { AuthenticatedPage } = require('../authenticated-page'); +const { RoleConditionsModal } = require('./role-conditions-modal'); + +export class AdminCreateRolePage extends AuthenticatedPage { + screenshotPath = '/admin/create-role' + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.nameInput = page.getByTestId('name-input'); + this.descriptionInput = page.getByTestId('description-input'); + this.createButton = page.getByTestId('create-button'); + this.connectionRow = page.getByTestId('Connection-permission-row'); + this.executionRow = page.getByTestId('Execution-permission-row'); + this.flowRow = page.getByTestId('Flow-permission-row'); + } + + /** + * @param {('Connection'|'Execution'|'Flow')} subject + */ + getRoleConditionsModal (subject) { + return new RoleConditionsModal(this.page, subject); + } + + async getPermissionConfigs () { + const subjects = ['Connection', 'Flow', 'Execution']; + const permissionConfigs = []; + for (let subject of subjects) { + const row = this.getSubjectRow(subject); + const actionInputs = await this.getRowInputs(row); + Object.keys(actionInputs).forEach(action => { + permissionConfigs.push({ + action, + locator: actionInputs[action], + subject, + row + }); + }); + } + return permissionConfigs; + } + + /** + * + * @param {( + * 'Connection' | 'Flow' | 'Execution' + * )} subject + */ + getSubjectRow (subject) { + const k = `${subject.toLowerCase()}Row` + if (this[k]) { + return this[k] + } else { + throw 'Unknown row' + } + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async getRowInputs (row) { + const inputs = { + // settingsButton: row.getByTestId('permission-settings-button') + } + for (let input of ['create', 'read', 'update', 'delete', 'publish']) { + const testId = `${input}-checkbox` + if (await row.getByTestId(testId).count() > 0) { + inputs[input] = row.getByTestId(testId).locator('input'); + } + } + return inputs + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickPermissionSettings (row) { + await row.getByTestId('permission-settings-button').click(); + } + + /** + * + * @param {string} subject + * @param {'create'|'read'|'update'|'delete'|'publish'} action + * @param {boolean} val + */ + async updateAction (subject, action, val) { + const row = await this.getSubjectRow(subject); + const inputs = await this.getRowInputs(row); + if (inputs[action]) { + if (await inputs[action].isChecked()) { + if (!val) { + await inputs[action].click(); + } + } else { + if (val) { + await inputs[action].click(); + } + } + } else { + throw new Error(`${subject} does not have action ${action}`) + } + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/delete-role-modal.js b/packages/e2e-tests/fixtures/admin/delete-role-modal.js new file mode 100644 index 00000000..e456f6bd --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/delete-role-modal.js @@ -0,0 +1,19 @@ +export class DeleteRoleModal { + screenshotPath = '/admin/delete-role-modal'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + this.page = page; + this.modal = page.getByTestId('delete-role-modal'); + this.cancelButton = this.modal.getByTestId('confirmation-cancel-button'); + this.deleteButton = this.modal.getByTestId('confirmation-confirm-button'); + } + + async close () { + await this.page.click('body', { + position: { x: 10, y: 10 } + }); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/edit-role-page.js b/packages/e2e-tests/fixtures/admin/edit-role-page.js new file mode 100644 index 00000000..679597ec --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/edit-role-page.js @@ -0,0 +1,9 @@ +const { AdminCreateRolePage } = require('./create-role-page') + +export class AdminEditRolePage extends AdminCreateRolePage { + constructor (page) { + super(page); + delete this.createButton; + this.updateButton = page.getByTestId('update-button'); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/edit-user-page.js b/packages/e2e-tests/fixtures/admin/edit-user-page.js index 69755a61..0297796a 100644 --- a/packages/e2e-tests/fixtures/admin/edit-user-page.js +++ b/packages/e2e-tests/fixtures/admin/edit-user-page.js @@ -13,6 +13,7 @@ export class AdminEditUserPage extends AuthenticatedPage { super(page); this.fullNameInput = page.getByTestId('full-name-input'); this.emailInput = page.getByTestId('email-input'); + this.roleInput = page.getByTestId('role.id-autocomplete'); this.updateButton = page.getByTestId('update-button'); } diff --git a/packages/e2e-tests/fixtures/admin/index.js b/packages/e2e-tests/fixtures/admin/index.js index 191546f5..8c25fd7c 100644 --- a/packages/e2e-tests/fixtures/admin/index.js +++ b/packages/e2e-tests/fixtures/admin/index.js @@ -2,6 +2,10 @@ const { AdminCreateUserPage } = require('./create-user-page'); const { AdminEditUserPage } = require('./edit-user-page'); const { AdminUsersPage } = require('./users-page'); +const { AdminRolesPage } = require('./roles-page'); +const { AdminCreateRolePage } = require('./create-role-page'); +const { AdminEditRolePage } = require('./edit-role-page'); + export const adminFixtures = { adminUsersPage: async ({ page }, use) => { await use(new AdminUsersPage(page)); @@ -11,5 +15,15 @@ export const adminFixtures = { }, adminEditUserPage: async ({page}, use) => { await use(new AdminEditUserPage(page)); - } -} \ No newline at end of file + }, + adminRolesPage: async ({ page}, use) => { + await use(new AdminRolesPage(page)); + }, + adminEditRolePage: async ({ page}, use) => { + await use(new AdminEditRolePage(page)); + }, + adminCreateRolePage: async ({ page}, use) => { + await use(new AdminCreateRolePage(page)); + }, +} + diff --git a/packages/e2e-tests/fixtures/admin/role-conditions-modal.js b/packages/e2e-tests/fixtures/admin/role-conditions-modal.js new file mode 100644 index 00000000..4e2c64fe --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/role-conditions-modal.js @@ -0,0 +1,47 @@ +export class RoleConditionsModal { + + /** + * @param {import('@playwright/test').Page} page + * @param {('Connection'|'Execution'|'Flow')} subject + */ + constructor (page, subject) { + this.page = page; + this.modal = page.getByTestId(`${subject}-role-conditions-modal`); + this.modalBody = this.modal.getByTestId('role-conditions-modal-body'); + this.createCheckbox = this.modal.getByTestId( + 'isCreator-create-checkbox' + ).locator('input'); + this.readCheckbox = this.modal.getByTestId( + 'isCreator-read-checkbox' + ).locator('input'); + this.updateCheckbox = this.modal.getByTestId( + 'isCreator-update-checkbox' + ).locator('input'); + this.deleteCheckbox = this.modal.getByTestId( + 'isCreator-delete-checkbox' + ).locator('input'); + this.publishCheckbox = this.modal.getByTestId( + 'isCreator-publish-checkbox' + ).locator('input'); + this.applyButton = this.modal.getByTestId('confirmation-confirm-button'); + this.cancelButton = this.modal.getByTestId('confirmation-cancel-button'); + } + + async getAvailableConditions () { + let conditions = {}; + const actions = ['create', 'read', 'update', 'delete', 'publish']; + for (let action of actions) { + const locator = this[`${action}Checkbox`]; + if (locator && await locator.count() > 0) { + conditions[action] = locator; + } + } + return conditions; + } + + async close () { + await this.page.click('body', { + position: { x: 10, y: 10 } + }); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/roles-page.js b/packages/e2e-tests/fixtures/admin/roles-page.js new file mode 100644 index 00000000..c051b8ee --- /dev/null +++ b/packages/e2e-tests/fixtures/admin/roles-page.js @@ -0,0 +1,79 @@ +const { AuthenticatedPage } = require('../authenticated-page'); +const { DeleteRoleModal } = require('./delete-role-modal') + +export class AdminRolesPage extends AuthenticatedPage { + screenshotPath = '/admin-roles'; + + /** + * @param {import('@playwright/test').Page} page + */ + constructor (page) { + super(page); + this.roleDrawerLink = page.getByTestId('roles-drawer-link'); + this.createRoleButton = page.getByTestId('create-role'); + this.deleteRoleModal = new DeleteRoleModal(page); + this.roleRow = page.getByTestId('role-row'); + this.rolesLoader = page.getByTestId('roles-list-loader'); + } + + /** + * + * @param {boolean} isMobile - navigation on smaller devices requires the + * user to open up the drawer menu + */ + async navigateTo (isMobile=false) { + await this.profileMenuButton.click(); + await this.adminMenuItem.click(); + if (isMobile) { + await this.drawerMenuButton.click(); + } + await this.roleDrawerLink.click(); + } + + /** + * @param {string} name + */ + async getRoleRowByName (name) { + return this.roleRow.filter({ + has: this.page.getByTestId('role-name').filter({ + hasText: name + }) + }); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async getRowData (row) { + return { + role: await row.getByTestId('role-name').textContent(), + description: await row.getByTestId('role-description').textContent(), + canEdit: await row.getByTestId( + 'role-edit' + ).isEnabled(), + canDelete: await row.getByTestId( + 'role-delete' + ).isEnabled() + } + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickEditRole (row) { + await row.getByTestId('role-edit').click(); + } + + /** + * @param {import('@playwright/test').Locator} row + */ + async clickDeleteRole (row) { + await row.getByTestId('role-delete').click(); + return this.deleteRoleModal; + } + + async editRole (subject) { + const row = await this.getRoleRowByName(subject); + await this.clickEditRole(row); + } +} \ No newline at end of file diff --git a/packages/e2e-tests/fixtures/admin/users-page.js b/packages/e2e-tests/fixtures/admin/users-page.js index 88641df2..4696fada 100644 --- a/packages/e2e-tests/fixtures/admin/users-page.js +++ b/packages/e2e-tests/fixtures/admin/users-page.js @@ -25,6 +25,11 @@ export class AdminUsersPage extends AuthenticatedPage { async navigateTo () { await this.profileMenuButton.click(); await this.adminMenuItem.click(); + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached' + }); + } } /** @@ -66,8 +71,14 @@ export class AdminUsersPage extends AuthenticatedPage { /** * @param {string} email + * @returns {import('@playwright/test').Locator | null} */ async findUserPageWithEmail (email) { + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached' + }); + } // start at the first page const firstPageDisabled = await this.firstPageButton.isDisabled(); if (!firstPageDisabled) { @@ -75,6 +86,11 @@ export class AdminUsersPage extends AuthenticatedPage { } while (true) { + if (await this.usersLoader.isVisible()) { + await this.usersLoader.waitFor({ + state: 'detached' + }); + } const rowLocator = await this.getUserRowByEmail(email); if ((await rowLocator.count()) === 1) { return rowLocator; diff --git a/packages/e2e-tests/fixtures/authenticated-page.js b/packages/e2e-tests/fixtures/authenticated-page.js index 58fc4d44..3bfc1584 100644 --- a/packages/e2e-tests/fixtures/authenticated-page.js +++ b/packages/e2e-tests/fixtures/authenticated-page.js @@ -14,6 +14,7 @@ export class AuthenticatedPage extends BasePage { this.adminMenuItem = this.page.getByRole('menuitem', { name: 'Admin' }); this.userInterfaceDrawerItem = this.page.getByTestId('user-interface-drawer-link'); this.appBar = this.page.getByTestId('app-bar'); + this.drawerMenuButton = this.page.getByTestId('drawer-menu-button'); this.goToDashboardButton = this.page.getByTestId('go-back-drawer-link'); this.typographyLogo = this.page.getByTestId('typography-logo'); this.customLogo = this.page.getByTestId('custom-logo'); diff --git a/packages/e2e-tests/fixtures/login-page.js b/packages/e2e-tests/fixtures/login-page.js index cfef7c0d..ad588dd3 100644 --- a/packages/e2e-tests/fixtures/login-page.js +++ b/packages/e2e-tests/fixtures/login-page.js @@ -1,9 +1,19 @@ -const path = require('node:path'); -const { expect } = require('@playwright/test'); const { BasePage } = require('./base-page'); export class LoginPage extends BasePage { path = '/login'; + static defaultEmail = process.env.LOGIN_EMAIL; + static defaultPassword = process.env.LOGIN_PASSWORD; + + static setDefaultLogin (email, password) { + this.defaultEmail = email; + this.defaultPassword = password; + } + + static resetDefaultLogin () { + this.defaultEmail = process.env.LOGIN_EMAIL; + this.defaultPassword = process.env.LOGIN_PASSWORD; + } /** * @param {import('@playwright/test').Page} page @@ -22,8 +32,8 @@ export class LoginPage extends BasePage { } async login( - email = process.env.LOGIN_EMAIL, - password = process.env.LOGIN_PASSWORD + email = LoginPage.defaultEmail, + password = LoginPage.defaultPassword ) { await this.page.goto(this.path); await this.emailTextField.fill(email); diff --git a/packages/e2e-tests/tests/admin/manage-roles.spec.js b/packages/e2e-tests/tests/admin/manage-roles.spec.js new file mode 100644 index 00000000..36f7aa08 --- /dev/null +++ b/packages/e2e-tests/tests/admin/manage-roles.spec.js @@ -0,0 +1,483 @@ +const { test, expect } = require('../../fixtures/index'); +const { LoginPage } = require('../../fixtures/login-page'); + +test.describe('Role management page', () => { + + test('Admin role is not deletable', async ({ adminRolesPage }) => { + await adminRolesPage.navigateTo(); + const adminRow = await adminRolesPage.getRoleRowByName('Admin'); + const rowCount = await adminRow.count(); + await expect(rowCount).toBe(1); + const data = await adminRolesPage.getRowData(adminRow); + await expect(data.role).toBe('Admin'); + await expect(data.canEdit).toBe(true); + await expect(data.canDelete).toBe(false); + }); + + test( + 'Can create, edit, and delete a role', + async ({ + adminCreateRolePage, adminEditRolePage, adminRolesPage, page + }) => { + await test.step('Create a new role', async () => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.nameInput.fill('Create Edit Test'); + await adminCreateRolePage.descriptionInput.fill('Test description'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + + let roleRow = await test.step( + 'Make sure role data is correct', + async () => { + const roleRow = await adminRolesPage.getRoleRowByName( + 'Create Edit Test' + ); + const rowCount = await roleRow.count(); + await expect(rowCount).toBe(1); + const roleData = await adminRolesPage.getRowData(roleRow); + await expect(roleData.role).toBe('Create Edit Test'); + await expect(roleData.description).toBe('Test description'); + await expect(roleData.canEdit).toBe(true); + await expect(roleData.canDelete).toBe(true); + return roleRow + } + ); + + await test.step('Edit the role', async () => { + await adminRolesPage.clickEditRole(roleRow); + await adminEditRolePage.nameInput.fill('Create Update Test'); + await adminEditRolePage.descriptionInput.fill( + 'Update test description' + ); + await adminEditRolePage.updateButton.click(); + await adminEditRolePage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminEditRolePage.getSnackbarData( + 'snackbar-edit-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminEditRolePage.closeSnackbar(); + }); + + roleRow = await test.step( + 'Make sure changes reflected on roles page', + async () => { + const roleRow = await adminRolesPage.getRoleRowByName( + 'Create Update Test' + ); + const rowCount = await roleRow.count(); + await expect(rowCount).toBe(1); + const roleData = await adminRolesPage.getRowData(roleRow); + await expect(roleData.role).toBe('Create Update Test'); + await expect(roleData.description).toBe('Update test description'); + await expect(roleData.canEdit).toBe(true); + await expect(roleData.canDelete).toBe(true); + return roleRow; + } + ); + + await test.step('Delete the role', async () => { + await adminRolesPage.clickDeleteRole(roleRow); + const deleteModal = adminRolesPage.deleteRoleModal; + await deleteModal.modal.waitFor({ + state: 'attached' + }); + await deleteModal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + await deleteModal.modal.waitFor({ + state: 'detached' + }); + const rowCount = await roleRow.count(); + await expect(rowCount).toBe(0); + }); + } + ); + + // This test breaks right now + test.skip( + 'Make sure create/edit role page is scrollable', + async ({ adminRolesPage, page }) => { + const initViewportSize = page.viewportSize; + await page.setViewportSize({ + width: 800, + height: 400 + }); + + await test.step('Ensure create role page is scrollable', async () => { + await adminRolesPage.navigateTo(true); + await adminRolesPage.createRoleButton.click(); + + const initScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await page.mouse.move(400, 100); + await page.mouse.click(400, 100); + await page.mouse.wheel(200, 0); + const updatedScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await expect(initScrollTop).not.toBe(updatedScrollTop); + }); + + await test.step('Ensure edit role page is scrollable', async () => { + await adminRolesPage.navigateTo(true); + const adminRow = await adminRolesPage.getRoleRowByName('Admin'); + await adminRolesPage.clickEditRole(adminRow); + + const initScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await page.mouse.move(400, 100); + await page.mouse.wheel(200, 0); + const updatedScrollTop = await page.evaluate(() => { + return document.documentElement.scrollTop; + }); + await expect(initScrollTop).not.toBe(updatedScrollTop); + }); + + await test.step('Reset viewport', async () => { + await page.setViewportSize(initViewportSize); + }); + } + ); + + test( + 'Cannot delete a role with a user attached to it', + async ({ + adminCreateRolePage, adminRolesPage, + adminUsersPage, adminCreateUserPage, adminEditUserPage, + page + }) => { + await adminRolesPage.navigateTo(); + await test.step('Create a new role', async () => { + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.nameInput.fill('Delete Role'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + await test.step( + 'Create a new user with the "Delete Role" role', + async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill('User Role Test'); + await adminCreateUserPage.emailInput.fill('user-role-test@automatisch.io'); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Delete Role' } + ).click(); + await adminCreateUserPage.createButton.click(); + await adminUsersPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + } + ); + await test.step( + 'Try to delete "Delete Role" role when new user has it', + async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-error' + ); + await expect(snackbar.variant).toBe('error'); + await adminRolesPage.closeSnackbar(); + await modal.close(); + } + ); + await test.step( + 'Change the role the user has', + async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.usersLoader.waitFor({ + state: 'detached' + }); + const row = await adminUsersPage.findUserPageWithEmail( + 'user-role-test@automatisch.io' + ); + await adminUsersPage.clickEditUser(row); + await adminEditUserPage.roleInput.click(); + await adminEditUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminEditUserPage.updateButton.click(); + await adminEditUserPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminEditUserPage.getSnackbarData( + 'snackbar-edit-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminEditUserPage.closeSnackbar(); + } + ); + await test.step( + 'Delete the original role', + async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName('Delete Role'); + const modal = await adminRolesPage.clickDeleteRole(row); + await expect(modal.modal).toBeVisible(); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + } + ); + } + ); + + test( + 'Deleting a role after deleting a user with that role', + async ({ + adminCreateRolePage, adminRolesPage, + adminUsersPage, adminCreateUserPage, + page + }) => { + await adminRolesPage.navigateTo(); + await test.step('Create a new role', async () => { + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.nameInput.fill('Cannot Delete Role'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + await test.step('Create a new user with this role', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill('User Delete Role Test'); + await adminCreateUserPage.emailInput.fill( + 'user-delete-role-test@automatisch.io' + ); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Cannot Delete Role' } + ).click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateUserPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateUserPage.closeSnackbar(); + }); + await test.step('Delete this user', async () => { + await adminUsersPage.navigateTo(); + const row = await adminUsersPage.findUserPageWithEmail( + 'user-delete-role-test@automatisch.io' + ); + const modal = await adminUsersPage.clickDeleteUser(row); + await modal.deleteButton.click(); + await adminUsersPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminUsersPage.getSnackbarData( + 'snackbar-delete-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminUsersPage.closeSnackbar(); + }); + await test.step('Try deleting this role', async () => { + await adminRolesPage.navigateTo(); + const row = await adminRolesPage.getRoleRowByName( + 'Cannot Delete Role' + ); + const modal = await adminRolesPage.clickDeleteRole(row); + await modal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached' + }); + /* + * TODO: await snackbar - make assertions based on product + * decisions + const snackbar = await adminRolesPage.getSnackbarData(); + await expect(snackbar.variant).toBe('...'); + */ + await adminRolesPage.closeSnackbar(); + }); + } + ); +}); + +test( + 'Accessibility of role management page', + async ({ + page, + adminUsersPage, adminCreateUserPage, adminEditUserPage, + adminRolesPage, adminCreateRolePage, + }) => { + test.slow(); + await test.step('Create the basic test role', async () => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + await adminCreateRolePage.nameInput.fill('Basic Test'); + await adminCreateRolePage.createButton.click(); + await adminCreateRolePage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateRolePage.getSnackbarData( + 'snackbar-create-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + + await test.step('Create a new user with the basic role', async () => { + await adminUsersPage.navigateTo(); + await adminUsersPage.createUserButton.click(); + await adminCreateUserPage.fullNameInput.fill('Role Test'); + await adminCreateUserPage.emailInput.fill('basic-role-test@automatisch.io'); + await adminCreateUserPage.passwordInput.fill('sample'); + await adminCreateUserPage.roleInput.click(); + await adminCreateUserPage.page.getByRole( + 'option', { name: 'Basic Test' } + ).click(); + await adminCreateUserPage.createButton.click(); + await adminCreateUserPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminCreateUserPage.getSnackbarData( + 'snackbar-create-user-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminCreateRolePage.closeSnackbar(); + }); + + await test.step('Logout and login to the basic role user', async () => { + await page.getByTestId('profile-menu-button').click(); + await page.getByTestId('logout-item').click(); + // await page.reload({ waitUntil: 'networkidle' }); + const loginPage = new LoginPage(page); + await loginPage.login('basic-role-test@automatisch.io', 'sample'); + await expect(loginPage.loginButton).not.toBeVisible(); + await expect(page).toHaveURL('/flows'); + }); + + await test.step( + 'Navigate to the admin settings page and make sure it is blank', + async () => { + const pageUrl = new URL(page.url()); + const url = `${pageUrl.origin}/admin-settings/users`; + await page.goto(url); + await page.waitForTimeout(750); + const isUnmounted = await page.evaluate(() => { + const root = document.querySelector('#root'); + if (root) { + return root.children.length === 0; + } + return false; + }); + await expect(isUnmounted).toBe(true); + } + ); + + await test.step( + 'Log back into the admin account', + async () => { + await page.goto('/'); + await page.getByTestId('profile-menu-button').click(); + await page.getByTestId('logout-item').click(); + const loginPage = new LoginPage(page); + await loginPage.login(); + } + ); + + await test.step( + 'Move the user off the role', + async () => { + await adminUsersPage.navigateTo(); + const row = await adminUsersPage.findUserPageWithEmail( + 'basic-role-test@automatisch.io' + ); + await adminUsersPage.clickEditUser(row); + await adminEditUserPage.roleInput.click(); + await adminEditUserPage.page.getByRole( + 'option', { name: 'Admin' } + ).click(); + await adminEditUserPage.updateButton.click(); + await adminEditUserPage.snackbar.waitFor({ + state: 'attached' + }); + await adminEditUserPage.closeSnackbar(); + } + ); + + await test.step( + 'Delete the role', + async () => { + await adminRolesPage.navigateTo(); + const roleRow = await adminRolesPage.getRoleRowByName( + 'Basic Test' + ); + await adminRolesPage.clickDeleteRole(roleRow); + const deleteModal = adminRolesPage.deleteRoleModal; + await deleteModal.modal.waitFor({ + state: 'attached' + }); + await deleteModal.deleteButton.click(); + await adminRolesPage.snackbar.waitFor({ + state: 'attached' + }); + const snackbar = await adminRolesPage.getSnackbarData( + 'snackbar-delete-role-success' + ); + await expect(snackbar.variant).toBe('success'); + await adminRolesPage.closeSnackbar(); + await deleteModal.modal.waitFor({ + state: 'detached' + }); + const rowCount = await roleRow.count(); + await expect(rowCount).toBe(0); + } + ); + } +); \ No newline at end of file diff --git a/packages/e2e-tests/tests/admin/role-conditions.spec.js b/packages/e2e-tests/tests/admin/role-conditions.spec.js new file mode 100644 index 00000000..6f69ad58 --- /dev/null +++ b/packages/e2e-tests/tests/admin/role-conditions.spec.js @@ -0,0 +1,69 @@ +const { test, expect } = require('../../fixtures/index'); + +test( + 'Role permissions conform with role conditions ', + async({ adminRolesPage, adminCreateRolePage }) => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + + /* + example config: { + action: 'read', + subject: 'connection', + row: page.getByTestId('connection-permission-row'), + locator: row.getByTestId('read-checkbox') + } + */ + const permissionConfigs = + await adminCreateRolePage.getPermissionConfigs(); + + await test.step( + 'Iterate over each permission config and make sure role conditions conform', + async () => { + for (let config of permissionConfigs) { + await config.locator.click(); + await adminCreateRolePage.clickPermissionSettings(config.row); + const modal = adminCreateRolePage.getRoleConditionsModal( + config.subject + ); + await expect(modal.modal).toBeVisible(); + const conditions = await modal.getAvailableConditions(); + for (let conditionAction of Object.keys(conditions)) { + if (conditionAction === config.action) { + await expect(conditions[conditionAction]).not.toBeDisabled(); + } else { + await expect(conditions[conditionAction]).toBeDisabled(); + } + } + await modal.close(); + await config.locator.click(); + } + } + ); + } +); + +test( + 'Default role permissions conforms with role conditions', + async({ adminRolesPage, adminCreateRolePage }) => { + await adminRolesPage.navigateTo(); + await adminRolesPage.createRoleButton.click(); + + const subjects = ['Connection', 'Execution', 'Flow']; + for (let subject of subjects) { + const row = adminCreateRolePage.getSubjectRow(subject) + const modal = adminCreateRolePage.getRoleConditionsModal(subject); + await adminCreateRolePage.clickPermissionSettings(row); + await expect(modal.modal).toBeVisible(); + const availableConditions = await modal.getAvailableConditions(); + const conditions = ['create', 'read', 'update', 'delete', 'publish']; + for (let condition of conditions) { + if (availableConditions[condition]) { + await expect(availableConditions[condition]).toBeDisabled(); + } + } + await modal.close(); + } + + } +); \ No newline at end of file diff --git a/packages/web/src/components/AppBar/index.tsx b/packages/web/src/components/AppBar/index.tsx index abadd456..60bea460 100644 --- a/packages/web/src/components/AppBar/index.tsx +++ b/packages/web/src/components/AppBar/index.tsx @@ -56,6 +56,7 @@ export default function AppBar(props: AppBarProps): React.ReactElement { aria-label="open drawer" onClick={drawerOpen ? onDrawerClose : onDrawerOpen} sx={{ mr: 2 }} + data-test="drawer-menu-button" > {drawerOpen && matchSmallScreens ? : } diff --git a/packages/web/src/components/ConditionalIconButton/index.tsx b/packages/web/src/components/ConditionalIconButton/index.tsx index ec981391..ddcff1c9 100644 --- a/packages/web/src/components/ConditionalIconButton/index.tsx +++ b/packages/web/src/components/ConditionalIconButton/index.tsx @@ -21,6 +21,7 @@ export default function ConditionalIconButton(props: any): React.ReactElement { component={buttonProps.component} to={buttonProps.to} disabled={buttonProps.disabled} + data-test={buttonProps['data-test']} > {icon} diff --git a/packages/web/src/components/ControlledCheckbox/index.tsx b/packages/web/src/components/ControlledCheckbox/index.tsx index ea8f38ac..1b8b269c 100644 --- a/packages/web/src/components/ControlledCheckbox/index.tsx +++ b/packages/web/src/components/ControlledCheckbox/index.tsx @@ -5,6 +5,7 @@ import Checkbox, { CheckboxProps } from '@mui/material/Checkbox'; type ControlledCheckboxProps = { name: string; defaultValue?: boolean; + dataTest?: string; } & Omit; export default function ControlledCheckbox( @@ -18,6 +19,7 @@ export default function ControlledCheckbox( disabled = false, onBlur, onChange, + dataTest, ...checkboxProps } = props; @@ -53,6 +55,7 @@ export default function ControlledCheckbox( onBlur?.(...args); }} inputRef={ref} + data-test={dataTest} /> ); }} diff --git a/packages/web/src/components/DeleteRoleButton/index.ee.tsx b/packages/web/src/components/DeleteRoleButton/index.ee.tsx index 2a07fac6..9eeb520e 100644 --- a/packages/web/src/components/DeleteRoleButton/index.ee.tsx +++ b/packages/web/src/components/DeleteRoleButton/index.ee.tsx @@ -48,6 +48,7 @@ export default function DeleteRoleButton(props: DeleteRoleButtonProps) { disabled={!allowed || disabled} onClick={() => setShowConfirmation(true)} size="small" + data-test="role-delete" > @@ -62,6 +63,7 @@ export default function DeleteRoleButton(props: DeleteRoleButtonProps) { onConfirm={handleConfirm} cancelButtonChildren={formatMessage('deleteRoleButton.cancel')} confirmButtionChildren={formatMessage('deleteRoleButton.confirm')} + data-test="delete-role-modal" /> ); diff --git a/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.tsx b/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.tsx index 6b9f9e04..0592d3de 100644 --- a/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.tsx +++ b/packages/web/src/components/PermissionCatalogField/PermissionSettings.ee.tsx @@ -66,10 +66,15 @@ export default function PermissionSettings(props: PermissionSettingsProps) { }; return ( - + {formatMessage('permissionSettings.title')} - + @@ -113,6 +118,7 @@ export default function PermissionSettings(props: PermissionSettingsProps) { {action.subjects.includes(subject) && ( {subject.label} @@ -74,6 +75,7 @@ const PermissionCatalogField = ({ )} @@ -89,6 +91,7 @@ const PermissionCatalogField = ({ size="small" onClick={() => setDialogName(subject.key)} disabled={disabled} + data-test="permission-settings-button" > diff --git a/packages/web/src/components/RoleList/index.ee.tsx b/packages/web/src/components/RoleList/index.ee.tsx index 285873ac..e19817fd 100644 --- a/packages/web/src/components/RoleList/index.ee.tsx +++ b/packages/web/src/components/RoleList/index.ee.tsx @@ -49,21 +49,29 @@ export default function RoleList(): React.ReactElement { - {loading && } + {loading && } {!loading && roles.map((role) => ( - {role.name} + {role.name} - - {role.description} - + {role.description} @@ -72,6 +80,7 @@ export default function RoleList(): React.ReactElement { size="small" component={Link} to={URLS.ROLE(role.id)} + data-test="role-edit" > @@ -79,6 +88,7 @@ export default function RoleList(): React.ReactElement { diff --git a/packages/web/src/pages/CreateRole/index.ee.tsx b/packages/web/src/pages/CreateRole/index.ee.tsx index 4ba49e6e..21d9d710 100644 --- a/packages/web/src/pages/CreateRole/index.ee.tsx +++ b/packages/web/src/pages/CreateRole/index.ee.tsx @@ -69,12 +69,14 @@ export default function CreateRole(): React.ReactElement { name="name" label={formatMessage('roleForm.name')} fullWidth + data-test="name-input" /> {formatMessage('createRole.submit')} diff --git a/packages/web/src/pages/EditRole/index.ee.tsx b/packages/web/src/pages/EditRole/index.ee.tsx index 098d32cb..8b412e6a 100644 --- a/packages/web/src/pages/EditRole/index.ee.tsx +++ b/packages/web/src/pages/EditRole/index.ee.tsx @@ -92,6 +92,7 @@ export default function EditRole(): React.ReactElement { required={true} name="name" label={formatMessage('roleForm.name')} + data-test="name-input" fullWidth /> @@ -99,6 +100,7 @@ export default function EditRole(): React.ReactElement { disabled={role.isAdmin} name="description" label={formatMessage('roleForm.description')} + data-test="description-input" fullWidth /> @@ -116,6 +118,7 @@ export default function EditRole(): React.ReactElement { sx={{ boxShadow: 2 }} loading={loading} disabled={role?.isAdmin || roleLoading} + data-test="update-button" > {formatMessage('editRole.submit')}