diff --git a/packages/integration-tests/src/tests/console/rbac/organization-rbac.test.ts b/packages/integration-tests/src/tests/console/rbac/organization-rbac.test.ts new file mode 100644 index 000000000..e9aedc38f --- /dev/null +++ b/packages/integration-tests/src/tests/console/rbac/organization-rbac.test.ts @@ -0,0 +1,90 @@ +import ExpectApiResources from '#src/ui-helpers/expect-api-resources.js'; +import ExpectConsole from '#src/ui-helpers/expect-console.js'; +import ExpectOrganizations from '#src/ui-helpers/expect-organizations.js'; +import { + generateResourceIndicator, + generateResourceName, + generateRoleName, + generateScopeName, +} from '#src/utils.js'; + +const expectConsole = new ExpectConsole(); +const expectApiResources = new ExpectApiResources(); +const expectOrganizations = new ExpectOrganizations(); + +const apiResourceName = generateResourceName(); +const apiResourceIndicator = generateResourceIndicator(); +const apiPermissionName = generateScopeName(); + +const organizationPermissionName = generateScopeName(); +const organizationRoleName = generateRoleName(); + +const dummyPermissionDescription = 'Dummy permission description'; + +describe('Organization RBAC', () => { + beforeAll(async () => { + await expectConsole.start(); + }); + + it('navigates to API resources page', async () => { + await expectApiResources.gotoPage('/api-resources', 'API resources'); + await expectApiResources.toExpectTableHeaders('API name', 'API Identifier'); + }); + + it('creates an API resource', async () => { + await expectApiResources.toCreateApiResource({ + name: apiResourceName, + indicator: apiResourceIndicator, + }); + }); + + it('creates an API permission for organization role', async () => { + await expectApiResources.toCreateApiResourcePermission( + { name: apiPermissionName, description: dummyPermissionDescription }, + apiResourceName + ); + }); + + it('navigates to the organization template', async () => { + await expectConsole.gotoPage('/organization-template', 'Organization template'); + await expectConsole.toExpectTabs('Organization roles', 'Organization permissions'); + await expectConsole.toExpectTableHeaders('Organization Role', 'Permissions'); + }); + + it('creates an organization permission', async () => { + await expectOrganizations.toCreateOrganizationPermission({ + name: organizationPermissionName, + description: dummyPermissionDescription, + }); + }); + + it('creates an organization role', async () => { + await expectOrganizations.toCreateOrganizationRole({ + name: organizationRoleName, + description: dummyPermissionDescription, + }); + }); + + it('assigns organization permissions and API permissions for organization role', async () => { + await expectOrganizations.toAssignPermissionsForOrganizationRole({ + organizationPermission: organizationPermissionName, + apiPermission: { + resource: apiResourceName, + permission: apiPermissionName, + }, + forOrganizationRole: organizationRoleName, + }); + }); + + // Clean up + it('deletes created resources', async () => { + // Delete created organization role + await expectOrganizations.toDeleteOrganizationRole(organizationRoleName); + + // Delete created organization permissions + await expectOrganizations.toDeleteOrganizationPermission(organizationPermissionName); + + // Delete created API resource + await expectApiResources.toDeleteApiResource(apiResourceName); + }); +}); diff --git a/packages/integration-tests/src/ui-helpers/expect-api-resources.ts b/packages/integration-tests/src/ui-helpers/expect-api-resources.ts new file mode 100644 index 000000000..b8a877a28 --- /dev/null +++ b/packages/integration-tests/src/ui-helpers/expect-api-resources.ts @@ -0,0 +1,105 @@ +import { cls } from '#src/utils.js'; + +import ExpectConsole from './expect-console.js'; +import { expectToClickDetailsPageOption, expectToClickModalAction } from './index.js'; + +export default class ExpectApiResources extends ExpectConsole { + /** + * Go to the api resources page and create a new API resource, then assert that the URL matches + * the API resource detail page. + * + * @param {Object} params The parameters for creating the API resource. + * @param {string} params.name The name of the API resource to create. + * @param {string} params.indicator The indicator of the API resource to create. + */ + async toCreateApiResource({ name, indicator }: { name: string; indicator: string }) { + await this.gotoPage('/api-resources', 'API resources'); + await this.toClickButton('Create API resource'); + + await this.toExpectModal('Start with tutorials'); + + // Click bottom button to skip tutorials + await this.toClickButton('Continue without tutorial', false); + + await this.toExpectModal('Create API resource'); + + await this.toFillForm({ + name, + indicator, + }); + + await this.toClick( + ['.ReactModalPortal', `button${cls('primary')}`].join(' '), + 'Create API resource', + false + ); + + this.toMatchUrl(/\/api-resources\/.+$/); + } + + /** + * Go to the api resource details page by given resource name and create a new API permission, then assert the permission is created. + * + * @param {Object} params The parameters for creating the API permission. + * @param {string} params.name The name of the API permission to create. + * @param {string} params.description The description of the API permission to create. + * @param {string} forResourceName The name of the API resource for which the permission is created. + */ + async toCreateApiResourcePermission( + { + name, + description, + }: { + name: string; + description: string; + }, + forResourceName: string + ) { + await this.gotoPage('/api-resources', 'API resources'); + await this.toExpectTableHeaders('API name', 'API Identifier'); + + await expect(this.page).toClick(['table', 'tbody', 'tr', 'td', `a${cls('title')}`].join(' '), { + text: forResourceName, + }); + + // Expect the API resource details page + await expect(this.page).toMatchElement([cls('header'), cls('metadata'), 'div'].join(' '), { + text: forResourceName, + }); + + await this.toClickButton('Create permission', false); + + await this.toExpectModal('Create permission'); + + await this.toFillForm({ name, description }); + + await this.toClick( + ['.ReactModalPortal', `button${cls('primary')}`].join(' '), + 'Create permission', + false + ); + + await this.waitForToast(`The permission ${name} has been successfully created`); + + await this.toExpectTableCell(name); + } + + /** + * Go to the api resource details page by given resource name and delete the API resource. + * + * @param {string} name The name of the API resource to delete. + */ + async toDeleteApiResource(name: string) { + await this.gotoPage('/api-resources', 'API resources'); + await this.toExpectTableCell(name); + await this.toClickTableCell(name); + + await expectToClickDetailsPageOption(this.page, 'Delete'); + + await this.toExpectModal('Reminder'); + await this.toFill('.ReactModalPortal input', name); + + await expectToClickModalAction(this.page, 'Delete'); + await this.waitForToast(`The API resource ${name} has been successfully deleted`); + } +} diff --git a/packages/integration-tests/src/ui-helpers/expect-console.ts b/packages/integration-tests/src/ui-helpers/expect-console.ts index b73e3b8b0..6494570c3 100644 --- a/packages/integration-tests/src/ui-helpers/expect-console.ts +++ b/packages/integration-tests/src/ui-helpers/expect-console.ts @@ -19,7 +19,11 @@ type ExpectConsoleOptions = { tenantId?: string; }; -export type ConsoleTitle = 'Sign-in experience' | 'Organizations'; +export type ConsoleTitle = + | 'Sign-in experience' + | 'Organizations' + | 'API resources' + | 'Organization template'; export default class ExpectConsole extends ExpectPage { readonly options: Required; @@ -59,9 +63,15 @@ export default class ExpectConsole extends ExpectPage { /** * Navigate to a specific page in the Console. + * If the current page is the target page, it will not navigate. */ async gotoPage(pathname: string, title: ConsoleTitle) { - await this.navigateTo(this.buildUrl(path.join(this.options.tenantId, pathname))); + const target = this.buildUrl(path.join(this.options.tenantId, pathname)); + if (this.page.url() === target.href) { + return; + } + + await this.navigateTo(target); await expect(this.page).toMatchElement( [dcls('main'), dcls('container'), dcls('title')].join(' '), { text: title } @@ -112,6 +122,19 @@ export default class ExpectConsole extends ExpectPage { }); } + /** + * To click a table cell with the given text. + * @param text The text to expect, case-insensitive. + * @param shouldNavigate Whether to navigate to the page after clicking the cell. + */ + async toClickTableCell(text: string, shouldNavigate = true) { + await this.toClick( + ['table', 'tbody', 'tr', 'td'].join(' '), + new RegExp(text, 'i'), + shouldNavigate + ); + } + /** * Expect a modal to appear with the given title. * diff --git a/packages/integration-tests/src/ui-helpers/expect-organizations.ts b/packages/integration-tests/src/ui-helpers/expect-organizations.ts index be27b4e67..07b305704 100644 --- a/packages/integration-tests/src/ui-helpers/expect-organizations.ts +++ b/packages/integration-tests/src/ui-helpers/expect-organizations.ts @@ -1,6 +1,7 @@ -import { cls } from '#src/utils.js'; +import { cls, dcls } from '#src/utils.js'; import ExpectConsole from './expect-console.js'; +import { selectDropdownMenuItem } from './select-dropdown-menu-item.js'; export default class ExpectOrganizations extends ExpectConsole { /** @@ -20,4 +21,156 @@ export default class ExpectOrganizations extends ExpectConsole { await this.toClick(['.ReactModalPortal', `button${cls('primary')}`].join(' '), 'Create', false); this.toMatchUrl(/\/organizations\/.+$/); } + + /** + * Go to he organization template page and create a new organization permission, + * then assert then assert the permission is created. + * + * @param param The parameters for creating the organization permission. + * @param param.name The name of the organization permission. + * @param param.description The description of the organization permission. + */ + async toCreateOrganizationPermission({ + name, + description, + }: { + name: string; + description: string; + }) { + await this.gotoPage('/organization-template', 'Organization template'); + await this.toClickTab('Organization permissions'); + await this.toClickButton('Create organization permission', false); + + // Use fill input since no form tag is present in this modal + await this.toExpectModal('Create organization permission'); + await this.toFillInput('name', name); + await this.toFillInput('description', description); + await this.toClickButton('Create permission', false); + + await this.toExpectTableCell(name); + } + + /** + * Go to the organization template page and create a new organization role, + * then skip the permission assignment and assert that the URL matches + * the organization detail page. + * + * @param param The parameters for creating the organization role. + * @param param.name The name of the organization role. + * @param param.description The description of the organization role. + */ + async toCreateOrganizationRole({ name, description }: { name: string; description: string }) { + await this.gotoPage('/organization-template/organization-roles', 'Organization template'); + await this.toClickTab('Organization roles'); + await this.toClickButton('Create organization role', false); + + // Use fill input since no form tag is present in this modal + await this.toExpectModal('Create organization role'); + await this.toFillInput('name', name); + await this.toFillInput('description', description); + await this.toClickButton('Create role', false); + + // Skip permission assignment + await this.toExpectModal('Assign permissions'); + await this.toClickButton('Discard'); + + this.toMatchUrl(/\/organization-template\/organization-roles\/.+$/); + } + + /** + * Go to the organization role details page, assign organization permissions and API permissions for an organization role, + * then assert the permissions are assigned. + * + * @param param The parameters for assigning permissions for an organization role. + * @param param.organizationPermission The name of the organization permission to assign. + * @param param.apiPermission The API permission to assign. + * @param param.forOrganizationRole The organization role to assign the permissions. + */ + async toAssignPermissionsForOrganizationRole({ + organizationPermission, + apiPermission: { resource: apiResource, permission: apiPermission }, + forOrganizationRole, + }: { + organizationPermission: string; + apiPermission: { + resource: string; + permission: string; + }; + forOrganizationRole: string; + }) { + await this.navigateToOrganizationRoleDetailsPage(forOrganizationRole); + + await this.toClickButton('Assign permissions', false); + await this.toExpectModal('Assign permissions'); + // Select organization permission + await this.toClickTab('Organization permissions'); + await this.toClick(`div[role=button]`, organizationPermission, false); + + // Select API permission + await this.toClickTab('API permissions'); + await this.toClick(`div[role=button]`, apiResource, false); + await this.toClick(`div[role=button]`, apiPermission, false); + + await this.toClickButton('Save', false); + + await this.toExpectTableCell(organizationPermission); + await this.toExpectTableCell(apiPermission); + } + + /** + * Go to the organization role details page and delete an organization role, + * then assert that the URL matches the organization roles page. + * + * @param name The name of the organization role to delete. + */ + async toDeleteOrganizationRole(name: string) { + await this.navigateToOrganizationRoleDetailsPage(name); + + // Open the dropdown menu + await this.toClick([dcls('header'), `button${cls('withIcon')}`].join(' '), undefined, false); + await this.toClick(`${dcls('danger')}[role=menuitem]`, 'Delete', false); + await this.toExpectModal('Reminder'); + await this.toClick(['.ReactModalPortal', `button${cls('danger')}`].join(' '), 'Delete'); + await this.waitForToast(`Organization role ${name} was successfully deleted.`, 'success'); + this.toMatchUrl(/\/organization-template\/organization-roles$/); + } + + /** + * Go to the organization permissions page and delete an organization permission, + * + * @param name The name of the organization permission to delete. + */ + async toDeleteOrganizationPermission(name: string) { + await this.gotoPage('/organization-template', 'Organization template'); + await this.toClickTab('Organization permissions'); + await this.toExpectTableCell(name); + + // Open the dropdown menu from the table row + const permissionRow = await expect(this.page).toMatchElement('table tbody tr:has(td div)', { + text: name, + }); + + // Click the action button from the permission row + await expect(permissionRow).toClick('td:last-of-type button'); + + await selectDropdownMenuItem(this.page, 'div[role=menuitem]', 'Delete permission'); + + await this.toExpectModal('Reminder'); + await this.toClick(['.ReactModalPortal', `button${cls('danger')}`].join(' '), 'Delete', false); + } + + /** + * Navigate to the organization role details page by given role name. + * + * @param name The name of the organization role. + */ + private async navigateToOrganizationRoleDetailsPage(name: string) { + await this.gotoPage('/organization-template', 'Organization template'); + await this.toClickTab('Organization roles'); + await this.toExpectTableCell(name); + await this.toClickTableCell(name); + + // Assert in details page + this.toMatchUrl(/\/organization-template\/organization-roles\/.+$/); + } }