mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
test(core): add register integration tests (#6248)
* test(core): add register integration tests add register integration tests * test: add enterprise sso integration tests add enterprise sso integration tests
This commit is contained in:
parent
ce3a62bc7a
commit
ae4a12757a
7 changed files with 398 additions and 12 deletions
|
@ -127,7 +127,7 @@ export default function experienceApiRoutes<T extends AnonymousRouter>(
|
|||
router.post(
|
||||
`${experienceRoutes.prefix}/submit`,
|
||||
koaGuard({
|
||||
status: [200],
|
||||
status: [200, 400],
|
||||
response: z.object({
|
||||
redirectTo: z.string(),
|
||||
}),
|
||||
|
|
|
@ -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 }) {
|
||||
|
|
|
@ -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,128 @@ 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;
|
||||
};
|
||||
|
||||
/**
|
||||
*
|
||||
* @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,
|
||||
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;
|
||||
};
|
||||
|
||||
export const signInWithEnterpriseSso = async (
|
||||
connectorId: string,
|
||||
enterpriseUserInfo: Record<string, unknown>,
|
||||
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;
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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<string, string>();
|
||||
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);
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue