mirror of
https://github.com/logto-io/logto.git
synced 2025-01-27 21:39:16 -05:00
feat(core,phrases): validate sign-in methods with enabled connectors (#480)
This commit is contained in:
parent
039f3d0cbb
commit
efdb24833e
7 changed files with 179 additions and 23 deletions
|
@ -1,12 +1,21 @@
|
|||
import { BrandingStyle, SignInMethodState } from '@logto/schemas';
|
||||
|
||||
import { ConnectorType } from '@/connectors/types';
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import {
|
||||
validateBranding,
|
||||
validateSignInMethods,
|
||||
validateTermsOfUse,
|
||||
} from '@/lib/sign-in-experience';
|
||||
import { mockBranding, mockSignInMethods } from '@/utils/mock';
|
||||
import {
|
||||
mockAliyunDmConnectorInstance,
|
||||
mockBranding,
|
||||
mockFacebookConnectorInstance,
|
||||
mockGithubConnectorInstance,
|
||||
mockSignInMethods,
|
||||
} from '@/utils/mock';
|
||||
|
||||
const enabledConnectorInstances = [mockFacebookConnectorInstance, mockGithubConnectorInstance];
|
||||
|
||||
describe('validate branding', () => {
|
||||
test('should throw when the UI style contains the slogan and slogan is empty', () => {
|
||||
|
@ -51,26 +60,66 @@ describe('validate terms of use', () => {
|
|||
|
||||
describe('validate sign-in methods', () => {
|
||||
describe('There must be one and only one primary sign-in method.', () => {
|
||||
test('should throw when there is no primary sign-in method', async () => {
|
||||
test('should throw when there is no primary sign-in method', () => {
|
||||
expect(() => {
|
||||
validateSignInMethods({
|
||||
...mockSignInMethods,
|
||||
username: SignInMethodState.disabled,
|
||||
});
|
||||
validateSignInMethods({ ...mockSignInMethods, username: SignInMethodState.disabled }, []);
|
||||
}).toMatchError(
|
||||
new RequestError('sign_in_experiences.not_one_and_only_one_primary_sign_in_method')
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw when there are more than one primary sign-in methods', async () => {
|
||||
test('should throw when there are more than one primary sign-in methods', () => {
|
||||
expect(() => {
|
||||
validateSignInMethods({
|
||||
...mockSignInMethods,
|
||||
social: SignInMethodState.primary,
|
||||
});
|
||||
validateSignInMethods({ ...mockSignInMethods, social: SignInMethodState.primary }, []);
|
||||
}).toMatchError(
|
||||
new RequestError('sign_in_experiences.not_one_and_only_one_primary_sign_in_method')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('There must be at least one enabled connector when the specific sign-in method is enabled.', () => {
|
||||
test('should throw when there is no enabled email connector and email sign-in method is enabled', async () => {
|
||||
expect(() => {
|
||||
validateSignInMethods(
|
||||
{ ...mockSignInMethods, email: SignInMethodState.secondary },
|
||||
// @ts-expect-error-this-line
|
||||
enabledConnectorInstances
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.Email,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw when there is no enabled SMS connector and SMS sign-in method is enabled', () => {
|
||||
expect(() => {
|
||||
validateSignInMethods(
|
||||
{ ...mockSignInMethods, sms: SignInMethodState.secondary },
|
||||
// @ts-expect-error-this-line
|
||||
enabledConnectorInstances
|
||||
);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.SMS,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
test('should throw when there is no enabled social connector and social sign-in method is enabled', () => {
|
||||
expect(() => {
|
||||
validateSignInMethods({ ...mockSignInMethods, social: SignInMethodState.secondary }, [
|
||||
// @ts-expect-error-this-line
|
||||
mockAliyunDmConnectorInstance,
|
||||
]);
|
||||
}).toMatchError(
|
||||
new RequestError({
|
||||
code: 'sign_in_experiences.enabled_connector_not_found',
|
||||
type: ConnectorType.Social,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
TermsOfUse,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { ConnectorInstance, ConnectorType } from '@/connectors/types';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
export const validateBranding = (branding: Branding) => {
|
||||
|
@ -21,12 +22,41 @@ export const validateTermsOfUse = (termsOfUse: TermsOfUse) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const validateSignInMethods = (signInMethods: SignInMethods) => {
|
||||
const isEnabled = (state: SignInMethodState) => state !== SignInMethodState.disabled;
|
||||
|
||||
export const validateSignInMethods = (
|
||||
signInMethods: SignInMethods,
|
||||
enabledConnectorInstances: ConnectorInstance[]
|
||||
) => {
|
||||
const signInMethodStates = Object.values(signInMethods);
|
||||
assertThat(
|
||||
signInMethodStates.filter((state) => state === SignInMethodState.primary).length === 1,
|
||||
'sign_in_experiences.not_one_and_only_one_primary_sign_in_method'
|
||||
);
|
||||
|
||||
// TODO: assert others next PR
|
||||
if (isEnabled(signInMethods.email)) {
|
||||
assertThat(
|
||||
enabledConnectorInstances.some((item) => item.metadata.type === ConnectorType.Email),
|
||||
'sign_in_experiences.enabled_connector_not_found',
|
||||
{ type: ConnectorType.Email }
|
||||
);
|
||||
}
|
||||
|
||||
if (isEnabled(signInMethods.sms)) {
|
||||
assertThat(
|
||||
enabledConnectorInstances.some((item) => item.metadata.type === ConnectorType.SMS),
|
||||
'sign_in_experiences.enabled_connector_not_found',
|
||||
{ type: ConnectorType.SMS }
|
||||
);
|
||||
}
|
||||
|
||||
if (isEnabled(signInMethods.social)) {
|
||||
assertThat(
|
||||
enabledConnectorInstances.some((item) => item.metadata.type === ConnectorType.Social),
|
||||
'sign_in_experiences.enabled_connector_not_found',
|
||||
{ type: ConnectorType.Social }
|
||||
);
|
||||
// TODO: assertNonemptySocialConnectorIds
|
||||
// TODO: assertEnabledSocialConnectorIds
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,15 +1,33 @@
|
|||
import { SignInExperience, CreateSignInExperience, TermsOfUse } from '@logto/schemas';
|
||||
|
||||
import * as signInExpLib from '@/lib/sign-in-experience';
|
||||
import { mockBranding, mockSignInExperience, mockSignInMethods } from '@/utils/mock';
|
||||
import {
|
||||
mockBranding,
|
||||
mockFacebookConnectorInstance,
|
||||
mockGithubConnectorInstance,
|
||||
mockGoogleConnectorInstance,
|
||||
mockSignInExperience,
|
||||
mockSignInMethods,
|
||||
} from '@/utils/mock';
|
||||
import { createRequester } from '@/utils/test-utils';
|
||||
|
||||
import signInExperiencesRoutes from './sign-in-experience';
|
||||
|
||||
jest.mock('@/connectors', () => ({
|
||||
...jest.requireActual('@/connectors'),
|
||||
getEnabledSocialConnectorIds: jest.fn(async () => ['facebook', 'github']),
|
||||
}));
|
||||
const connectorInstances = [
|
||||
mockFacebookConnectorInstance,
|
||||
mockGithubConnectorInstance,
|
||||
mockGoogleConnectorInstance,
|
||||
];
|
||||
|
||||
const getConnectorInstances = jest.fn(async () => connectorInstances);
|
||||
|
||||
jest.mock('@/connectors', () => {
|
||||
return {
|
||||
...jest.requireActual('@/connectors'),
|
||||
getEnabledSocialConnectorIds: jest.fn(async () => ['facebook', 'github']),
|
||||
getConnectorInstances: jest.fn(async () => getConnectorInstances()),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@/queries/sign-in-experience', () => ({
|
||||
findDefaultSignInExperience: jest.fn(async (): Promise<SignInExperience> => mockSignInExperience),
|
||||
|
@ -34,7 +52,7 @@ describe('signInExperiences routes', () => {
|
|||
|
||||
it('PATCH /sign-in-exp', async () => {
|
||||
const termsOfUse: TermsOfUse = { enabled: false };
|
||||
const socialSignInConnectorIds = ['abc', 'def'];
|
||||
const socialSignInConnectorIds = ['github', 'facebook'];
|
||||
|
||||
const validateBranding = jest.spyOn(signInExpLib, 'validateBranding');
|
||||
const validateTermsOfUse = jest.spyOn(signInExpLib, 'validateTermsOfUse');
|
||||
|
@ -49,7 +67,10 @@ describe('signInExperiences routes', () => {
|
|||
|
||||
expect(validateBranding).toHaveBeenCalledWith(mockBranding);
|
||||
expect(validateTermsOfUse).toHaveBeenCalledWith(termsOfUse);
|
||||
expect(validateSignInMethods).toHaveBeenCalledWith(mockSignInMethods);
|
||||
expect(validateSignInMethods).toHaveBeenCalledWith(mockSignInMethods, [
|
||||
mockFacebookConnectorInstance,
|
||||
mockGithubConnectorInstance,
|
||||
]);
|
||||
// TODO: only update socialSignInConnectorIds when social sign-in is enabled.
|
||||
|
||||
expect(response).toMatchObject({
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { SignInExperiences } from '@logto/schemas';
|
||||
|
||||
import { getEnabledSocialConnectorIds } from '@/connectors';
|
||||
import { getConnectorInstances, getEnabledSocialConnectorIds } from '@/connectors';
|
||||
import {
|
||||
validateBranding,
|
||||
validateTermsOfUse,
|
||||
|
@ -57,7 +57,13 @@ export default function signInExperiencesRoutes<T extends AuthedRouter>(router:
|
|||
}
|
||||
|
||||
if (signInMethods) {
|
||||
validateSignInMethods(signInMethods);
|
||||
// TODO: LOG-2055 refactor connectors
|
||||
const connectorInstances = await getConnectorInstances();
|
||||
const enabledConnectorInstances = connectorInstances.filter(
|
||||
(instance) => instance.connector.enabled
|
||||
);
|
||||
|
||||
validateSignInMethods(signInMethods, enabledConnectorInstances);
|
||||
}
|
||||
|
||||
// TODO: validate socialConnectorIds
|
||||
|
|
|
@ -390,4 +390,52 @@ export const mockSignInMethods: SignInMethods = {
|
|||
sms: SignInMethodState.disabled,
|
||||
social: SignInMethodState.disabled,
|
||||
};
|
||||
|
||||
export const mockAliyunDmConnectorInstance = {
|
||||
connector: {
|
||||
id: 'aliyun-dm',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
},
|
||||
metadata: {
|
||||
type: ConnectorType.Email,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockFacebookConnectorInstance = {
|
||||
connector: {
|
||||
id: 'facebook',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_333,
|
||||
},
|
||||
metadata: {
|
||||
type: ConnectorType.Social,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGithubConnectorInstance = {
|
||||
connector: {
|
||||
id: 'github',
|
||||
enabled: true,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
},
|
||||
metadata: {
|
||||
type: ConnectorType.Social,
|
||||
},
|
||||
};
|
||||
|
||||
export const mockGoogleConnectorInstance = {
|
||||
connector: {
|
||||
id: 'google',
|
||||
enabled: false,
|
||||
config: {},
|
||||
createdAt: 1_646_382_233_000,
|
||||
},
|
||||
metadata: {
|
||||
type: ConnectorType.Social,
|
||||
},
|
||||
};
|
||||
/* eslint-enable max-lines */
|
||||
|
|
|
@ -347,6 +347,7 @@ const errors = {
|
|||
'Empty "Terms of use" content URL. Please add the content URL if "Terms of use" is enabled.',
|
||||
empty_slogan:
|
||||
'Empty branding slogan. Please add a branding slogan if a UI style containing the slogan is selected.',
|
||||
enabled_connector_not_found: 'Enabled {{type}} connector not found.',
|
||||
not_one_and_only_one_primary_sign_in_method:
|
||||
'There must be one and only one primary sign-in method. Please check your input.',
|
||||
},
|
||||
|
|
|
@ -344,7 +344,8 @@ const errors = {
|
|||
empty_content_url_of_terms_of_use:
|
||||
'空的《用户协议》内容链接。当启用《用户协议》时,请添加其内容链接。',
|
||||
empty_slogan: '空的标语。当使用包含标语的 UI 风格时,请添加标语。',
|
||||
not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个。请检查你的输入。',
|
||||
enabled_connector_not_found: '未找到可用的 {{type}} 类型的连接器。',
|
||||
not_one_and_only_one_primary_sign_in_method: '主要的登录方式必须有且仅有一个,请检查你的输入。',
|
||||
},
|
||||
swagger: {
|
||||
invalid_zod_type: '无效的 Zod 类型,请检查路由 guard 配置。',
|
||||
|
|
Loading…
Add table
Reference in a new issue