diff --git a/packages/integration-tests/src/tests/console/applications/helpers.ts b/packages/integration-tests/src/tests/console/applications/helpers.ts index cea8cebd0..3766fa627 100644 --- a/packages/integration-tests/src/tests/console/applications/helpers.ts +++ b/packages/integration-tests/src/tests/console/applications/helpers.ts @@ -25,7 +25,7 @@ export const expectFrameworksInGroup = async (page: Page, groupSelector: string) /* eslint-enable no-await-in-loop */ }; -export const expectToClickFramework = async (page: Page, framework: string) => { +export const expectToChooseAndClickApplicationFramework = async (page: Page, framework: string) => { const frameworkCard = await expect(page).toMatchElement( 'div[class*=card]:has(div[class$=header] div[class$=name])', { @@ -42,9 +42,9 @@ export const expectFrameworkExists = async (page: Page, framework: string) => { }); }; -export const expectToProceedCreationFrom = async ( +export const expectToProceedApplicationCreationFrom = async ( page: Page, - { name, description }: ApplicationCase + { name, description }: { name: string; description: string } ) => { // Expect the creation form to be open await expectModalWithTitle(page, 'Create Application'); diff --git a/packages/integration-tests/src/tests/console/applications/index.test.ts b/packages/integration-tests/src/tests/console/applications/index.test.ts index 24cbff1cf..b7f241ecb 100644 --- a/packages/integration-tests/src/tests/console/applications/index.test.ts +++ b/packages/integration-tests/src/tests/console/applications/index.test.ts @@ -20,8 +20,8 @@ import { } from './constants.js'; import { expectFrameworkExists, - expectToClickFramework, - expectToProceedCreationFrom, + expectToChooseAndClickApplicationFramework, + expectToProceedApplicationCreationFrom, expectToProceedSdkGuide, expectToProceedAppDeletion, expectFrameworksInGroup, @@ -54,9 +54,9 @@ describe('applications', () => { }); it('create the initial application from the table placeholder', async () => { - await expectToClickFramework(page, initialApp.framework); + await expectToChooseAndClickApplicationFramework(page, initialApp.framework); - await expectToProceedCreationFrom(page, initialApp); + await expectToProceedApplicationCreationFrom(page, initialApp); await expectToProceedSdkGuide(page, initialApp, true); @@ -126,9 +126,9 @@ describe('applications', () => { // Expect the framework exists after filtering await expectFrameworkExists(page, testApp.framework); - await expectToClickFramework(page, testApp.framework); + await expectToChooseAndClickApplicationFramework(page, testApp.framework); - await expectToProceedCreationFrom(page, testApp); + await expectToProceedApplicationCreationFrom(page, testApp); await expectToProceedSdkGuide(page, testApp); diff --git a/packages/integration-tests/src/tests/console/machine-to-machine-rbac/machime-to-machime-rbac.test.ts b/packages/integration-tests/src/tests/console/machine-to-machine-rbac/machime-to-machime-rbac.test.ts new file mode 100644 index 000000000..067de9360 --- /dev/null +++ b/packages/integration-tests/src/tests/console/machine-to-machine-rbac/machime-to-machime-rbac.test.ts @@ -0,0 +1,306 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { + expectConfirmModalAndAct, + expectModalWithTitle, + expectToClickModalAction, + goToAdminConsole, + waitForToast, +} from '#src/ui-helpers/index.js'; +import { + expectNavigation, + appendPathname, + generateResourceName, + generateResourceIndicator, + generateScopeName, + generateRoleName, +} from '#src/utils.js'; + +import { + expectToChooseAndClickApplicationFramework, + expectToProceedApplicationCreationFrom, +} from '../applications/helpers.js'; + +import { createM2mRoleAndAssignPermissions } from './utils.js'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('M2M RBAC', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + const managementApiResourceName = 'Logto Management API'; + const managementApiPermission = 'all'; + const apiResourceName = generateResourceName(); + const apiResourceIndicator = generateResourceIndicator(); + const permissionName = generateScopeName(); + const permissionDescription = 'Dummy permission description'; + const roleName = generateRoleName(); + const roleDescription = 'Dummy role description'; + + const rbacTestAppname = 'm2m-app-001'; + const m2mFramework = 'Machine-to-machine'; + + beforeAll(async () => { + await goToAdminConsole(); + }); + + describe('create api resource and permissions', () => { + it('navigate to api resources page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/api-resources', logtoConsoleUrl).href) + ); + + await expect(page).toMatchElement( + 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + { + text: 'API Resources', + } + ); + }); + + it('create an api resource', async () => { + await expect(page).toClick('div[class$=headline] button span', { + text: 'Create API Resource', + }); + + await expectModalWithTitle(page, 'Start with tutorials'); + + // Click bottom button to skip tutorials + await expect(page).toClick('.ReactModalPortal nav[class$=actionBar] button span', { + text: 'Continue without tutorial', + }); + + await expectModalWithTitle(page, 'Create API Resource'); + + await expect(page).toFillForm('.ReactModalPortal form', { + name: apiResourceName, + indicator: apiResourceIndicator, + }); + + await expectToClickModalAction(page, 'Create API Resource'); + + await waitForToast(page, { + text: `The API resource ${apiResourceName} has been successfully created`, + }); + + await expect(page).toMatchElement('div[class$=header] div[class$=info] div[class$=name]', { + text: apiResourceName, + }); + }); + + it('create permission for api resource', async () => { + await expect(page).toClick('nav div[class$=item] div[class$=link] a', { + text: 'Permissions', + }); + + await expect(page).toClick('div[class$=filter] button[class$=createButton] span', { + text: 'Create Permission', + }); + + await expectModalWithTitle(page, 'Create permission'); + + await expect(page).toFillForm('.ReactModalPortal form', { + name: permissionName, + description: permissionDescription, + }); + + await expectToClickModalAction(page, 'Create permission'); + + await waitForToast(page, { + text: `The permission ${permissionName} has been successfully created`, + }); + + await expect(page).toMatchElement('table tbody tr td div[class$=name]', { + text: permissionName, + }); + }); + }); + + describe('create m2m app', () => { + it('navigate to applications page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/applications', logtoConsoleUrl).href) + ); + await expect(page).toMatchElement( + 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + { + text: 'Applications', + } + ); + }); + + it('create a m2m app for rbac testing', async () => { + await expectToChooseAndClickApplicationFramework(page, m2mFramework); + + await expectToProceedApplicationCreationFrom(page, { + name: rbacTestAppname, + description: rbacTestAppname, + }); + + // Skip guide + await page.keyboard.press('Escape'); + }); + }); + + describe('create m2m role', () => { + it('navigate to roles page', async () => { + await expectNavigation(page.goto(appendPathname('/console/roles', logtoConsoleUrl).href)); + + await expect(page).toMatchElement( + 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + { + text: 'Roles', + } + ); + }); + + it('create a m2m role and assign permissions to the role', async () => { + await createM2mRoleAndAssignPermissions(page, { roleName, roleDescription }, [ + { apiResourceName, permissionName }, + { apiResourceName: managementApiResourceName, permissionName: managementApiPermission }, + ]); + }); + + it('delete a permission from a role on the role details page', async () => { + await expect(page).toClick('nav div[class$=item] div[class$=link] a', { + text: 'Permissions', + }); + + const permissionRow = await expect(page).toMatchElement( + 'table tbody tr:has(td div[class$=name])', + { text: permissionName } + ); + await expect(permissionRow).toClick('td[class$=deleteColumn] button'); + + await expectConfirmModalAndAct(page, { + title: 'Reminder', + actionText: 'Remove', + }); + + await waitForToast(page, { + text: `The permission "${permissionName}" was successfully removed from this role`, + }); + }); + + it('assign a permission to a role on the role details page', async () => { + await expect(page).toClick('div[class$=filter] button span', { + text: 'Assign Permissions', + }); + + await expectModalWithTitle(page, 'Assign permissions'); + + await expect(page).toClick( + '.ReactModalPortal div[class$=resourceItem] div[class$=title] div[class$=name]', + { + text: apiResourceName, + } + ); + + await expect(page).toClick( + '.ReactModalPortal div[class$=resourceItem] div[class$=sourceScopeItem] div[role=button]', + { + text: permissionName, + } + ); + + await expectToClickModalAction(page, 'Assign Permissions'); + + await waitForToast(page, { + text: 'The selected permissions were successfully assigned to this role', + }); + + await expect(page).toMatchElement('table tbody tr:has(td div[class$=name])', { + text: permissionName, + }); + }); + }); + + describe('assign a role to a m2m app (on role details page)', () => { + it('assign a role to a m2m app on the role details page', async () => { + await expect(page).toClick('nav div[class$=item] div[class$=link] a', { + text: 'Machine-to-machine apps', + }); + + await expect(page).toClick('div[class$=filter] button span', { + text: 'Assign applications', + }); + + await expectModalWithTitle(page, 'Assign apps'); + + await expect(page).toClick( + '.ReactModalPortal div[class$=rolesTransfer] div[class$=item] div[class$=title]', + { + text: rbacTestAppname, + } + ); + await expectToClickModalAction(page, 'Assign applications'); + + await waitForToast(page, { + text: 'The selected applications were successfully assigned to this role', + }); + + await expect(page).toMatchElement( + 'div[class$=applicationsTable] td div[class$=item] a[class$=title]', + { + text: rbacTestAppname, + } + ); + }); + }); + + describe('assign/remove a role to/from a m2m app (on m2m app details page)', () => { + it('remove a role form a m2m app on the app details page', async () => { + // Navigate to app details page + await expect(page).toClick('table tbody tr td a[class$=title]', { + text: rbacTestAppname, + }); + + await expect(page).toMatchElement( + 'div[class$=header] > div[class$=metadata] div[class$=name]', + { + text: rbacTestAppname, + } + ); + + // Go to roles tab + await expect(page).toClick('nav div[class$=item] div[class$=link] a', { + text: 'Roles', + }); + + const roleRow = await expect(page).toMatchElement('table tbody tr:has(td a[class$=title])', { + text: roleName, + }); + + // Click remove button + await expect(roleRow).toClick('td:last-of-type button'); + + await expectConfirmModalAndAct(page, { + title: 'Reminder', + actionText: 'Remove', + }); + + await waitForToast(page, { + text: `${roleName} was successfully removed from this user.`, + }); + }); + + it('add a role to m2m app on the application details page', async () => { + await expect(page).toClick('div[class$=filter] button span', { + text: 'Assign roles', + }); + + await expectModalWithTitle(page, `Assign roles to ${rbacTestAppname}`); + + await expect(page).toClick( + '.ReactModalPortal div[class$=rolesTransfer] div[class$=item] div[class$=name]', + { + text: roleName, + } + ); + + await expectToClickModalAction(page, 'Assign roles'); + + await waitForToast(page, { + text: 'Successfully assigned role(s)', + }); + }); + }); +}); diff --git a/packages/integration-tests/src/tests/console/machine-to-machine-rbac/utils.ts b/packages/integration-tests/src/tests/console/machine-to-machine-rbac/utils.ts new file mode 100644 index 000000000..fcb347edc --- /dev/null +++ b/packages/integration-tests/src/tests/console/machine-to-machine-rbac/utils.ts @@ -0,0 +1,66 @@ +import { type Page } from 'puppeteer'; + +import { + expectModalWithTitle, + expectToClickModalAction, + waitForToast, +} from '#src/ui-helpers/index.js'; + +export const createM2mRoleAndAssignPermissions = async ( + page: Page, + { roleName, roleDescription }: { roleName: string; roleDescription: string }, + apiResources: Array<{ apiResourceName: string; permissionName: string }> +) => { + await expect(page).toClick('div[class$=headline] button span', { + text: 'Create Role', + }); + + await expectModalWithTitle(page, 'Create Role'); + + // Expand role type selection + await expect(page).toClick('button[class$=roleTypeSelectionSwitch] span', { + text: 'Show more options', + }); + + await expect(page).toClick('div[class*=radioGroup][class$=roleTypes] div[class$=content]', { + text: 'Machine-to-machine app role', + }); + + await expect(page).toFillForm('.ReactModalPortal form', { + name: roleName, + description: roleDescription, + }); + + /* eslint-disable no-await-in-loop */ + for (const apiResource of apiResources) { + const { apiResourceName, permissionName } = apiResource; + // Assign customized permission + await expect(page).toClick( + '.ReactModalPortal div[class$=resourceItem] div[class$=title] div[class$=name]', + { + text: apiResourceName, + } + ); + + await expect(page).toClick( + '.ReactModalPortal div[class$=resourceItem] div[class$=sourceScopeItem] div[role=button]', + { + text: permissionName, + } + ); + } + /* eslint-enable no-await-in-loop */ + + await expectToClickModalAction(page, 'Create Role'); + + await waitForToast(page, { + text: `The role ${roleName} has been successfully created.`, + }); + + await expectModalWithTitle(page, 'Assign apps'); + await expectToClickModalAction(page, 'Skip for now'); + + await expect(page).toMatchElement('div[class$=header] div[class$=info] div[class$=name]', { + text: roleName, + }); +};