fix(editor): Allow disabling SSO when config request fails (#10635)

This commit is contained in:
Raúl Gómez Morales 2024-09-03 10:06:16 +02:00 committed by GitHub
parent 7fd0c71bdc
commit ce39933766
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 222 additions and 2 deletions

View file

@ -0,0 +1,203 @@
import { createTestingPinia } from '@pinia/testing';
import { createComponentRenderer } from '@/__tests__/render';
import SettingsSso from './SettingsSso.vue';
import { useSSOStore } from '@/stores/sso.store';
import { useUIStore } from '@/stores/ui.store';
import { within, waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { mockedStore } from '@/__tests__/utils';
const renderView = createComponentRenderer(SettingsSso);
const samlConfig = {
metadata: 'metadata dummy',
metadataUrl:
'https://dev-qqkrykgkoo0p63d5.eu.auth0.com/samlp/metadata/KR1cSrRrxaZT2gV8ZhPAUIUHtEY4duhN',
entityID: 'https://n8n-tunnel.myhost.com/rest/sso/saml/metadata',
returnUrl: 'https://n8n-tunnel.myhost.com/rest/sso/saml/acs',
};
const telemetryTrack = vi.fn();
vi.mock('@/composables/useTelemetry', () => ({
useTelemetry: () => ({
track: telemetryTrack,
}),
}));
const showError = vi.fn();
vi.mock('@/composables/useToast', () => ({
useToast: () => ({
showError,
}),
}));
const confirmMessage = vi.fn();
vi.mock('@/composables/useMessage', () => ({
useMessage: () => ({
confirm: confirmMessage,
}),
}));
describe('SettingsSso View', () => {
beforeEach(() => {
telemetryTrack.mockReset();
confirmMessage.mockReset();
showError.mockReset();
});
it('should show upgrade banner when enterprise SAML is disabled', async () => {
const pinia = createTestingPinia();
const ssoStore = mockedStore(useSSOStore);
ssoStore.isEnterpriseSamlEnabled = false;
const uiStore = useUIStore();
const { getByTestId } = renderView({ pinia });
const actionBox = getByTestId('sso-content-unlicensed');
expect(actionBox).toBeInTheDocument();
await userEvent.click(await within(actionBox).findByText('See plans'));
expect(uiStore.goToUpgrade).toHaveBeenCalledWith('sso', 'upgrade-sso');
});
it('should show user SSO config', async () => {
const pinia = createTestingPinia();
const ssoStore = mockedStore(useSSOStore);
ssoStore.isEnterpriseSamlEnabled = 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 () => {
const pinia = createTestingPinia();
const ssoStore = mockedStore(useSSOStore);
ssoStore.isEnterpriseSamlEnabled = true;
ssoStore.isSamlLoginEnabled = false;
ssoStore.getSamlConfig.mockResolvedValue(samlConfig);
const { getByTestId } = renderView({ pinia });
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;
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' }),
);
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;
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('PAY-1812: allows user to disable SSO even if config request failed', async () => {
const pinia = createTestingPinia();
const ssoStore = mockedStore(useSSOStore);
ssoStore.isEnterpriseSamlEnabled = true;
ssoStore.isSamlLoginEnabled = true;
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');
});
});
});

View file

@ -134,6 +134,15 @@ const goToUpgrade = () => {
void uiStore.goToUpgrade('sso', 'upgrade-sso'); void uiStore.goToUpgrade('sso', 'upgrade-sso');
}; };
const isToggleSsoDisabled = computed(() => {
/** Allow users to disable SSO even if config request fails */
if (ssoStore.isSamlLoginEnabled) {
return false;
}
return !ssoSettingsSaved.value;
});
onMounted(async () => { onMounted(async () => {
if (!ssoStore.isEnterpriseSamlEnabled) { if (!ssoStore.isEnterpriseSamlEnabled) {
return; return;
@ -162,7 +171,8 @@ onMounted(async () => {
</template> </template>
<el-switch <el-switch
v-model="ssoStore.isSamlLoginEnabled" v-model="ssoStore.isSamlLoginEnabled"
:disabled="!ssoSettingsSaved" data-test-id="sso-toggle"
:disabled="isToggleSsoDisabled"
:class="$style.switch" :class="$style.switch"
:inactive-text="ssoActivatedLabel" :inactive-text="ssoActivatedLabel"
/> />
@ -205,11 +215,18 @@ onMounted(async () => {
name="metadataUrl" name="metadataUrl"
size="large" size="large"
:placeholder="i18n.baseText('settings.sso.settings.ips.url.placeholder')" :placeholder="i18n.baseText('settings.sso.settings.ips.url.placeholder')"
data-test-id="sso-provider-url"
/> />
<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-show="ipsType === IdentityProviderSettingsType.XML">
<n8n-input v-model="metadata" type="textarea" name="metadata" :rows="4" /> <n8n-input
v-model="metadata"
type="textarea"
name="metadata"
:rows="4"
data-test-id="sso-provider-xml"
/>
<small>{{ i18n.baseText('settings.sso.settings.ips.xml.help') }}</small> <small>{{ i18n.baseText('settings.sso.settings.ips.xml.help') }}</small>
</div> </div>
</div> </div>