test(editor): SSO tests (#5946)

* test(editor): SSO tests

* test(editor): move store tests to __tests__ folder

* test(editor): move tests in a different PR

* test(editor): add SSO tests

* test(editor): add SSO settings page tests

* test(editor): add SSO onboarding page base test

* test(editor): add SSO onboarding page test

* test(editor): fix router spy
This commit is contained in:
Csaba Tuncsik 2023-04-13 16:17:47 +02:00 committed by GitHub
parent 1a8a9f8ddb
commit bc1db5e16a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 475 additions and 79 deletions

View file

@ -14,8 +14,7 @@ import { showMessage } from '@/mixins/showMessage';
import { i18nInstance } from '@/plugins/i18n';
import type { IWorkflowShortResponse } from '@/Interface';
import type { IExecutionsSummary } from 'n8n-workflow';
const waitAllPromises = () => new Promise((resolve) => setTimeout(resolve));
import { waitAllPromises } from '@/utils/testUtils';
const workflowDataFactory = (): IWorkflowShortResponse => ({
createdAt: faker.date.past().toDateString(),

View file

@ -0,0 +1,59 @@
import { PiniaVuePlugin } from 'pinia';
import { render } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
import SSOLogin from '@/components/SSOLogin.vue';
import { STORES } from '@/constants';
import { useSSOStore } from '@/stores/sso';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/utils/testUtils';
import { afterEach } from 'vitest';
let pinia: ReturnType<typeof createTestingPinia>;
let ssoStore: ReturnType<typeof useSSOStore>;
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
SSOLogin,
merge(
{
pinia,
stubs: {
'n8n-button': {
template: '<button data-testid="sso-button"></button>',
},
},
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SSOLogin', () => {
beforeEach(() => {
pinia = createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
},
});
ssoStore = useSSOStore();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should not render button if conditions are not met', () => {
const { queryByRole } = renderComponent();
expect(queryByRole('button')).not.toBeInTheDocument();
});
it('should render button if the store returns true for the conditions', () => {
vi.spyOn(ssoStore, 'showSsoLoginButton', 'get').mockReturnValue(true);
const { queryByRole } = renderComponent();
expect(queryByRole('button')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,48 @@
import { createPinia, setActivePinia } from 'pinia';
import { useSettingsStore } from '@/stores/settings';
import { useSSOStore } from '@/stores/sso';
import { merge } from 'lodash-es';
import { IN8nUISettings } from '@/Interface';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/utils/testUtils';
let ssoStore: ReturnType<typeof useSSOStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings;
describe('SSO store', () => {
beforeEach(() => {
setActivePinia(createPinia());
ssoStore = useSSOStore();
settingsStore = useSettingsStore();
});
test.each([
['saml', true, true, true],
['saml', false, true, false],
['saml', false, false, false],
['saml', true, false, false],
['email', true, true, false],
])(
'should check SSO login button availability when authenticationMethod is %s and enterprise feature is %s and sso login is set to %s',
(authenticationMethod, saml, loginEnabled, expectation) => {
settingsStore.setSettings(
merge({}, DEFAULT_SETTINGS, {
userManagement: {
authenticationMethod,
},
enterprise: {
saml,
},
sso: {
saml: {
loginEnabled,
},
},
}),
);
expect(ssoStore.showSsoLoginButton).toBe(expectation);
},
);
});

View file

@ -75,6 +75,7 @@ export const useSSOStore = defineStore('sso', () => {
setLoading,
isSamlLoginEnabled,
isEnterpriseSamlEnabled,
isDefaultAuthenticationSaml,
showSsoLoginButton,
getSSORedirectUrl,
getSamlMetadata,

View file

@ -1,76 +1,15 @@
import { beforeAll } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { merge } from 'lodash-es';
import { isAuthorized } from '@/utils';
import { useSettingsStore } from '@/stores/settings';
import { useSSOStore } from '@/stores/sso';
import { IN8nUISettings, IUser, UserManagementAuthenticationMethod } from '@/Interface';
import { IN8nUISettings, IUser } from '@/Interface';
import { routes } from '@/router';
import { VIEWS } from '@/constants';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/utils/testUtils';
const DEFAULT_SETTINGS: IN8nUISettings = {
allowedModules: {},
communityNodesEnabled: false,
defaultLocale: '',
endpointWebhook: '',
endpointWebhookTest: '',
enterprise: {
advancedExecutionFilters: false,
sharing: false,
ldap: false,
saml: false,
logStreaming: false,
},
executionMode: '',
executionTimeout: 0,
hideUsagePage: false,
hiringBannerEnabled: false,
instanceId: '',
isNpmAvailable: false,
license: { environment: 'production' },
logLevel: 'info',
maxExecutionTimeout: 0,
oauthCallbackUrls: { oauth1: '', oauth2: '' },
onboardingCallPromptEnabled: false,
personalizationSurveyEnabled: false,
posthog: {
apiHost: '',
apiKey: '',
autocapture: false,
debug: false,
disableSessionRecording: false,
enabled: false,
},
publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } },
pushBackend: 'sse',
saveDataErrorExecution: '',
saveDataSuccessExecution: '',
saveManualExecutions: false,
sso: {
ldap: { loginEnabled: false, loginLabel: '' },
saml: { loginEnabled: false, loginLabel: '' },
},
telemetry: { enabled: false },
templates: { enabled: false, host: '' },
timezone: '',
urlBaseEditor: '',
urlBaseWebhook: '',
userManagement: {
enabled: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
},
versionCli: '',
versionNotifications: {
enabled: false,
endpoint: '',
infoUrl: '',
},
workflowCallerPolicyDefaultOption: 'any',
workflowTagsDisabled: false,
deployment: {
type: 'default',
},
};
const DEFAULT_SETTINGS: IN8nUISettings = SETTINGS_STORE_DEFAULT_STATE.settings;
const DEFAULT_USER: IUser = {
id: '1',
@ -101,23 +40,18 @@ describe('userUtils', () => {
.find((route) => route.path.startsWith('/settings'))
?.children?.find((route) => route.name === VIEWS.SSO_SETTINGS)?.meta?.permissions;
const user: IUser = {
...DEFAULT_USER,
const user: IUser = merge({}, DEFAULT_USER, {
isDefaultUser: false,
isOwner: true,
globalRole: {
...DEFAULT_USER.globalRole,
id: '1',
name: 'owner',
createdAt: new Date(),
},
};
settingsStore.setSettings({
...DEFAULT_SETTINGS,
enterprise: { ...DEFAULT_SETTINGS.enterprise, saml: true },
});
settingsStore.setSettings(merge({}, DEFAULT_SETTINGS, { enterprise: { saml: true } }));
expect(isAuthorized(ssoSettingsPermissions, user)).toBe(true);
});
});

View file

@ -0,0 +1,103 @@
import { ISettingsState, UserManagementAuthenticationMethod } from '@/Interface';
export const waitAllPromises = () => new Promise((resolve) => setTimeout(resolve));
export const SETTINGS_STORE_DEFAULT_STATE: ISettingsState = {
settings: {
allowedModules: {},
communityNodesEnabled: false,
defaultLocale: '',
endpointWebhook: '',
endpointWebhookTest: '',
enterprise: {
advancedExecutionFilters: false,
sharing: false,
ldap: false,
saml: false,
logStreaming: false,
},
executionMode: '',
executionTimeout: 0,
hideUsagePage: false,
hiringBannerEnabled: false,
instanceId: '',
isNpmAvailable: false,
license: { environment: 'production' },
logLevel: 'info',
maxExecutionTimeout: 0,
oauthCallbackUrls: { oauth1: '', oauth2: '' },
onboardingCallPromptEnabled: false,
personalizationSurveyEnabled: false,
posthog: {
apiHost: '',
apiKey: '',
autocapture: false,
debug: false,
disableSessionRecording: false,
enabled: false,
},
publicApi: { enabled: false, latestVersion: 0, path: '', swaggerUi: { enabled: false } },
pushBackend: 'sse',
saveDataErrorExecution: '',
saveDataSuccessExecution: '',
saveManualExecutions: false,
sso: {
ldap: { loginEnabled: false, loginLabel: '' },
saml: { loginEnabled: false, loginLabel: '' },
},
telemetry: { enabled: false },
templates: { enabled: false, host: '' },
timezone: '',
urlBaseEditor: '',
urlBaseWebhook: '',
userManagement: {
enabled: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
},
versionCli: '',
versionNotifications: {
enabled: false,
endpoint: '',
infoUrl: '',
},
workflowCallerPolicyDefaultOption: 'any',
workflowTagsDisabled: false,
deployment: {
type: 'default',
},
},
promptsData: {
message: '',
title: '',
showContactPrompt: false,
showValueSurvey: false,
},
userManagement: {
enabled: false,
showSetupOnFirstLoad: false,
smtpSetup: false,
authenticationMethod: UserManagementAuthenticationMethod.Email,
},
templatesEndpointHealthy: false,
api: {
enabled: false,
latestVersion: 0,
path: '/',
swaggerUi: {
enabled: false,
},
},
ldap: {
loginLabel: '',
loginEnabled: false,
},
saml: {
loginLabel: '',
loginEnabled: false,
},
onboardingCallPromptEnabled: false,
saveDataErrorExecution: 'all',
saveDataSuccessExecution: 'all',
saveManualExecutions: false,
};

View file

@ -112,7 +112,7 @@ onBeforeMount(async () => {
</template>
</i18n>
</n8n-info-tip>
<div v-if="ssoStore.isEnterpriseSamlEnabled">
<div v-if="ssoStore.isEnterpriseSamlEnabled" data-testid="sso-content-licensed">
<div :class="$style.group">
<label>{{ locale.baseText('settings.sso.settings.redirectUrl.label') }}</label>
<CopyInput
@ -135,20 +135,26 @@ onBeforeMount(async () => {
</div>
<div :class="$style.group">
<label>{{ locale.baseText('settings.sso.settings.ips.label') }}</label>
<n8n-input v-model="metadata" type="textarea" />
<n8n-input v-model="metadata" type="textarea" name="metadata" />
<small>{{ locale.baseText('settings.sso.settings.ips.help') }}</small>
</div>
<div :class="$style.buttons">
<n8n-button :disabled="!ssoSettingsSaved" type="tertiary" @click="onTest">
<n8n-button
:disabled="!ssoSettingsSaved"
type="tertiary"
@click="onTest"
data-testid="sso-test"
>
{{ locale.baseText('settings.sso.settings.test') }}
</n8n-button>
<n8n-button :disabled="!metadata" @click="onSave">
<n8n-button :disabled="!metadata" @click="onSave" data-testid="sso-save">
{{ locale.baseText('settings.sso.settings.save') }}
</n8n-button>
</div>
</div>
<n8n-action-box
v-else
data-testid="sso-content-unlicensed"
:class="$style.actionBox"
:description="locale.baseText('settings.sso.actionBox.description')"
:buttonText="locale.baseText('settings.sso.actionBox.buttonText')"

View file

@ -0,0 +1,49 @@
import { PiniaVuePlugin } from 'pinia';
import { render } from '@testing-library/vue';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
import AuthView from '@/views/AuthView.vue';
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
AuthView,
merge(
{
pinia: createTestingPinia(),
stubs: {
SSOLogin: {
template: '<div data-testid="sso-login"></div>',
},
},
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('AuthView', () => {
it('should render with subtitle', () => {
const { getByText } = renderComponent({
props: {
subtitle: 'Some text',
},
});
expect(getByText('Some text')).toBeInTheDocument();
});
it('should render without SSO component', () => {
const { queryByTestId } = renderComponent();
expect(queryByTestId('sso-login')).not.toBeInTheDocument();
});
it('should render with SSO component', () => {
const { getByTestId } = renderComponent({
props: {
withSso: true,
},
});
expect(getByTestId('sso-login')).toBeInTheDocument();
});
});

View file

@ -0,0 +1,82 @@
import { PiniaVuePlugin } from 'pinia';
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { useRouter } from 'vue-router/composables';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
import SamlOnboarding from '@/views/SamlOnboarding.vue';
import { useSSOStore } from '@/stores/sso';
import { STORES } from '@/constants';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/utils/testUtils';
import { i18nInstance } from '@/plugins/i18n';
vi.mock('vue-router/composables', () => {
const push = vi.fn();
return {
useRouter: () => ({
push,
}),
};
});
let pinia: ReturnType<typeof createTestingPinia>;
let ssoStore: ReturnType<typeof useSSOStore>;
let router: ReturnType<typeof useRouter>;
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
SamlOnboarding,
merge(
{
pinia,
i18n: i18nInstance,
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SamlOnboarding', () => {
beforeEach(() => {
pinia = createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
},
});
ssoStore = useSSOStore(pinia);
router = useRouter();
});
afterEach(() => {
vi.clearAllMocks();
});
it('should submit filled in form only and redirect', async () => {
vi.spyOn(ssoStore, 'updateUser').mockResolvedValue({
id: '1',
isPending: false,
});
const { getByRole, getAllByRole } = renderComponent();
const inputs = getAllByRole('textbox');
const submit = getByRole('button');
await userEvent.click(submit);
await waitAllPromises();
expect(ssoStore.updateUser).not.toHaveBeenCalled();
expect(router.push).not.toHaveBeenCalled();
await userEvent.type(inputs[0], 'test');
await userEvent.type(inputs[1], 'test');
await userEvent.click(submit);
expect(ssoStore.updateUser).toHaveBeenCalled();
expect(router.push).toHaveBeenCalled();
});
});

View file

@ -0,0 +1,115 @@
import { PiniaVuePlugin } from 'pinia';
import { render } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { createTestingPinia } from '@pinia/testing';
import { merge } from 'lodash-es';
import { faker } from '@faker-js/faker';
import SettingsSso from '@/views/SettingsSso.vue';
import { useSSOStore } from '@/stores/sso';
import { STORES } from '@/constants';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/utils/testUtils';
import { i18nInstance } from '@/plugins/i18n';
import { SamlPreferences, SamlPreferencesExtractedData } from '@/Interface';
let pinia: ReturnType<typeof createTestingPinia>;
let ssoStore: ReturnType<typeof useSSOStore>;
const samlConfig: SamlPreferences & SamlPreferencesExtractedData = {
metadata: '<?xml version="1.0"?>',
entityID: faker.internet.url(),
returnUrl: faker.internet.url(),
};
const renderComponent = (renderOptions: Parameters<typeof render>[1] = {}) =>
render(
SettingsSso,
merge(
{
pinia,
i18n: i18nInstance,
},
renderOptions,
),
(vue) => {
vue.use(PiniaVuePlugin);
},
);
describe('SettingsSso', () => {
beforeEach(() => {
pinia = createTestingPinia({
initialState: {
[STORES.SETTINGS]: {
settings: merge({}, SETTINGS_STORE_DEFAULT_STATE.settings),
},
},
});
ssoStore = useSSOStore(pinia);
});
afterEach(() => {
vi.clearAllMocks();
});
it('should render paywall state when there is no license', () => {
const { getByTestId, queryByTestId, queryByRole } = renderComponent();
expect(queryByRole('checkbox')).not.toBeInTheDocument();
expect(queryByTestId('sso-content-licensed')).not.toBeInTheDocument();
expect(getByTestId('sso-content-unlicensed')).toBeInTheDocument();
});
it('should render licensed content', () => {
vi.spyOn(ssoStore, 'isEnterpriseSamlEnabled', 'get').mockReturnValue(true);
const { getByTestId, queryByTestId, getByRole } = renderComponent();
expect(getByRole('checkbox')).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 () => {
vi.spyOn(ssoStore, 'isEnterpriseSamlEnabled', 'get').mockReturnValue(true);
vi.spyOn(ssoStore, 'getSamlConfig').mockResolvedValue(samlConfig);
const { getByRole, getByTestId } = renderComponent();
await waitAllPromises();
expect(getByRole('checkbox')).toBeEnabled();
expect(getByTestId('sso-test')).toBeEnabled();
});
it('should enable activation checkbox after data is saved', async () => {
vi.spyOn(ssoStore, 'isEnterpriseSamlEnabled', 'get').mockReturnValue(true);
const { getByRole, getAllByRole, getByTestId } = renderComponent();
const checkbox = getByRole('checkbox');
const btnSave = getByTestId('sso-save');
const btnTest = getByTestId('sso-test');
expect(checkbox).toBeDisabled();
[btnSave, btnTest].forEach((el) => {
expect(el).toBeDisabled();
});
await userEvent.type(
getAllByRole('textbox').find((el) => el.getAttribute('name') === 'metadata')!,
'<?xml version="1.0"?>',
);
expect(checkbox).toBeDisabled();
expect(btnTest).toBeDisabled();
expect(btnSave).toBeEnabled();
const saveSpy = vi.spyOn(ssoStore, 'saveSamlConfig');
const getSpy = vi.spyOn(ssoStore, 'getSamlConfig').mockResolvedValue(samlConfig);
await userEvent.click(btnSave);
expect(saveSpy).toHaveBeenCalled();
expect(getSpy).toHaveBeenCalled();
expect(checkbox).toBeEnabled();
expect(btnTest).toBeEnabled();
expect(btnSave).toBeEnabled();
});
});