diff --git a/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts b/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts index afef62594..21283c111 100644 --- a/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts +++ b/packages/integration-tests/src/tests/console/sso-connectors/helpers.ts @@ -1,6 +1,7 @@ -import { conditionalString } from '@silverhand/essentials'; +import { conditional, conditionalString } from '@silverhand/essentials'; import { type Page } from 'puppeteer'; +import { expectToSaveChanges, waitForToast } from '#src/ui-helpers/index.js'; import { dcls, cls } from '#src/utils.js'; import { type SsoConnectorTestCase, type Protocol } from './sso-connectors-test-cases.js'; @@ -68,6 +69,65 @@ const checkSsoConnectorConnectionTabInfo = async (page: Page, protocol: Protocol } }; +const checkOidcConfigPreview = async (previewResults: Record) => { + await Promise.all( + Object.entries(previewResults).map(async ([key, value]) => { + const valueField = await expect(page).toMatchElement( + [`${dcls('container')}${cls('oidcConfigPreview')}`, `div:has(${dcls('title')})`].join(' '), + { text: key } + ); + const valueDisplayed = await valueField.$eval( + [dcls('content')].join(' '), + (element) => element.textContent + ); + + expect(valueDisplayed).toBe(value); + }) + ); +}; + +const checkSamlConfigPreview = async (previewResults: Record) => { + await Promise.all( + Object.entries(previewResults).map(async ([key, value]) => { + const valueField = await expect(page).toMatchElement( + [`${dcls('samlMetadataForm')}`, `${dcls('container')}`, `div:has(${dcls('title')})`].join( + ' ' + ), + { text: key } + ); + const valueDisplayed = await valueField.$eval( + [ + dcls('content'), + conditional(key === 'Signing certificate' && dcls('certificatePreview')), + ].join(' '), + (element) => element.textContent + ); + + expect(valueDisplayed).toBe(value); + }) + ); +}; + +// Configure the SSO connector connection and check the validity by comparing the preview results. +const configureSsoConnectorConnection = async ( + page: Page, + formData: Record, + protocol: Protocol, + previewResults: Record +) => { + await expect(page).toFillForm(dcls('form'), formData); + await expectToSaveChanges(page); + await waitForToast(page, { text: 'Saved' }); + + if (protocol === 'OIDC') { + await checkOidcConfigPreview(previewResults); + } + + if (protocol === 'SAML') { + await checkSamlConfigPreview(previewResults); + } +}; + export const findModalFooterButton = async (isButtonDisabled = false) => { return page.waitForSelector( `.ReactModalPortal div[class$=footer] button${conditionalString( @@ -78,7 +138,7 @@ export const findModalFooterButton = async (isButtonDisabled = false) => { export const fillSsoConnectorCreationModal = async ( page: Page, - { connectorFactoryName, connectorName, protocol }: SsoConnectorTestCase, + { connectorFactoryName, connectorName, protocol, formData, previewResults }: SsoConnectorTestCase, checkConnectionInfo = false ) => { // Button should be disabled util form is filled. @@ -114,5 +174,7 @@ export const fillSsoConnectorCreationModal = async ( await page.waitForNavigation({ waitUntil: 'networkidle0' }); await checkSsoConnectorConnectionTabInfo(page, protocol); + + await configureSsoConnectorConnection(page, formData, protocol, previewResults); } }; 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 index cd2f63ad2..04840b091 100644 --- 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 @@ -5,6 +5,33 @@ export type SsoConnectorTestCase = { connectorName: string; connectorFactoryName: string; protocol: Protocol; + formData: Record; + previewResults: Record; +}; + +const oidcFormData = { + clientId: 'client-id', + clientSecret: 'client-secret', +}; + +const samlFormData = { + // This is a real metadata URL from Microsoft Entra ID test application. + metadataUrl: + 'https://login.microsoftonline.com/ac016212-4f8d-46c6-892c-57c90a255a02/federationmetadata/2007-06/federationmetadata.xml?appid=f562b098-dc8c-4e20-8c4b-ea55554fbd8c', +}; + +export const oidcPreviewResults = { + 'Authorization endpoint': 'https://accounts.google.com/o/oauth2/v2/auth', + 'Token endpoint': 'https://oauth2.googleapis.com/token', + 'User information endpoint': 'https://openidconnect.googleapis.com/v1/userinfo', + 'JSON web key set endpoint': 'https://www.googleapis.com/oauth2/v3/certs', + Issuer: 'https://accounts.google.com', +}; + +export const samlPreviewResults = { + 'Sign on URL': 'https://login.microsoftonline.com/ac016212-4f8d-46c6-892c-57c90a255a02/saml2', + Issuer: 'https://sts.windows.net/ac016212-4f8d-46c6-892c-57c90a255a02/', + 'Signing certificate': 'Expiring Sunday, October 25, 2026', }; const microsoftEntraIdName = 'Microsoft Entra ID'; @@ -12,6 +39,8 @@ const microsoftEntraID: SsoConnectorTestCase = { connectorName: microsoftEntraIdName, connectorFactoryName: microsoftEntraIdName, protocol: 'SAML', + formData: samlFormData, + previewResults: samlPreviewResults, }; const googleWorkspaceName = 'Google Workspace'; @@ -19,6 +48,8 @@ const googleWorkspace: SsoConnectorTestCase = { connectorName: googleWorkspaceName, connectorFactoryName: googleWorkspaceName, protocol: 'OIDC', + formData: oidcFormData, + previewResults: oidcPreviewResults, }; const oktaName = 'Okta'; @@ -26,6 +57,9 @@ const okta: SsoConnectorTestCase = { connectorName: oktaName, connectorFactoryName: oktaName, protocol: 'OIDC', + // Google Workspace connector have `issuer` predefined, for other OIDC-protocol-based connectors, we use Google's issuer to test the config fetcher and preview. + formData: { ...oidcFormData, issuer: 'https://accounts.google.com' }, + previewResults: oidcPreviewResults, }; const oidcName = 'OIDC'; @@ -33,6 +67,9 @@ const oidc: SsoConnectorTestCase = { connectorName: oidcName, connectorFactoryName: oidcName, protocol: 'OIDC', + // Google Workspace connector have `issuer` predefined, for other OIDC-protocol-based connectors, we use Google's issuer to test the config fetcher and preview. + formData: { ...oidcFormData, issuer: 'https://accounts.google.com' }, + previewResults: oidcPreviewResults, }; const samlName = 'SAML'; @@ -40,6 +77,8 @@ const saml: SsoConnectorTestCase = { connectorName: samlName, connectorFactoryName: samlName, protocol: 'SAML', + formData: samlFormData, + previewResults: samlPreviewResults, }; export const ssoConnectorTestCases: SsoConnectorTestCase[] = [ 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 index d65446013..fa663b283 100644 --- 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 @@ -62,7 +62,8 @@ describe('create SSO connectors', () => { }); it.each(ssoConnectorTestCases.slice(1))( - 'create other SSO connectors %p', + // The full object of `ssoConnector` test case is too burdensome for the test title, so we only use the index here. + 'create other SSO connectors %#', async (ssoConnector) => { await page.waitForNavigation({ waitUntil: 'networkidle0' }); @@ -100,13 +101,15 @@ describe('create SSO connectors', () => { * 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, protocol } = ssoConnectorTestCases[0]!; + const { connectorFactoryName, protocol, formData, previewResults } = ssoConnectorTestCases[0]!; const { connectorName } = ssoConnectorTestCases[1]!; // Since the creation process is expected to be blocked in this test case, we do not want to check the connection info on details page. await fillSsoConnectorCreationModal(page, { connectorFactoryName, connectorName, protocol, + formData, + previewResults, }); // Error message should be shown.