From 67ab1b3d52a135e96aa162575e7839e4d98ff7ef Mon Sep 17 00:00:00 2001 From: Darcy Ye Date: Fri, 8 Dec 2023 18:55:52 +0800 Subject: [PATCH] chore(test): add console ITs for SSO connectors creation and deletion (#5073) --- .../tests/console/sso-connectors/helpers.ts | 38 +++++ .../sso-connectors-test-cases.ts | 43 ++++++ .../sso-connectors/sso-connectors.test.ts | 142 ++++++++++++++++++ 3 files changed, 223 insertions(+) create mode 100644 packages/integration-tests/src/tests/console/sso-connectors/helpers.ts create mode 100644 packages/integration-tests/src/tests/console/sso-connectors/sso-connectors-test-cases.ts create mode 100644 packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts diff --git a/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts b/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts new file mode 100644 index 000000000..d0c6152ab --- /dev/null +++ b/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts @@ -0,0 +1,38 @@ +import { conditionalString } from '@silverhand/essentials'; +import { type Page } from 'puppeteer'; + +import { type SsoConnectorTestCase } from './sso-connectors-test-cases.js'; + +export const findModalFooterButton = async (isButtonDisabled = false) => { + return page.waitForSelector( + `.ReactModalPortal div[class$=footer] button${conditionalString( + isButtonDisabled && '[disabled]' + )}` + ); +}; + +export const fillSsoConnectorCreationModal = async ( + page: Page, + { connectorFactoryName, connectorName }: SsoConnectorTestCase +) => { + // Button should be disabled util form is filled. + await expect(findModalFooterButton(true)).resolves.toBeTruthy(); + + // Select connector factory + await expect(page).toClick( + `.ReactModalPortal div[role=radio] div[class$=ssoConnector] div[class$=content] div[class$=name] span`, + { text: connectorFactoryName } + ); + + // Button should be disabled util form is filled. + await expect(findModalFooterButton(true)).resolves.toBeTruthy(); + + await expect(page).toFill( + '.ReactModalPortal input[type=text][name=connectorName]', + connectorName + ); + + // Button should enabled. + const createButton = await findModalFooterButton(); + await createButton?.click(); +}; diff --git a/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors-test-cases.ts b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors-test-cases.ts new file mode 100644 index 000000000..738bd7a4c --- /dev/null +++ b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors-test-cases.ts @@ -0,0 +1,43 @@ +// Will extend this type definition later since we are going to configure the SSO connectors with specific values. +export type SsoConnectorTestCase = { + connectorName: string; + connectorFactoryName: string; +}; + +const microsoftEntraIdName = 'Microsoft Entra ID'; +const microsoftEntraID: SsoConnectorTestCase = { + connectorName: microsoftEntraIdName, + connectorFactoryName: microsoftEntraIdName, +}; + +const googleWorkspaceName = 'Google Workspace'; +const googleWorkspace: SsoConnectorTestCase = { + connectorName: googleWorkspaceName, + connectorFactoryName: googleWorkspaceName, +}; + +const oktaName = 'Okta'; +const okta: SsoConnectorTestCase = { + connectorName: oktaName, + connectorFactoryName: oktaName, +}; + +const oidcName = 'OIDC'; +const oidc: SsoConnectorTestCase = { + connectorName: oidcName, + connectorFactoryName: oidcName, +}; + +const samlName = 'SAML'; +const saml: SsoConnectorTestCase = { + connectorName: samlName, + connectorFactoryName: samlName, +}; + +export const ssoConnectorTestCases: SsoConnectorTestCase[] = [ + microsoftEntraID, + googleWorkspace, + okta, + oidc, + saml, +]; diff --git a/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts new file mode 100644 index 000000000..12a3c353f --- /dev/null +++ b/packages/integration-tests/src/tests/console/sso-connectors/sso-connectors.test.ts @@ -0,0 +1,142 @@ +import { logtoConsoleUrl as logtoConsoleUrlString } from '#src/constants.js'; +import { + goToAdminConsole, + expectModalWithTitle, + expectToClickDetailsPageOption, + expectConfirmModalAndAct, +} from '#src/ui-helpers/index.js'; +import { expectNavigation, appendPathname } from '#src/utils.js'; + +import { findModalFooterButton, fillSsoConnectorCreationModal } from './helpers.js'; +import { ssoConnectorTestCases } from './sso-connectors-test-cases.js'; + +await page.setViewport({ width: 1920, height: 1080 }); + +describe('create SSO connectors', () => { + const logtoConsoleUrl = new URL(logtoConsoleUrlString); + + beforeAll(async () => { + // Enter admin console + await goToAdminConsole(); + }); + + it('navigate to Enterprise SSO connectors listing page', async () => { + await expectNavigation( + page.goto(appendPathname('/console/enterprise-sso', logtoConsoleUrl).href) + ); + + await expect(page).toMatchElement( + 'div[class$=main] div[class$=headline] div[class$=titleEllipsis]', + { + text: 'Enterprise SSO', + } + ); + + expect(page.url()).toBe(new URL(`console/enterprise-sso`, logtoConsoleUrl).href); + }); + + it('can open create SSO connector modal from table placeholder and create the first SSO connector', async () => { + // When no SSO connector is created, use the create button in placeholder. + await expect(page).toClick('table div[class$=placeholder] button span', { + text: 'Add enterprise connector', + }); + + await expectModalWithTitle(page, 'Add enterprise connector'); + + await fillSsoConnectorCreationModal(page, ssoConnectorTestCases[0]!); + + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + // Come back to Enterprise SSO listing page. + await page.goto(appendPathname('/console/enterprise-sso', logtoConsoleUrl).href); + }); + + it.each(ssoConnectorTestCases.slice(1))( + 'create other SSO connectors %p', + async (ssoConnector) => { + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + // When there are existing SSO connector(s), use the create button in page header. + await expect(page).toClick('div[class$=main] div[class$=headline] button[type=button] span', { + text: 'Add enterprise connector', + }); + + await expectModalWithTitle(page, 'Add enterprise connector'); + + await fillSsoConnectorCreationModal(page, ssoConnector); + + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + // Come back to Enterprise SSO listing page. + await page.goto(appendPathname('/console/enterprise-sso', logtoConsoleUrl).href); + } + ); + + it('should block the create of SSO connector with a duplicated name', async () => { + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + // When there are existing SSO connector(s), use the create button in page header. + await expect(page).toClick('div[class$=main] div[class$=headline] button[type=button] span', { + text: 'Add enterprise connector', + }); + + await expectModalWithTitle(page, 'Add enterprise connector'); + + /** + * To check only the `duplicated connector name` is blocked, even if the + * existing SSO connector (with the occupied name) is created with a different connector factory. + */ + const { connectorFactoryName } = ssoConnectorTestCases[0]!; + const { connectorName } = ssoConnectorTestCases[1]!; + await fillSsoConnectorCreationModal(page, { + connectorFactoryName, + connectorName, + }); + + // Error message should be shown. + await expect(page).toMatchElement( + '.ReactModalPortal div[class$=field] div[class$=errorMessage]', + { + text: 'Connector name already exists. Please choose a different name.', + } + ); + + await expect(findModalFooterButton(true)).resolves.toBeTruthy(); + + await expect(page).toFill( + '.ReactModalPortal input[type=text][name=connectorName]', + `${connectorName} (1)` + ); + + // Button should enabled. + const createButton = await findModalFooterButton(); + await createButton?.click(); + + // Wait until the user is redirected to the details page. + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + /** + * If the page still shows the modal, the URL ends with `/enterprise-sso/create`; + * if the SSO connector is successfully created, user is redirected to the details + * page `/enterprise-sso/${id}` and then automatically be redirected to `/enterprise-sso/${id}/connection` (default tab). + */ + expect(page.url().endsWith('/enterprise-sso/create')).toBeFalsy(); + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + expect(page.url().endsWith('/connection')).toBeTruthy(); + }); + + it('can delete an SSO connector from details page', async () => { + // Delete connector + await expectToClickDetailsPageOption(page, 'Delete'); + + await expectConfirmModalAndAct(page, { + title: 'Delete enterprise SSO connector', + actionText: 'Delete', + }); + + // Wait to navigate to the connector list page + await page.waitForNavigation({ waitUntil: 'networkidle0' }); + + expect(page.url()).toBe(new URL(`console/enterprise-sso`, logtoConsoleUrl).href); + }); +});