From fb35438ff4bc3682678c5384459f102e93264077 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 13:54:50 +0800 Subject: [PATCH 1/2] test(core): add register integration tests add register integration tests --- packages/core/src/routes/experience/index.ts | 2 +- .../src/client/experience/index.ts | 10 +- .../src/helpers/experience/index.ts | 84 +++++++++++++++ .../verification-code.test.ts | 101 ++++++++++++++++++ .../sign-in-interaction/social.test.ts | 48 +++++++++ .../verification-code.test.ts | 70 +++++++++++- 6 files changed, 303 insertions(+), 12 deletions(-) create mode 100644 packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts create mode 100644 packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts diff --git a/packages/core/src/routes/experience/index.ts b/packages/core/src/routes/experience/index.ts index ec83da600bd..850ed02ef9e 100644 --- a/packages/core/src/routes/experience/index.ts +++ b/packages/core/src/routes/experience/index.ts @@ -127,7 +127,7 @@ export default function experienceApiRoutes( router.post( `${experienceRoutes.prefix}/submit`, koaGuard({ - status: [200], + status: [200, 400], response: z.object({ redirectTo: z.string(), }), diff --git a/packages/integration-tests/src/client/experience/index.ts b/packages/integration-tests/src/client/experience/index.ts index 8a06e389f4f..1b5217616ed 100644 --- a/packages/integration-tests/src/client/experience/index.ts +++ b/packages/integration-tests/src/client/experience/index.ts @@ -26,12 +26,10 @@ export const identifyUser = async (cookie: string, payload: IdentificationApiPay export class ExperienceClient extends MockClient { public async identifyUser(payload: IdentificationApiPayload) { - return api - .post(experienceRoutes.identification, { - headers: { cookie: this.interactionCookie }, - json: payload, - }) - .json(); + return api.post(experienceRoutes.identification, { + headers: { cookie: this.interactionCookie }, + json: payload, + }); } public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) { diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index 95082dda021..f702fdabd97 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -2,6 +2,7 @@ * @fileoverview This file contains the successful interaction flow helper functions that use the experience APIs. */ +import { type SocialUserInfo } from '@logto/connector-kit'; import { InteractionEvent, SignInIdentifier, @@ -12,7 +13,12 @@ import { import { type ExperienceClient } from '#src/client/experience/index.js'; import { initExperienceClient, logoutClient, processSession } from '../client.js'; +import { expectRejects } from '../index.js'; +import { + successFullyCreateSocialVerification, + successFullyVerifySocialAuthorization, +} from './social-verification.js'; import { successfullySendVerificationCode, successfullyVerifyVerificationCode, @@ -96,3 +102,81 @@ export const identifyUserWithUsernamePassword = async ( return { verificationId }; }; + +export const registerNewUserWithVerificationCode = async ( + identifier: VerificationCodeIdentifier +) => { + const client = await initExperienceClient(); + + await client.initInteraction({ interactionEvent: InteractionEvent.Register }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.Register, + }); + + const verifiedVerificationId = await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await client.identifyUser({ + verificationId: verifiedVerificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; + +export const signInWithSocial = async ( + connectorId: string, + socialUserInfo: SocialUserInfo, + registerNewUser = false +) => { + const state = 'state'; + const redirectUri = 'http://localhost:3000'; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId } = await successFullyCreateSocialVerification(client, connectorId, { + redirectUri, + state, + }); + + await successFullyVerifySocialAuthorization(client, connectorId, { + verificationId, + connectorData: { + state, + redirectUri, + code: 'fake_code', + userId: socialUserInfo.id, + email: socialUserInfo.email, + }, + }); + + if (registerNewUser) { + await expectRejects(client.identifyUser({ verificationId }), { + code: 'user.identity_not_exist', + status: 404, + }); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + await client.identifyUser({ verificationId }); + } else { + await client.identifyUser({ + verificationId, + }); + } + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; diff --git a/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts new file mode 100644 index 00000000000..c210797d41e --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/register-interaction/verification-code.test.ts @@ -0,0 +1,101 @@ +import { + InteractionEvent, + SignInIdentifier, + type VerificationCodeIdentifier, +} from '@logto/schemas'; + +import { deleteUser } from '#src/api/admin-user.js'; +import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js'; +import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js'; +import { registerNewUserWithVerificationCode } from '#src/helpers/experience/index.js'; +import { + successfullySendVerificationCode, + successfullyVerifyVerificationCode, +} from '#src/helpers/experience/verification-code.js'; +import { expectRejects } from '#src/helpers/index.js'; +import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; +import { generateNewUser } from '#src/helpers/user.js'; +import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js'; + +const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] = + Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]); + +const identifiersTypeToUserProfile = Object.freeze({ + email: 'primaryEmail', + phone: 'primaryPhone', +}); + +devFeatureTest.describe('Register interaction with verification code happy path', () => { + beforeAll(async () => { + await Promise.all([setEmailConnector(), setSmsConnector()]); + await enableAllVerificationCodeSignInMethods({ + identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone], + password: false, + verify: true, + }); + }); + + it.each(verificationIdentifierType)( + 'Should register with verification code using %p successfully', + async (identifier) => { + const userId = await registerNewUserWithVerificationCode({ + type: identifier, + value: identifier === SignInIdentifier.Email ? generateEmail() : generatePhone(), + }); + + await deleteUser(userId); + } + ); + + it.each(verificationIdentifierType)( + 'Should fail to sign-up with existing %p identifier and directly sign-in instead ', + async (identifierType) => { + const { userProfile, user } = await generateNewUser({ + [identifiersTypeToUserProfile[identifierType]]: true, + }); + + const identifier: VerificationCodeIdentifier = { + type: identifierType, + value: userProfile[identifiersTypeToUserProfile[identifierType]]!, + }; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.Register }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.Register, + }); + + await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await expectRejects( + client.identifyUser({ + verificationId, + }), + { + code: `user.${identifierType}_already_in_use`, + status: 422, + } + ); + + await client.updateInteractionEvent({ + interactionEvent: InteractionEvent.SignIn, + }); + + await client.identifyUser({ + verificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + await processSession(client, redirectTo); + await logoutClient(client); + + await deleteUser(user.id); + } + ); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts new file mode 100644 index 00000000000..6ae61112025 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/social.test.ts @@ -0,0 +1,48 @@ +import { ConnectorType } from '@logto/connector-kit'; +import { generateStandardId } from '@logto/shared'; + +import { mockSocialConnectorId } from '#src/__mocks__/connectors-mock.js'; +import { deleteUser, getUser } from '#src/api/admin-user.js'; +import { clearConnectorsByTypes, setSocialConnector } from '#src/helpers/connector.js'; +import { signInWithSocial } from '#src/helpers/experience/index.js'; +import { devFeatureTest, generateEmail } from '#src/utils.js'; + +devFeatureTest.describe('social sign-in and sign-up', () => { + const connectorIdMap = new Map(); + const socialUserId = generateStandardId(); + const email = generateEmail(); + + beforeAll(async () => { + await clearConnectorsByTypes([ConnectorType.Social]); + + const { id: socialConnectorId } = await setSocialConnector(); + connectorIdMap.set(mockSocialConnectorId, socialConnectorId); + }); + + afterAll(async () => { + await clearConnectorsByTypes([ConnectorType.Social]); + }); + + it('should successfully sign-up with social and sync email', async () => { + const userId = await signInWithSocial( + connectorIdMap.get(mockSocialConnectorId)!, + { + id: socialUserId, + email, + }, + true + ); + + const { primaryEmail } = await getUser(userId); + expect(primaryEmail).toBe(email); + }); + + it('should successfully sign-in with social', async () => { + const userId = await signInWithSocial(connectorIdMap.get(mockSocialConnectorId)!, { + id: socialUserId, + email, + }); + + await deleteUser(userId); + }); +}); diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts index 4deb166f167..5c2b92807f6 100644 --- a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/verification-code.test.ts @@ -1,11 +1,21 @@ -import { SignInIdentifier } from '@logto/schemas'; +import { + InteractionEvent, + SignInIdentifier, + type VerificationCodeIdentifier, +} from '@logto/schemas'; import { deleteUser } from '#src/api/admin-user.js'; +import { initExperienceClient, logoutClient, processSession } from '#src/helpers/client.js'; import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js'; import { signInWithVerificationCode } from '#src/helpers/experience/index.js'; +import { + successfullySendVerificationCode, + successfullyVerifyVerificationCode, +} from '#src/helpers/experience/verification-code.js'; +import { expectRejects } from '#src/helpers/index.js'; import { enableAllVerificationCodeSignInMethods } from '#src/helpers/sign-in-experience.js'; import { generateNewUser } from '#src/helpers/user.js'; -import { devFeatureTest } from '#src/utils.js'; +import { devFeatureTest, generateEmail, generatePhone } from '#src/utils.js'; const verificationIdentifierType: readonly [SignInIdentifier.Email, SignInIdentifier.Phone] = Object.freeze([SignInIdentifier.Email, SignInIdentifier.Phone]); @@ -15,14 +25,18 @@ const identifiersTypeToUserProfile = Object.freeze({ phone: 'primaryPhone', }); -devFeatureTest.describe('Sign-in with verification code happy path', () => { +devFeatureTest.describe('Sign-in with verification code', () => { beforeAll(async () => { await Promise.all([setEmailConnector(), setSmsConnector()]); - await enableAllVerificationCodeSignInMethods(); + await enableAllVerificationCodeSignInMethods({ + identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone], + password: false, + verify: true, + }); }); it.each(verificationIdentifierType)( - 'Should sign-in with verification code using %p', + 'should sign-in with verification code using %p', async (identifier) => { const { userProfile, user } = await generateNewUser({ [identifiersTypeToUserProfile[identifier]]: true, @@ -37,4 +51,50 @@ devFeatureTest.describe('Sign-in with verification code happy path', () => { await deleteUser(user.id); } ); + + it.each(verificationIdentifierType)( + 'should fail to sign-in with non-existing %p identifier and directly sign-up instead', + async (type) => { + const identifier: VerificationCodeIdentifier = { + type, + value: type === SignInIdentifier.Email ? generateEmail() : generatePhone(), + }; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId, code } = await successfullySendVerificationCode(client, { + identifier, + interactionEvent: InteractionEvent.SignIn, + }); + + await successfullyVerifyVerificationCode(client, { + identifier, + verificationId, + code, + }); + + await expectRejects( + client.identifyUser({ + verificationId, + }), + { + code: 'user.user_not_exist', + status: 404, + } + ); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + + await client.identifyUser({ + verificationId, + }); + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + await deleteUser(userId); + } + ); }); From 58556ef41ea12f3ed2c59d5030e8676ba49ac09e Mon Sep 17 00:00:00 2001 From: simeng-li Date: Mon, 15 Jul 2024 14:58:09 +0800 Subject: [PATCH 2/2] test: add enterprise sso integration tests add enterprise sso integration tests --- .../src/helpers/experience/index.ts | 47 ++++++++++++++++++ .../enterprise-sso.test.ts | 48 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts diff --git a/packages/integration-tests/src/helpers/experience/index.ts b/packages/integration-tests/src/helpers/experience/index.ts index f702fdabd97..71926a4986c 100644 --- a/packages/integration-tests/src/helpers/experience/index.ts +++ b/packages/integration-tests/src/helpers/experience/index.ts @@ -133,6 +133,11 @@ export const registerNewUserWithVerificationCode = async ( return userId; }; +/** + * + * @param socialUserInfo The social user info that will be returned by the social connector. + * @param registerNewUser Optional. If true, the user will be registered if the user does not exist, otherwise a error will be thrown if the user does not exist. + */ export const signInWithSocial = async ( connectorId: string, socialUserInfo: SocialUserInfo, @@ -180,3 +185,45 @@ export const signInWithSocial = async ( return userId; }; + +export const signInWithEnterpriseSso = async ( + connectorId: string, + enterpriseUserInfo: Record, + registerNewUser = false +) => { + const state = 'state'; + const redirectUri = 'http://localhost:3000'; + + const client = await initExperienceClient(); + await client.initInteraction({ interactionEvent: InteractionEvent.SignIn }); + + const { verificationId } = await client.getEnterpriseSsoAuthorizationUri(connectorId, { + redirectUri, + state, + }); + + await client.verifyEnterpriseSsoAuthorization(connectorId, { + verificationId, + connectorData: enterpriseUserInfo, + }); + + if (registerNewUser) { + await expectRejects(client.identifyUser({ verificationId }), { + code: 'user.identity_not_exist', + status: 404, + }); + + await client.updateInteractionEvent({ interactionEvent: InteractionEvent.Register }); + await client.identifyUser({ verificationId }); + } else { + await client.identifyUser({ + verificationId, + }); + } + + const { redirectTo } = await client.submitInteraction(); + const userId = await processSession(client, redirectTo); + await logoutClient(client); + + return userId; +}; diff --git a/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts new file mode 100644 index 00000000000..e86cc54ec46 --- /dev/null +++ b/packages/integration-tests/src/tests/api/experience-api/sign-in-interaction/enterprise-sso.test.ts @@ -0,0 +1,48 @@ +import { generateStandardId } from '@logto/shared'; + +import { deleteUser, getUser } from '#src/api/admin-user.js'; +import { updateSignInExperience } from '#src/api/sign-in-experience.js'; +import { SsoConnectorApi } from '#src/api/sso-connector.js'; +import { signInWithEnterpriseSso } from '#src/helpers/experience/index.js'; +import { devFeatureTest, generateEmail } from '#src/utils.js'; + +devFeatureTest.describe('enterprise sso sign-in and sign-up', () => { + const ssoConnectorApi = new SsoConnectorApi(); + const domain = 'foo.com'; + const socialUserId = generateStandardId(); + const email = generateEmail(domain); + + beforeAll(async () => { + await ssoConnectorApi.createMockOidcConnector([domain]); + await updateSignInExperience({ singleSignOnEnabled: true }); + }); + + afterAll(async () => { + await ssoConnectorApi.cleanUp(); + }); + + it('should successfully sign-up with enterprise sso ans sync email', async () => { + const userId = await signInWithEnterpriseSso( + ssoConnectorApi.firstConnectorId!, + { + sub: socialUserId, + email, + email_verified: true, + }, + true + ); + + const { primaryEmail } = await getUser(userId); + expect(primaryEmail).toBe(email); + }); + + it('should successfully sign-in with enterprise sso', async () => { + const userId = await signInWithEnterpriseSso(ssoConnectorApi.firstConnectorId!, { + sub: socialUserId, + email, + email_verified: true, + }); + + await deleteUser(userId); + }); +});