0
Fork 0
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:
simeng-li 2024-07-16 10:55:48 +08:00 committed by GitHub
parent ce3a62bc7a
commit ae4a12757a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 398 additions and 12 deletions

View file

@ -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(),
}),

View file

@ -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, {
return api.post(experienceRoutes.identification, {
headers: { cookie: this.interactionCookie },
json: payload,
})
.json();
});
}
public async updateInteractionEvent(payload: { interactionEvent: InteractionEvent }) {

View file

@ -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;
};

View file

@ -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);
}
);
});

View file

@ -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);
});
});

View file

@ -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);
});
});

View file

@ -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);
}
);
});