mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(editor): Add OIDC paywall (#16347)
This commit is contained in:
committed by
GitHub
parent
ac032418cb
commit
1da3c70507
@@ -100,6 +100,7 @@
|
|||||||
"generic.rename": "Rename",
|
"generic.rename": "Rename",
|
||||||
"generic.missing.permissions": "Missing permissions to perform this action",
|
"generic.missing.permissions": "Missing permissions to perform this action",
|
||||||
"generic.shortcutHint": "Or press",
|
"generic.shortcutHint": "Or press",
|
||||||
|
"generic.upgradeToEnterprise": "Upgrade to Enterprise",
|
||||||
"about.aboutN8n": "About n8n",
|
"about.aboutN8n": "About n8n",
|
||||||
"about.close": "Close",
|
"about.close": "Close",
|
||||||
"about.license": "License",
|
"about.license": "License",
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import type { OidcConfigDto, SamlPreferences } from '@n8n/api-types';
|
import type { OidcConfigDto, SamlPreferences } from '@n8n/api-types';
|
||||||
import { createTestingPinia } from '@pinia/testing';
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
import { within, waitFor } from '@testing-library/vue';
|
import { within, waitFor } from '@testing-library/vue';
|
||||||
import { mockedStore, retry } from '@/__tests__/utils';
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
import { createPinia, setActivePinia } from 'pinia';
|
|
||||||
import SettingsSso from '@/views/SettingsSso.vue';
|
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 userEvent from '@testing-library/user-event';
|
||||||
import { useSSOStore } from '@/stores/sso.store';
|
import { useSSOStore } from '@/stores/sso.store';
|
||||||
import { createComponentRenderer } from '@/__tests__/render';
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
import { nextTick } from 'vue';
|
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso';
|
import type { SamlPreferencesExtractedData } from '@n8n/rest-api-client/api/sso';
|
||||||
|
|
||||||
@@ -64,366 +60,340 @@ describe('SettingsSso View', () => {
|
|||||||
showError.mockReset();
|
showError.mockReset();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show upgrade banner when enterprise SAML is disabled', async () => {
|
describe('SAML', () => {
|
||||||
const pinia = createTestingPinia();
|
it('should show upgrade banner when enterprise SAML is disabled', async () => {
|
||||||
const ssoStore = mockedStore(useSSOStore);
|
const pinia = createTestingPinia();
|
||||||
ssoStore.isEnterpriseSamlEnabled = false;
|
const ssoStore = mockedStore(useSSOStore);
|
||||||
ssoStore.isEnterpriseOidcEnabled = false;
|
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');
|
const actionBox = getByTestId('sso-content-unlicensed');
|
||||||
expect(actionBox).toBeInTheDocument();
|
expect(actionBox).toBeInTheDocument();
|
||||||
|
|
||||||
await userEvent.click(await within(actionBox).findByText('See plans'));
|
await userEvent.click(await within(actionBox).findByText('See plans'));
|
||||||
expect(pageRedirectionHelper.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso');
|
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);
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
it('allows user to toggle SSO', async () => {
|
it('should show user SSO config', async () => {
|
||||||
const pinia = createTestingPinia();
|
const pinia = createTestingPinia();
|
||||||
|
|
||||||
const ssoStore = mockedStore(useSSOStore);
|
const ssoStore = mockedStore(useSSOStore);
|
||||||
ssoStore.isEnterpriseSamlEnabled = true;
|
ssoStore.isEnterpriseSamlEnabled = true;
|
||||||
ssoStore.isSamlLoginEnabled = false;
|
ssoStore.isEnterpriseOidcEnabled = 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);
|
it('allows user to toggle SSO', async () => {
|
||||||
expect(toggle.textContent).toContain('Activated');
|
const pinia = createTestingPinia();
|
||||||
|
|
||||||
await userEvent.click(toggle);
|
const ssoStore = mockedStore(useSSOStore);
|
||||||
expect(toggle.textContent).toContain('Deactivated');
|
ssoStore.isEnterpriseSamlEnabled = true;
|
||||||
});
|
ssoStore.isSamlLoginEnabled = false;
|
||||||
|
ssoStore.isEnterpriseOidcEnabled = true;
|
||||||
|
|
||||||
it("allows user to fill Identity Provider's URL", async () => {
|
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
|
||||||
confirmMessage.mockResolvedValueOnce('confirm');
|
|
||||||
|
|
||||||
const pinia = createTestingPinia();
|
const { getByTestId } = renderView({ pinia });
|
||||||
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!);
|
|
||||||
|
|
||||||
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');
|
const toggle = getByTestId('sso-toggle');
|
||||||
|
|
||||||
|
expect(toggle.textContent).toContain('Deactivated');
|
||||||
|
|
||||||
|
await userEvent.click(toggle);
|
||||||
expect(toggle.textContent).toContain('Activated');
|
expect(toggle.textContent).toContain('Activated');
|
||||||
|
|
||||||
await userEvent.click(toggle);
|
await userEvent.click(toggle);
|
||||||
expect(toggle.textContent).toContain('Deactivated');
|
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 () => {
|
describe('OIDC', () => {
|
||||||
const pinia = createTestingPinia();
|
it('should show upgrade banner when enterprise OIDC is disabled', async () => {
|
||||||
|
const pinia = createTestingPinia();
|
||||||
|
const ssoStore = mockedStore(useSSOStore);
|
||||||
|
|
||||||
const ssoStore = mockedStore(useSSOStore);
|
ssoStore.isDefaultAuthenticationSaml = false;
|
||||||
ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig);
|
ssoStore.isEnterpriseSamlEnabled = false;
|
||||||
ssoStore.isEnterpriseOidcEnabled = true;
|
|
||||||
ssoStore.isEnterpriseSamlEnabled = false;
|
|
||||||
ssoStore.isOidcLoginEnabled = true;
|
|
||||||
ssoStore.isSamlLoginEnabled = false;
|
|
||||||
|
|
||||||
const { getByTestId, getByRole } = renderView({ pinia });
|
ssoStore.isDefaultAuthenticationOidc = true;
|
||||||
|
ssoStore.isEnterpriseOidcEnabled = false;
|
||||||
|
|
||||||
// Set authProtocol component ref to OIDC
|
const pageRedirectionHelper = usePageRedirectionHelper();
|
||||||
const protocolSelect = getByRole('combobox');
|
|
||||||
expect(protocolSelect).toBeInTheDocument();
|
|
||||||
await userEvent.click(protocolSelect);
|
|
||||||
|
|
||||||
const dropdown = await waitFor(() => getByRole('listbox'));
|
const { getByTestId } = renderView({ pinia });
|
||||||
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();
|
|
||||||
|
|
||||||
await userEvent.click(oidcItem!);
|
await waitFor(() => {
|
||||||
|
const actionBox = getByTestId('sso-content-unlicensed');
|
||||||
|
expect(actionBox).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
const saveButton = await waitFor(() => getByTestId('sso-oidc-save'));
|
await userEvent.click(
|
||||||
expect(saveButton).toBeVisible();
|
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();
|
const ssoStore = mockedStore(useSSOStore);
|
||||||
await userEvent.type(oidcDiscoveryUrlInput, oidcConfig.discoveryEndpoint);
|
ssoStore.saveOidcConfig.mockResolvedValue(oidcConfig);
|
||||||
|
ssoStore.isEnterpriseOidcEnabled = true;
|
||||||
|
ssoStore.isEnterpriseSamlEnabled = false;
|
||||||
|
ssoStore.isOidcLoginEnabled = true;
|
||||||
|
ssoStore.isSamlLoginEnabled = false;
|
||||||
|
|
||||||
const clientIdInput = getByTestId('oidc-client-id');
|
const { getByTestId, getByRole } = renderView({ pinia });
|
||||||
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();
|
// Set authProtocol component ref to OIDC
|
||||||
await userEvent.click(saveButton);
|
const protocolSelect = getByRole('combobox');
|
||||||
|
expect(protocolSelect).toBeInTheDocument();
|
||||||
|
await userEvent.click(protocolSelect);
|
||||||
|
|
||||||
expect(ssoStore.saveOidcConfig).toHaveBeenCalledWith(
|
const dropdown = await waitFor(() => getByRole('listbox'));
|
||||||
expect.objectContaining({
|
expect(dropdown).toBeInTheDocument();
|
||||||
discoveryEndpoint: oidcConfig.discoveryEndpoint,
|
const items = dropdown.querySelectorAll('.el-select-dropdown__item');
|
||||||
clientId: 'test-client-id',
|
const oidcItem = Array.from(items).find((item) => item.textContent?.includes('OIDC'));
|
||||||
clientSecret: 'test-client-secret',
|
expect(oidcItem).toBeDefined();
|
||||||
loginEnabled: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(telemetryTrack).toHaveBeenCalledWith(
|
await userEvent.click(oidcItem!);
|
||||||
expect.any(String),
|
|
||||||
expect.objectContaining({
|
const saveButton = await waitFor(() => getByTestId('sso-oidc-save'));
|
||||||
authentication_method: 'oidc',
|
expect(saveButton).toBeVisible();
|
||||||
discovery_endpoint: oidcConfig.discoveryEndpoint,
|
|
||||||
is_active: true,
|
const oidcDiscoveryUrlInput = getByTestId('oidc-discovery-endpoint');
|
||||||
}),
|
|
||||||
);
|
expect(oidcDiscoveryUrlInput).toBeVisible();
|
||||||
});
|
await userEvent.type(oidcDiscoveryUrlInput, oidcConfig.discoveryEndpoint);
|
||||||
});
|
|
||||||
|
const clientIdInput = getByTestId('oidc-client-id');
|
||||||
let pinia: ReturnType<typeof createPinia>;
|
expect(clientIdInput).toBeVisible();
|
||||||
let ssoStore: ReturnType<typeof useSSOStore>;
|
await userEvent.type(clientIdInput, 'test-client-id');
|
||||||
let settingsStore: ReturnType<typeof useSettingsStore>;
|
const clientSecretInput = getByTestId('oidc-client-secret');
|
||||||
let server: ReturnType<typeof setupServer>;
|
expect(clientSecretInput).toBeVisible();
|
||||||
|
await userEvent.type(clientSecretInput, 'test-client-secret');
|
||||||
const renderComponent = createComponentRenderer(SettingsSso);
|
|
||||||
|
expect(saveButton).not.toBeDisabled();
|
||||||
describe('SettingsSso', () => {
|
await userEvent.click(saveButton);
|
||||||
beforeAll(() => {
|
|
||||||
server = setupServer();
|
expect(ssoStore.saveOidcConfig).toHaveBeenCalledWith(
|
||||||
});
|
expect.objectContaining({
|
||||||
|
discoveryEndpoint: oidcConfig.discoveryEndpoint,
|
||||||
beforeEach(async () => {
|
clientId: 'test-client-id',
|
||||||
window.open = vi.fn();
|
clientSecret: 'test-client-secret',
|
||||||
|
loginEnabled: true,
|
||||||
pinia = createPinia();
|
}),
|
||||||
setActivePinia(pinia);
|
);
|
||||||
|
|
||||||
ssoStore = useSSOStore();
|
expect(telemetryTrack).toHaveBeenCalledWith(
|
||||||
settingsStore = useSettingsStore();
|
expect.any(String),
|
||||||
|
expect.objectContaining({
|
||||||
await settingsStore.getSettings();
|
authentication_method: 'oidc',
|
||||||
});
|
discovery_endpoint: oidcConfig.discoveryEndpoint,
|
||||||
|
is_active: true,
|
||||||
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(
|
|
||||||
'<?xml version="1.0"?>',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(getByRole('switch')).toBeEnabled();
|
|
||||||
expect(getByTestId('sso-test')).toBeEnabled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, onMounted } from 'vue';
|
|
||||||
import { useSSOStore } from '@/stores/sso.store';
|
|
||||||
import CopyInput from '@/components/CopyInput.vue';
|
import CopyInput from '@/components/CopyInput.vue';
|
||||||
import { useI18n } from '@n8n/i18n';
|
|
||||||
import { useMessage } from '@/composables/useMessage';
|
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
|
||||||
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
import { useDocumentTitle } from '@/composables/useDocumentTitle';
|
||||||
import { useRootStore } from '@n8n/stores/useRootStore';
|
import { useMessage } from '@/composables/useMessage';
|
||||||
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
import { usePageRedirectionHelper } from '@/composables/usePageRedirectionHelper';
|
||||||
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
|
import { useToast } from '@/composables/useToast';
|
||||||
import { MODAL_CONFIRM } from '@/constants';
|
import { MODAL_CONFIRM } from '@/constants';
|
||||||
|
import { useSSOStore } from '@/stores/sso.store';
|
||||||
|
import { useI18n } from '@n8n/i18n';
|
||||||
|
import { useRootStore } from '@n8n/stores/useRootStore';
|
||||||
|
import { computed, onMounted, ref } from 'vue';
|
||||||
|
|
||||||
type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof SupportedProtocols];
|
type SupportedProtocolType = (typeof SupportedProtocols)[keyof typeof SupportedProtocols];
|
||||||
|
|
||||||
@@ -44,6 +44,21 @@ const oidcActivatedLabel = computed(() =>
|
|||||||
: i18n.baseText('settings.sso.deactivated'),
|
: i18n.baseText('settings.sso.deactivated'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const options = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: SupportedProtocols.SAML.toUpperCase(),
|
||||||
|
value: SupportedProtocols.SAML,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: ssoStore.isEnterpriseOidcEnabled
|
||||||
|
? SupportedProtocols.OIDC.toUpperCase()
|
||||||
|
: `${SupportedProtocols.OIDC.toUpperCase()} (${i18n.baseText('generic.upgradeToEnterprise')})`,
|
||||||
|
value: SupportedProtocols.OIDC,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
const ssoSettingsSaved = ref(false);
|
const ssoSettingsSaved = ref(false);
|
||||||
|
|
||||||
const entityId = ref();
|
const entityId = ref();
|
||||||
@@ -300,33 +315,29 @@ async function onOidcSettingsSave() {
|
|||||||
{{ i18n.baseText('settings.sso.info.link') }}
|
{{ i18n.baseText('settings.sso.info.link') }}
|
||||||
</a>
|
</a>
|
||||||
</n8n-info-tip>
|
</n8n-info-tip>
|
||||||
<div
|
<div :class="$style.group">
|
||||||
v-if="ssoStore.isEnterpriseSamlEnabled || ssoStore.isEnterpriseOidcEnabled"
|
<label>Select Authentication Protocol</label>
|
||||||
data-test-id="sso-content-licensed"
|
<div>
|
||||||
>
|
<N8nSelect
|
||||||
<div :class="$style.group">
|
filterable
|
||||||
<label>Select Authentication Protocol</label>
|
:model-value="authProtocol"
|
||||||
<div>
|
:placeholder="i18n.baseText('parameterInput.select')"
|
||||||
<N8nSelect
|
@update:model-value="onAuthProtocolUpdated"
|
||||||
filterable
|
@keydown.stop
|
||||||
:model-value="authProtocol"
|
>
|
||||||
data-test-id="sso-auth-protocol-select"
|
<N8nOption
|
||||||
:placeholder="i18n.baseText('parameterInput.select')"
|
v-for="{ label, value } in options"
|
||||||
@update:model-value="onAuthProtocolUpdated"
|
:key="value"
|
||||||
@keydown.stop
|
:value="value"
|
||||||
|
:label="label"
|
||||||
|
data-test-id="credential-select-option"
|
||||||
>
|
>
|
||||||
<N8nOption
|
</N8nOption>
|
||||||
v-for="protocol in Object.values(SupportedProtocols)"
|
</N8nSelect>
|
||||||
:key="protocol"
|
|
||||||
:value="protocol"
|
|
||||||
:label="protocol.toUpperCase()"
|
|
||||||
data-test-id="credential-select-option"
|
|
||||||
>
|
|
||||||
</N8nOption>
|
|
||||||
</N8nSelect>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="authProtocol === SupportedProtocols.SAML">
|
</div>
|
||||||
|
<div v-if="authProtocol === SupportedProtocols.SAML">
|
||||||
|
<div v-if="ssoStore.isEnterpriseSamlEnabled" data-test-id="sso-content-licensed">
|
||||||
<div :class="$style.group">
|
<div :class="$style.group">
|
||||||
<label>{{ i18n.baseText('settings.sso.settings.redirectUrl.label') }}</label>
|
<label>{{ i18n.baseText('settings.sso.settings.redirectUrl.label') }}</label>
|
||||||
<CopyInput
|
<CopyInput
|
||||||
@@ -350,7 +361,7 @@ async function onOidcSettingsSave() {
|
|||||||
<div class="mt-2xs mb-s">
|
<div class="mt-2xs mb-s">
|
||||||
<n8n-radio-buttons v-model="ipsType" :options="ipsOptions" />
|
<n8n-radio-buttons v-model="ipsType" :options="ipsOptions" />
|
||||||
</div>
|
</div>
|
||||||
<div v-show="ipsType === IdentityProviderSettingsType.URL">
|
<div v-if="ipsType === IdentityProviderSettingsType.URL">
|
||||||
<n8n-input
|
<n8n-input
|
||||||
v-model="metadataUrl"
|
v-model="metadataUrl"
|
||||||
type="text"
|
type="text"
|
||||||
@@ -361,7 +372,7 @@ async function onOidcSettingsSave() {
|
|||||||
/>
|
/>
|
||||||
<small>{{ i18n.baseText('settings.sso.settings.ips.url.help') }}</small>
|
<small>{{ i18n.baseText('settings.sso.settings.ips.url.help') }}</small>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="ipsType === IdentityProviderSettingsType.XML">
|
<div v-if="ipsType === IdentityProviderSettingsType.XML">
|
||||||
<n8n-input
|
<n8n-input
|
||||||
v-model="metadata"
|
v-model="metadata"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
@@ -415,7 +426,21 @@ async function onOidcSettingsSave() {
|
|||||||
{{ i18n.baseText('settings.sso.settings.footer.hint') }}
|
{{ i18n.baseText('settings.sso.settings.footer.hint') }}
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="authProtocol === SupportedProtocols.OIDC">
|
<n8n-action-box
|
||||||
|
v-else
|
||||||
|
data-test-id="sso-content-unlicensed"
|
||||||
|
:class="$style.actionBox"
|
||||||
|
:description="i18n.baseText('settings.sso.actionBox.description')"
|
||||||
|
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
|
||||||
|
@click:button="goToUpgrade"
|
||||||
|
>
|
||||||
|
<template #heading>
|
||||||
|
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
|
||||||
|
</template>
|
||||||
|
</n8n-action-box>
|
||||||
|
</div>
|
||||||
|
<div v-if="authProtocol === SupportedProtocols.OIDC">
|
||||||
|
<div v-if="ssoStore.isEnterpriseOidcEnabled">
|
||||||
<div :class="$style.group">
|
<div :class="$style.group">
|
||||||
<label>Redirect URL</label>
|
<label>Redirect URL</label>
|
||||||
<CopyInput
|
<CopyInput
|
||||||
@@ -481,19 +506,18 @@ async function onOidcSettingsSave() {
|
|||||||
</n8n-button>
|
</n8n-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<n8n-action-box
|
||||||
|
v-else
|
||||||
|
data-test-id="sso-content-unlicensed"
|
||||||
|
:class="$style.actionBox"
|
||||||
|
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
|
||||||
|
@click:button="goToUpgrade"
|
||||||
|
>
|
||||||
|
<template #heading>
|
||||||
|
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
|
||||||
|
</template>
|
||||||
|
</n8n-action-box>
|
||||||
</div>
|
</div>
|
||||||
<n8n-action-box
|
|
||||||
v-else
|
|
||||||
data-test-id="sso-content-unlicensed"
|
|
||||||
:class="$style.actionBox"
|
|
||||||
:description="i18n.baseText('settings.sso.actionBox.description')"
|
|
||||||
:button-text="i18n.baseText('settings.sso.actionBox.buttonText')"
|
|
||||||
@click:button="goToUpgrade"
|
|
||||||
>
|
|
||||||
<template #heading>
|
|
||||||
<span>{{ i18n.baseText('settings.sso.actionBox.title') }}</span>
|
|
||||||
</template>
|
|
||||||
</n8n-action-box>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user