From 1da3c70507f86fc59a9c43eb43fd89c3628b7ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Tue, 17 Jun 2025 15:21:34 +0200 Subject: [PATCH] feat(editor): Add OIDC paywall (#16347) --- .../frontend/@n8n/i18n/src/locales/en.json | 1 + .../editor-ui/src/views/SettingsSso.test.ts | 632 +++++++++--------- .../editor-ui/src/views/SettingsSso.vue | 118 ++-- 3 files changed, 373 insertions(+), 378 deletions(-) diff --git a/packages/frontend/@n8n/i18n/src/locales/en.json b/packages/frontend/@n8n/i18n/src/locales/en.json index 18a556c5e3..7f5ce2f4e1 100644 --- a/packages/frontend/@n8n/i18n/src/locales/en.json +++ b/packages/frontend/@n8n/i18n/src/locales/en.json @@ -100,6 +100,7 @@ "generic.rename": "Rename", "generic.missing.permissions": "Missing permissions to perform this action", "generic.shortcutHint": "Or press", + "generic.upgradeToEnterprise": "Upgrade to Enterprise", "about.aboutN8n": "About n8n", "about.close": "Close", "about.license": "License", diff --git a/packages/frontend/editor-ui/src/views/SettingsSso.test.ts b/packages/frontend/editor-ui/src/views/SettingsSso.test.ts index c06477b07f..ad3cbbba22 100644 --- a/packages/frontend/editor-ui/src/views/SettingsSso.test.ts +++ b/packages/frontend/editor-ui/src/views/SettingsSso.test.ts @@ -1,15 +1,11 @@ import type { OidcConfigDto, SamlPreferences } from '@n8n/api-types'; import { createTestingPinia } from '@pinia/testing'; import { within, waitFor } from '@testing-library/vue'; -import { mockedStore, retry } from '@/__tests__/utils'; -import { createPinia, setActivePinia } from 'pinia'; +import { mockedStore } from '@/__tests__/utils'; import SettingsSso from '@/views/SettingsSso.vue'; -import { setupServer } from '@/__tests__/server'; -import { useSettingsStore } from '@/stores/settings.store'; import userEvent from '@testing-library/user-event'; import { useSSOStore } from '@/stores/sso.store'; import { createComponentRenderer } from '@/__tests__/render'; -import { nextTick } from 'vue'; import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper'; import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso'; @@ -64,366 +60,340 @@ describe('SettingsSso View', () => { showError.mockReset(); }); - it('should show upgrade banner when enterprise SAML is disabled', async () => { - const pinia = createTestingPinia(); - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = false; - ssoStore.isEnterpriseOidcEnabled = false; + describe('SAML', () => { + it('should show upgrade banner when enterprise SAML is disabled', async () => { + const pinia = createTestingPinia(); + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = false; + ssoStore.isEnterpriseOidcEnabled = false; - const pageRedirectionHelper = usePageRedirectionHelper(); + const pageRedirectionHelper = usePageRedirectionHelper(); - const { getByTestId } = renderView({ pinia }); + const { getByTestId } = renderView({ pinia }); - const actionBox = getByTestId('sso-content-unlicensed'); - expect(actionBox).toBeInTheDocument(); + const actionBox = getByTestId('sso-content-unlicensed'); + expect(actionBox).toBeInTheDocument(); - await userEvent.click(await within(actionBox).findByText('See plans')); - expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso'); - }); - - it('should show user SSO config', async () => { - const pinia = createTestingPinia(); - - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - - ssoStore.getSamlConfig.mockResolvedValue(samlConfig); - - const { getAllByTestId } = renderView({ pinia }); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); - - await waitFor(async () => { - const copyInputs = getAllByTestId('copy-input'); - expect(copyInputs[0].textContent).toContain(samlConfig.returnUrl); - expect(copyInputs[1].textContent).toContain(samlConfig.entityID); + await userEvent.click(await within(actionBox).findByText('See plans')); + expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso'); }); - }); - it('allows user to toggle SSO', async () => { - const pinia = createTestingPinia(); + it('should show user SSO config', async () => { + const pinia = createTestingPinia(); - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isSamlLoginEnabled = false; - ssoStore.isEnterpriseOidcEnabled = true; + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; - ssoStore.getSamlConfig.mockResolvedValue(samlConfig); + ssoStore.getSamlConfig.mockResolvedValue(samlConfig); - const { getByTestId } = renderView({ pinia }); + const { getAllByTestId } = renderView({ pinia }); - const toggle = getByTestId('sso-toggle'); + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); - expect(toggle.textContent).toContain('Deactivated'); + await waitFor(async () => { + const copyInputs = getAllByTestId('copy-input'); + expect(copyInputs[0].textContent).toContain(samlConfig.returnUrl); + expect(copyInputs[1].textContent).toContain(samlConfig.entityID); + }); + }); - await userEvent.click(toggle); - expect(toggle.textContent).toContain('Activated'); + it('allows user to toggle SSO', async () => { + const pinia = createTestingPinia(); - await userEvent.click(toggle); - expect(toggle.textContent).toContain('Deactivated'); - }); + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isSamlLoginEnabled = false; + ssoStore.isEnterpriseOidcEnabled = true; - it("allows user to fill Identity Provider's URL", async () => { - confirmMessage.mockResolvedValueOnce('confirm'); + ssoStore.getSamlConfig.mockResolvedValue(samlConfig); - const pinia = createTestingPinia(); - const windowOpenSpy = vi.spyOn(window, 'open'); + const { getByTestId } = renderView({ pinia }); - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - - const { getByTestId } = renderView({ pinia }); - - const saveButton = getByTestId('sso-save'); - expect(saveButton).toBeDisabled(); - - const urlinput = getByTestId('sso-provider-url'); - - expect(urlinput).toBeVisible(); - await userEvent.type(urlinput, samlConfig.metadataUrl!); - - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); - - expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( - expect.objectContaining({ metadataUrl: samlConfig.metadataUrl }), - ); - - expect(ssoStore.testSamlConfig).toHaveBeenCalled(); - expect(windowOpenSpy).toHaveBeenCalled(); - - expect(telemetryTrack).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ identity_provider: 'metadata', authentication_method: 'saml' }), - ); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); - }); - - it("allows user to fill Identity Provider's XML", async () => { - confirmMessage.mockResolvedValueOnce('confirm'); - - const pinia = createTestingPinia(); - const windowOpenSpy = vi.spyOn(window, 'open'); - - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - - const { getByTestId } = renderView({ pinia }); - - const saveButton = getByTestId('sso-save'); - expect(saveButton).toBeDisabled(); - - await userEvent.click(getByTestId('radio-button-xml')); - - const xmlInput = getByTestId('sso-provider-xml'); - - expect(xmlInput).toBeVisible(); - await userEvent.type(xmlInput, samlConfig.metadata!); - - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); - - expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( - expect.objectContaining({ metadata: samlConfig.metadata }), - ); - - expect(ssoStore.testSamlConfig).toHaveBeenCalled(); - expect(windowOpenSpy).toHaveBeenCalled(); - - expect(telemetryTrack).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ identity_provider: 'xml' }), - ); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); - }); - - it('should validate the url before setting the saml config', async () => { - const pinia = createTestingPinia(); - - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - - const { getByTestId } = renderView({ pinia }); - - const saveButton = getByTestId('sso-save'); - expect(saveButton).toBeDisabled(); - - const urlinput = getByTestId('sso-provider-url'); - - expect(urlinput).toBeVisible(); - await userEvent.type(urlinput, samlConfig.metadata!); - - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); - - expect(showError).toHaveBeenCalled(); - expect(ssoStore.saveSamlConfig).not.toHaveBeenCalled(); - - expect(ssoStore.testSamlConfig).not.toHaveBeenCalled(); - - expect(telemetryTrack).not.toHaveBeenCalled(); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); - }); - - it('should ensure the url does not support invalid protocols like mailto', async () => { - const pinia = createTestingPinia(); - - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - - const { getByTestId } = renderView({ pinia }); - - const saveButton = getByTestId('sso-save'); - expect(saveButton).toBeDisabled(); - - const urlinput = getByTestId('sso-provider-url'); - - expect(urlinput).toBeVisible(); - await userEvent.type(urlinput, 'mailto://test@example.com'); - - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); - - expect(showError).toHaveBeenCalled(); - expect(ssoStore.saveSamlConfig).not.toHaveBeenCalled(); - - expect(ssoStore.testSamlConfig).not.toHaveBeenCalled(); - - expect(telemetryTrack).not.toHaveBeenCalled(); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); - }); - - it('allows user to disable SSO even if config request failed', async () => { - const pinia = createTestingPinia(); - - const ssoStore = mockedStore(useSSOStore); - ssoStore.isEnterpriseSamlEnabled = true; - ssoStore.isSamlLoginEnabled = true; - ssoStore.isEnterpriseOidcEnabled = true; - ssoStore.isOidcLoginEnabled = false; - - const error = new Error('Request failed with status code 404'); - ssoStore.getSamlConfig.mockRejectedValue(error); - - const { getByTestId } = renderView({ pinia }); - - expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); - - await waitFor(async () => { - expect(showError).toHaveBeenCalledWith(error, 'error'); const toggle = getByTestId('sso-toggle'); + + expect(toggle.textContent).toContain('Deactivated'); + + await userEvent.click(toggle); expect(toggle.textContent).toContain('Activated'); + await userEvent.click(toggle); expect(toggle.textContent).toContain('Deactivated'); }); + + it("allows user to fill Identity Provider's URL", async () => { + confirmMessage.mockResolvedValueOnce('confirm'); + + const pinia = createTestingPinia(); + const windowOpenSpy = vi.spyOn(window, 'open'); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + const urlInput = getByTestId('sso-provider-url'); + + expect(urlInput).toBeVisible(); + await userEvent.type(urlInput, samlConfig.metadataUrl as string); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( + expect.objectContaining({ metadataUrl: samlConfig.metadataUrl }), + ); + + expect(ssoStore.testSamlConfig).toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalled(); + + expect(telemetryTrack).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ identity_provider: 'metadata' }), + ); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it("allows user to fill Identity Provider's XML", async () => { + confirmMessage.mockResolvedValueOnce('confirm'); + + const pinia = createTestingPinia(); + const windowOpenSpy = vi.spyOn(window, 'open'); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + await userEvent.click(getByTestId('radio-button-xml')); + + const xmlInput = getByTestId('sso-provider-xml'); + + expect(xmlInput).toBeVisible(); + await userEvent.type(xmlInput, samlConfig.metadata!); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(ssoStore.saveSamlConfig).toHaveBeenCalledWith( + expect.objectContaining({ metadata: samlConfig.metadata }), + ); + + expect(ssoStore.testSamlConfig).toHaveBeenCalled(); + expect(windowOpenSpy).toHaveBeenCalled(); + + expect(telemetryTrack).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ identity_provider: 'xml' }), + ); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it('should validate the url before setting the saml config', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + const urlInput = getByTestId('sso-provider-url'); + + expect(urlInput).toBeVisible(); + await userEvent.type(urlInput, samlConfig.metadata as string); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(showError).toHaveBeenCalled(); + expect(ssoStore.saveSamlConfig).not.toHaveBeenCalled(); + + expect(ssoStore.testSamlConfig).not.toHaveBeenCalled(); + + expect(telemetryTrack).not.toHaveBeenCalled(); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it('should ensure the url does not support invalid protocols like mailto', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; + + const { getByTestId } = renderView({ pinia }); + + const saveButton = getByTestId('sso-save'); + expect(saveButton).toBeDisabled(); + + const urlinput = getByTestId('sso-provider-url'); + + expect(urlinput).toBeVisible(); + await userEvent.type(urlinput, 'mailto://test@example.com'); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(showError).toHaveBeenCalled(); + expect(ssoStore.saveSamlConfig).not.toHaveBeenCalled(); + + expect(ssoStore.testSamlConfig).not.toHaveBeenCalled(); + + expect(telemetryTrack).not.toHaveBeenCalled(); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(2); + }); + + it('allows user to disable SSO even if config request failed', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isSamlLoginEnabled = true; + ssoStore.isEnterpriseOidcEnabled = true; + ssoStore.isOidcLoginEnabled = false; + + const error = new Error('Request failed with status code 404'); + ssoStore.getSamlConfig.mockRejectedValue(error); + + const { getByTestId } = renderView({ pinia }); + + expect(ssoStore.getSamlConfig).toHaveBeenCalledTimes(1); + + await waitFor(async () => { + expect(showError).toHaveBeenCalledWith(error, 'error'); + const toggle = getByTestId('sso-toggle'); + expect(toggle.textContent).toContain('Activated'); + await userEvent.click(toggle); + expect(toggle.textContent).toContain('Deactivated'); + }); + }); + it('should enable activation checkbox and test button if data is already saved', async () => { + const pinia = createTestingPinia(); + + const ssoStore = mockedStore(useSSOStore); + ssoStore.isEnterpriseSamlEnabled = true; + ssoStore.isSamlLoginEnabled = true; + + ssoStore.isDefaultAuthenticationOidc = false; + ssoStore.isEnterpriseOidcEnabled = false; + + ssoStore.getSamlConfig.mockResolvedValue(samlConfig); + + const { container, getByTestId, getByRole } = renderView({ + pinia, + }); + + await userEvent.click(getByTestId('radio-button-xml')); + + expect(container.querySelector('textarea[name="metadata"]')).toHaveValue(samlConfig.metadata); + + expect(getByRole('switch')).toBeEnabled(); + expect(getByTestId('sso-test')).toBeEnabled(); + }); }); - it('allows user to save OIDC config', async () => { - const pinia = createTestingPinia(); + describe('OIDC', () => { + it('should show upgrade banner when enterprise OIDC is disabled', async () => { + const pinia = createTestingPinia(); + const ssoStore = mockedStore(useSSOStore); - const ssoStore = mockedStore(useSSOStore); - ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig); - ssoStore.isEnterpriseOidcEnabled = true; - ssoStore.isEnterpriseSamlEnabled = false; - ssoStore.isOidcLoginEnabled = true; - ssoStore.isSamlLoginEnabled = false; + ssoStore.isDefaultAuthenticationSaml = false; + ssoStore.isEnterpriseSamlEnabled = false; - const { getByTestId, getByRole } = renderView({ pinia }); + ssoStore.isDefaultAuthenticationOidc = true; + ssoStore.isEnterpriseOidcEnabled = false; - // Set authProtocol component ref to OIDC - const protocolSelect = getByRole('combobox'); - expect(protocolSelect).toBeInTheDocument(); - await userEvent.click(protocolSelect); + const pageRedirectionHelper = usePageRedirectionHelper(); - const dropdown = await waitFor(() => getByRole('listbox')); - expect(dropdown).toBeInTheDocument(); - const items = dropdown.querySelectorAll('.el-select-dropdown__item'); - const oidcItem = Array.from(items).find((item) => item.textContent?.includes('OIDC')); - expect(oidcItem).toBeDefined(); + const { getByTestId } = renderView({ pinia }); - await userEvent.click(oidcItem!); + await waitFor(() => { + const actionBox = getByTestId('sso-content-unlicensed'); + expect(actionBox).toBeInTheDocument(); + }); - const saveButton = await waitFor(() => getByTestId('sso-oidc-save')); - expect(saveButton).toBeVisible(); + await userEvent.click( + await within(getByTestId('sso-content-unlicensed')).findByText('See plans'), + ); + expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso'); + }); - const oidcDiscoveryUrlInput = getByTestId('oidc-discovery-endpoint'); + it('allows user to save OIDC config', async () => { + const pinia = createTestingPinia(); - expect(oidcDiscoveryUrlInput).toBeVisible(); - await userEvent.type(oidcDiscoveryUrlInput, oidcConfig.discoveryEndpoint); + const ssoStore = mockedStore(useSSOStore); + ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig); + ssoStore.isEnterpriseOidcEnabled = true; + ssoStore.isEnterpriseSamlEnabled = false; + ssoStore.isOidcLoginEnabled = true; + ssoStore.isSamlLoginEnabled = false; - const clientIdInput = getByTestId('oidc-client-id'); - expect(clientIdInput).toBeVisible(); - await userEvent.type(clientIdInput, 'test-client-id'); - const clientSecretInput = getByTestId('oidc-client-secret'); - expect(clientSecretInput).toBeVisible(); - await userEvent.type(clientSecretInput, 'test-client-secret'); + const { getByTestId, getByRole } = renderView({ pinia }); - expect(saveButton).not.toBeDisabled(); - await userEvent.click(saveButton); + // Set authProtocol component ref to OIDC + const protocolSelect = getByRole('combobox'); + expect(protocolSelect).toBeInTheDocument(); + await userEvent.click(protocolSelect); - expect(ssoStore.saveOidcConfig).toHaveBeenCalledWith( - expect.objectContaining({ - discoveryEndpoint: oidcConfig.discoveryEndpoint, - clientId: 'test-client-id', - clientSecret: 'test-client-secret', - loginEnabled: true, - }), - ); + const dropdown = await waitFor(() => getByRole('listbox')); + expect(dropdown).toBeInTheDocument(); + const items = dropdown.querySelectorAll('.el-select-dropdown__item'); + const oidcItem = Array.from(items).find((item) => item.textContent?.includes('OIDC')); + expect(oidcItem).toBeDefined(); - expect(telemetryTrack).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - authentication_method: 'oidc', - discovery_endpoint: oidcConfig.discoveryEndpoint, - is_active: true, - }), - ); - }); -}); - -let pinia: ReturnType; -let ssoStore: ReturnType; -let settingsStore: ReturnType; -let server: ReturnType; - -const renderComponent = createComponentRenderer(SettingsSso); - -describe('SettingsSso', () => { - beforeAll(() => { - server = setupServer(); - }); - - beforeEach(async () => { - window.open = vi.fn(); - - pinia = createPinia(); - setActivePinia(pinia); - - ssoStore = useSSOStore(); - settingsStore = useSettingsStore(); - - await settingsStore.getSettings(); - }); - - afterEach(() => { - vi.clearAllMocks(); - }); - - afterAll(() => { - server.shutdown(); - }); - - it('should render paywall state when there is no license', () => { - const { getByTestId, queryByTestId, queryByRole } = renderComponent({ - pinia, - }); - - expect(queryByRole('checkbox')).not.toBeInTheDocument(); - expect(queryByTestId('sso-content-licensed')).not.toBeInTheDocument(); - expect(getByTestId('sso-content-unlicensed')).toBeInTheDocument(); - }); - - it('should render licensed content', async () => { - ssoStore.isEnterpriseSamlEnabled = true; - await nextTick(); - - const { getByTestId, queryByTestId, getByRole } = renderComponent({ - pinia, - }); - - expect(getByRole('switch')).toBeInTheDocument(); - expect(getByTestId('sso-content-licensed')).toBeInTheDocument(); - expect(queryByTestId('sso-content-unlicensed')).not.toBeInTheDocument(); - }); - - it('should enable activation checkbox and test button if data is already saved', async () => { - await ssoStore.getSamlConfig(); - ssoStore.isEnterpriseSamlEnabled = true; - await nextTick(); - - const { container, getByTestId, getByRole } = renderComponent({ - pinia, - }); - - const xmlRadioButton = getByTestId('radio-button-xml'); - await userEvent.click(xmlRadioButton); - - await retry(() => - expect(container.querySelector('textarea[name="metadata"]')).toHaveValue( - '', - ), - ); - - expect(getByRole('switch')).toBeEnabled(); - expect(getByTestId('sso-test')).toBeEnabled(); + await userEvent.click(oidcItem!); + + const saveButton = await waitFor(() => getByTestId('sso-oidc-save')); + expect(saveButton).toBeVisible(); + + const oidcDiscoveryUrlInput = getByTestId('oidc-discovery-endpoint'); + + expect(oidcDiscoveryUrlInput).toBeVisible(); + await userEvent.type(oidcDiscoveryUrlInput, oidcConfig.discoveryEndpoint); + + const clientIdInput = getByTestId('oidc-client-id'); + expect(clientIdInput).toBeVisible(); + await userEvent.type(clientIdInput, 'test-client-id'); + const clientSecretInput = getByTestId('oidc-client-secret'); + expect(clientSecretInput).toBeVisible(); + await userEvent.type(clientSecretInput, 'test-client-secret'); + + expect(saveButton).not.toBeDisabled(); + await userEvent.click(saveButton); + + expect(ssoStore.saveOidcConfig).toHaveBeenCalledWith( + expect.objectContaining({ + discoveryEndpoint: oidcConfig.discoveryEndpoint, + clientId: 'test-client-id', + clientSecret: 'test-client-secret', + loginEnabled: true, + }), + ); + + expect(telemetryTrack).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ + authentication_method: 'oidc', + discovery_endpoint: oidcConfig.discoveryEndpoint, + is_active: true, + }), + ); + }); }); }); diff --git a/packages/frontend/editor-ui/src/views/SettingsSso.vue b/packages/frontend/editor-ui/src/views/SettingsSso.vue index af28b9f03d..9fd1acadf0 100644 --- a/packages/frontend/editor-ui/src/views/SettingsSso.vue +++ b/packages/frontend/editor-ui/src/views/SettingsSso.vue @@ -1,15 +1,15 @@