Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test(core): add register integration tests #6248

Merged
merged 2 commits into from
Jul 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading