diff --git a/packages/core/src/routes/sign-in-experience.branding.guard.test.ts b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts new file mode 100644 index 000000000..9b3c7b7d2 --- /dev/null +++ b/packages/core/src/routes/sign-in-experience.branding.guard.test.ts @@ -0,0 +1,122 @@ +import { BrandingStyle, CreateSignInExperience, SignInExperience } from '@logto/schemas'; + +import { mockBranding, mockSignInExperience } from '@/utils/mock'; +import { createRequester } from '@/utils/test-utils'; + +import signInExperiencesRoutes from './sign-in-experience'; + +jest.mock('@/queries/sign-in-experience', () => ({ + updateDefaultSignInExperience: jest.fn( + async (data: Partial): Promise => ({ + ...mockSignInExperience, + ...data, + }) + ), +})); + +const signInExperienceRequester = createRequester({ authedRoutes: signInExperiencesRoutes }); + +const expectPatchResponseStatus = async (signInExperience: any, status: number) => { + const response = await signInExperienceRequester.patch('/sign-in-exp').send(signInExperience); + expect(response.status).toEqual(status); +}; + +describe('branding', () => { + const colorKeys = ['primaryColor', 'backgroundColor', 'darkPrimaryColor', 'darkBackgroundColor']; + + const invalidColors = [ + undefined, + null, + '', + '#', + '#1', + '#2B', + '#3cZ', + '#4D9e', + '#5f80E', + '#6GHiXY', + '#78Cb5dA', + 'rgb(0,13,255)', + ]; + + const validColors = ['#aB3', '#169deF']; + + describe('colors', () => { + test.each(validColors)('%p should succeed', async (validColor) => { + for (const colorKey of colorKeys) { + // eslint-disable-next-line no-await-in-loop + await expectPatchResponseStatus( + { branding: { ...mockBranding, [colorKey]: validColor } }, + 200 + ); + } + }); + test.each(invalidColors)('%p should fail', async (invalidColor) => { + for (const colorKey of colorKeys) { + // eslint-disable-next-line no-await-in-loop + await expectPatchResponseStatus( + { branding: { ...mockBranding, [colorKey]: invalidColor } }, + 400 + ); + } + }); + }); + + describe('style', () => { + test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => { + const signInExperience = { branding: { ...mockBranding, style } }; + await expectPatchResponseStatus(signInExperience, 200); + }); + + test.each([undefined, '', 'invalid'])('%p should fail', async (style) => { + const signInExperience = { branding: { ...mockBranding, style } }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + describe('logoUrl', () => { + test.each(['http://silverhand.com/silverhand.png', 'https://logto.dev/logto.jpg'])( + '%p should success', + async (logoUrl) => { + const signInExperience = { branding: { ...mockBranding, logoUrl } }; + await expectPatchResponseStatus(signInExperience, 200); + } + ); + + test.each([undefined, null, '', 'invalid'])('%p should fail', async (logoUrl) => { + const signInExperience = { branding: { ...mockBranding, logoUrl } }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + describe('slogan', () => { + test.each([undefined, 'Silverhand.', 'Supercharge innovations.'])( + '%p should success', + async (slogan) => { + const signInExperience = { + branding: { + ...mockBranding, + style: BrandingStyle.Logo, + slogan, + }, + }; + await expectPatchResponseStatus(signInExperience, 200); + } + ); + + test.each([null, ''])('%p should fail', async (slogan) => { + const signInExperience = { + branding: { + ...mockBranding, + style: BrandingStyle.Logo, + slogan, + }, + }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + it('should succeed when branding is valid', async () => { + await expectPatchResponseStatus({ branding: mockBranding }, 200); + }); +}); diff --git a/packages/core/src/routes/sign-in-experience.guard.test.ts b/packages/core/src/routes/sign-in-experience.guard.test.ts index d56fbe384..8a8a732ce 100644 --- a/packages/core/src/routes/sign-in-experience.guard.test.ts +++ b/packages/core/src/routes/sign-in-experience.guard.test.ts @@ -1,10 +1,35 @@ -import { BrandingStyle, CreateSignInExperience, Language, SignInExperience } from '@logto/schemas'; +import { + CreateSignInExperience, + Language, + SignInExperience, + SignInMethodState, +} from '@logto/schemas'; -import { mockBranding, mockLanguageInfo, mockSignInExperience, mockTermsOfUse } from '@/utils/mock'; +import { + mockAliyunDmConnectorInstance, + mockAliyunSmsConnectorInstance, + mockFacebookConnectorInstance, + mockGithubConnectorInstance, + mockGoogleConnectorInstance, + mockLanguageInfo, + mockSignInExperience, + mockSignInMethods, + mockTermsOfUse, +} from '@/utils/mock'; import { createRequester } from '@/utils/test-utils'; import signInExperiencesRoutes from './sign-in-experience'; +jest.mock('@/connectors', () => ({ + getConnectorInstances: jest.fn(async () => [ + mockAliyunDmConnectorInstance, + mockAliyunSmsConnectorInstance, + mockFacebookConnectorInstance, + mockGithubConnectorInstance, + mockGoogleConnectorInstance, + ]), +})); + jest.mock('@/queries/sign-in-experience', () => ({ updateDefaultSignInExperience: jest.fn( async (data: Partial): Promise => ({ @@ -24,109 +49,6 @@ const expectPatchResponseStatus = async (signInExperience: any, status: number) const validBooleans = [true, false]; const invalidBooleans = [undefined, null, 0, 1, '0', '1', 'true', 'false']; -describe('branding', () => { - const colorKeys = ['primaryColor', 'backgroundColor', 'darkPrimaryColor', 'darkBackgroundColor']; - - const invalidColors = [ - undefined, - null, - '', - '#', - '#1', - '#2B', - '#3cZ', - '#4D9e', - '#5f80E', - '#6GHiXY', - '#78Cb5dA', - 'rgb(0,13,255)', - ]; - - const validColors = ['#aB3', '#169deF']; - - describe('colors', () => { - test.each(validColors)('%p should succeed', async (validColor) => { - for (const colorKey of colorKeys) { - // eslint-disable-next-line no-await-in-loop - await expectPatchResponseStatus( - { branding: { ...mockBranding, [colorKey]: validColor } }, - 200 - ); - } - }); - test.each(invalidColors)('%p should fail', async (invalidColor) => { - for (const colorKey of colorKeys) { - // eslint-disable-next-line no-await-in-loop - await expectPatchResponseStatus( - { branding: { ...mockBranding, [colorKey]: invalidColor } }, - 400 - ); - } - }); - }); - - describe('style', () => { - test.each(Object.values(BrandingStyle))('%p should succeed', async (style) => { - const signInExperience = { branding: { ...mockBranding, style } }; - await expectPatchResponseStatus(signInExperience, 200); - }); - - test.each([undefined, '', 'invalid'])('%p should fail', async (style) => { - const signInExperience = { branding: { ...mockBranding, style } }; - await expectPatchResponseStatus(signInExperience, 400); - }); - }); - - describe('logoUrl', () => { - test.each(['http://silverhand.com/silverhand.png', 'https://logto.dev/logto.jpg'])( - '%p should success', - async (logoUrl) => { - const signInExperience = { branding: { ...mockBranding, logoUrl } }; - await expectPatchResponseStatus(signInExperience, 200); - } - ); - - test.each([undefined, null, '', 'invalid'])('%p should fail', async (logoUrl) => { - const signInExperience = { branding: { ...mockBranding, logoUrl } }; - await expectPatchResponseStatus(signInExperience, 400); - }); - }); - - describe('slogan', () => { - test.each([undefined, 'Silverhand.', 'Supercharge innovations.'])( - '%p should success', - async (slogan) => { - const signInExperience = { - branding: { - ...mockBranding, - style: BrandingStyle.Logo, - slogan, - }, - }; - await expectPatchResponseStatus(signInExperience, 200); - } - ); - - test.each([null, ''])('%p should fail', async (slogan) => { - const signInExperience = { - branding: { - ...mockBranding, - style: BrandingStyle.Logo, - slogan, - }, - }; - await expectPatchResponseStatus(signInExperience, 400); - }); - }); - - it('should succeed when branding is valid', async () => { - const response = signInExperienceRequester - .patch('/sign-in-exp') - .send({ branding: mockBranding }); - await expect(response).resolves.toMatchObject({ status: 200 }); - }); -}); - describe('terms of use', () => { describe('enabled', () => { test.each(validBooleans)('%p should success', async (enabled) => { @@ -197,12 +119,167 @@ describe('languageInfo', () => { }); }); -describe('socialSignInConnectorIds', () => { - it('should throw when the type of social connector IDs is wrong', async () => { - const socialSignInConnectorIds = [123, 456]; - const response = await signInExperienceRequester.patch('/sign-in-exp').send({ - socialSignInConnectorIds, +describe('signInMethods', () => { + const validSignInMethodStates = Object.values(SignInMethodState); + const invalidSignInMethodStates = [undefined, null, '', ' \t\n\r', 'invalid']; + + describe('username', () => { + test.each(validSignInMethodStates)('%p should success', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: state, + email: SignInMethodState.primary, + sms: SignInMethodState.disabled, + social: SignInMethodState.disabled, + }, + }; + await expectPatchResponseStatus(signInExperience, 200); + }); + + test.each(invalidSignInMethodStates)('%p should fail', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: state, + email: SignInMethodState.primary, + sms: SignInMethodState.disabled, + social: SignInMethodState.disabled, + }, + }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + describe('email', () => { + test.each(validSignInMethodStates)('%p should success', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.disabled, + email: state, + sms: SignInMethodState.primary, + social: SignInMethodState.disabled, + }, + }; + await expectPatchResponseStatus(signInExperience, 200); + }); + + test.each(invalidSignInMethodStates)('%p should fail', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.disabled, + email: state, + sms: SignInMethodState.primary, + social: SignInMethodState.disabled, + }, + }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + describe('sms', () => { + test.each(validSignInMethodStates)('%p should success', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.disabled, + email: SignInMethodState.disabled, + sms: state, + social: SignInMethodState.primary, + }, + socialSignInConnectorIds: ['github'], + }; + await expectPatchResponseStatus(signInExperience, 200); + }); + + test.each(invalidSignInMethodStates)('%p should fail', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.disabled, + email: SignInMethodState.disabled, + sms: state, + social: SignInMethodState.primary, + }, + socialSignInConnectorIds: ['github'], + }; + await expectPatchResponseStatus(signInExperience, 400); + }); + }); + + describe('social', () => { + test.each(validSignInMethodStates)('%p should success', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.primary, + email: SignInMethodState.disabled, + sms: SignInMethodState.disabled, + social: state, + }, + socialSignInConnectorIds: ['github'], + }; + await expectPatchResponseStatus(signInExperience, 200); + }); + + test.each(invalidSignInMethodStates)('%p should fail', async (state) => { + if (state === SignInMethodState.primary) { + return; + } + const signInExperience = { + signInMethods: { + username: SignInMethodState.primary, + email: SignInMethodState.disabled, + sms: SignInMethodState.disabled, + social: state, + }, + socialSignInConnectorIds: ['github'], + }; + await expectPatchResponseStatus(signInExperience, 400); }); - expect(response.status).toEqual(400); }); }); + +describe('socialSignInConnectorIds', () => { + test.each([[['facebook']], [['facebook', 'github']]])( + '%p should success', + async (socialSignInConnectorIds) => { + await expectPatchResponseStatus( + { + signInMethods: { ...mockSignInMethods, social: SignInMethodState.secondary }, + socialSignInConnectorIds, + }, + 200 + ); + } + ); + + test.each([[[]], [[null, undefined]], [['', ' \t\n\r']], [[123, 456]]])( + '%p should fail', + async (socialSignInConnectorIds: any[]) => { + await expectPatchResponseStatus( + { + signInMethods: { ...mockSignInMethods, social: SignInMethodState.secondary }, + socialSignInConnectorIds, + }, + 400 + ); + } + ); +}); diff --git a/packages/core/src/utils/mock.ts b/packages/core/src/utils/mock.ts index fb8b15014..bcc7b1bc9 100644 --- a/packages/core/src/utils/mock.ts +++ b/packages/core/src/utils/mock.ts @@ -430,6 +430,18 @@ export const mockAliyunDmConnectorInstance = { }, }; +export const mockAliyunSmsConnectorInstance = { + connector: { + id: 'aliyun-sms', + enabled: true, + config: {}, + createdAt: 1_646_382_233_333, + }, + metadata: { + type: ConnectorType.SMS, + }, +}; + export const mockFacebookConnectorInstance = { connector: { id: 'facebook',