Skip to content

Commit

Permalink
test(core): add register integration tests (#6248)
Browse files Browse the repository at this point in the history
* test(core): add register integration tests

add register integration tests

* test: add enterprise sso integration tests

add enterprise sso integration tests
  • Loading branch information
simeng-li authored Jul 16, 2024
1 parent ce3a62b commit ae4a127
Show file tree
Hide file tree
Showing 7 changed files with 398 additions and 12 deletions.
2 changes: 1 addition & 1 deletion packages/core/src/routes/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}),
Expand Down
10 changes: 4 additions & 6 deletions packages/integration-tests/src/client/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) {
Expand Down
131 changes: 131 additions & 0 deletions packages/integration-tests/src/helpers/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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;
};
Original file line number Diff line number Diff line change
@@ -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);
}
);
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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);
});
});
Loading

0 comments on commit ae4a127

Please sign in to comment.