Skip to content

Commit

Permalink
test(core): add profile fulfillment integration tests (#6294)
Browse files Browse the repository at this point in the history
* test(core): add profile fufillment integration tests

add profile fufillment integration tests

* fix: fix integration tests

fix integration tests

* refactor(test): rebase and update the latest profile api

rebase and update the latest profile api
  • Loading branch information
simeng-li authored Jul 25, 2024
1 parent 248ee7f commit 9d2770c
Show file tree
Hide file tree
Showing 8 changed files with 569 additions and 16 deletions.
1 change: 1 addition & 0 deletions packages/integration-tests/src/client/experience/const.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ const prefix = 'experience';
export const experienceRoutes = {
verification: `${prefix}/verification`,
identification: `${prefix}/identification`,
profile: `${prefix}/profile`,
prefix,
};
15 changes: 15 additions & 0 deletions packages/integration-tests/src/client/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
type InteractionEvent,
type NewPasswordIdentityVerificationPayload,
type PasswordVerificationPayload,
type UpdateProfileApiPayload,
type VerificationCodeIdentifier,
} from '@logto/schemas';

Expand Down Expand Up @@ -196,4 +197,18 @@ export class ExperienceClient extends MockClient {
})
.json<{ verificationId: string }>();
}

public async resetPassword(payload: { password: string }) {
return api.put(`${experienceRoutes.profile}/password`, {
headers: { cookie: this.interactionCookie },
json: payload,
});
}

public async updateProfile(payload: UpdateProfileApiPayload) {
return api.post(`${experienceRoutes.profile}`, {
headers: { cookie: this.interactionCookie },
json: payload,
});
}
}
18 changes: 17 additions & 1 deletion packages/integration-tests/src/helpers/experience/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@logto/schemas';

import { type ExperienceClient } from '#src/client/experience/index.js';
import { generatePassword } from '#src/utils.js';

import { initExperienceClient, logoutClient, processSession } from '../client.js';
import { expectRejects } from '../index.js';
Expand Down Expand Up @@ -104,7 +105,8 @@ export const identifyUserWithUsernamePassword = async (
};

export const registerNewUserWithVerificationCode = async (
identifier: VerificationCodeIdentifier
identifier: VerificationCodeIdentifier,
options?: { fulfillPassword?: boolean }
) => {
const client = await initExperienceClient();

Expand All @@ -125,6 +127,20 @@ export const registerNewUserWithVerificationCode = async (
verificationId: verifiedVerificationId,
});

if (options?.fulfillPassword) {
await expectRejects(client.submitInteraction(), {
code: 'user.missing_profile',
status: 422,
});

const password = generatePassword();

await client.updateProfile({
type: 'password',
value: password,
});
}

const { redirectTo } = await client.submitInteraction();

const userId = await processSession(client, redirectTo);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import { ConnectorType } from '@logto/connector-kit';
import { InteractionEvent, MfaFactor, SignInIdentifier } from '@logto/schemas';
import { authenticator } from 'otplib';

import { createUserMfaVerification } from '#src/api/admin-user.js';
import { initExperienceClient } from '#src/helpers/client.js';
import {
clearConnectorsByTypes,
setEmailConnector,
setSmsConnector,
} from '#src/helpers/connector.js';
import { identifyUserWithUsernamePassword } from '#src/helpers/experience/index.js';
import { successfullyVerifyTotp } from '#src/helpers/experience/totp-verification.js';
import {
successfullySendVerificationCode,
successfullyVerifyVerificationCode,
} from '#src/helpers/experience/verification-code.js';
import { expectRejects } from '#src/helpers/index.js';
import {
enableAllPasswordSignInMethods,
enableMandatoryMfaWithTotpAndBackupCode,
resetMfaSettings,
} from '#src/helpers/sign-in-experience.js';
import { generateNewUserProfile, UserApiTest } from '#src/helpers/user.js';
import { devFeatureTest, generateEmail } from '#src/utils.js';

devFeatureTest.describe('Fulfill User Profiles', () => {
const userApi = new UserApiTest();

beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await Promise.all([setEmailConnector(), setSmsConnector()]);
await enableAllPasswordSignInMethods();
});

afterEach(async () => {
await userApi.cleanUp();
});

it('should throw 400 if the interaction event is ForgotPassword', async () => {
const client = await initExperienceClient();

await client.initInteraction({ interactionEvent: InteractionEvent.ForgotPassword });

await expectRejects(
client.updateProfile({ type: SignInIdentifier.Username, value: 'username' }),
{
status: 400,
code: 'session.not_supported_for_forgot_password',
}
);
});

it('should throw 404 if the interaction is not identified', async () => {
const client = await initExperienceClient();

await client.initInteraction({ interactionEvent: InteractionEvent.SignIn });

await expectRejects(
client.updateProfile({ type: SignInIdentifier.Username, value: 'username' }),
{
status: 404,
code: 'session.identifier_not_found',
}
);
});

it('should throw 422 if the profile field is already exist in current user account', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });

await userApi.create({ username, password });

const client = await initExperienceClient();
await identifyUserWithUsernamePassword(client, username, password);

await expectRejects(
client.updateProfile({ type: SignInIdentifier.Username, value: 'username' }),
{
status: 422,
code: 'user.username_exists_in_profile',
}
);

await expectRejects(client.updateProfile({ type: 'password', value: 'password' }), {
status: 422,
code: 'user.password_exists_in_profile',
});
});

it('should throw 422 if the profile field is used by another user', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
await userApi.create({ username, password });

const { primaryEmail } = generateNewUserProfile({ primaryEmail: true });
await userApi.create({ primaryEmail });

const client = await initExperienceClient();
await identifyUserWithUsernamePassword(client, username, password);

const { verificationId, code: verificationCode } = await successfullySendVerificationCode(
client,
{
identifier: { type: SignInIdentifier.Email, value: primaryEmail },
interactionEvent: InteractionEvent.SignIn,
}
);

await successfullyVerifyVerificationCode(client, {
identifier: { type: SignInIdentifier.Email, value: primaryEmail },
verificationId,
code: verificationCode,
});

await expectRejects(client.updateProfile({ type: SignInIdentifier.Email, verificationId }), {
status: 422,
code: 'user.email_already_in_use',
});
});

describe('MFA verification status is required', () => {
beforeAll(async () => {
await enableMandatoryMfaWithTotpAndBackupCode();
});
afterAll(async () => {
await resetMfaSettings();
});

it('should throw 422 if the mfa is enabled but not verified', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
const user = await userApi.create({ username, password });
await createUserMfaVerification(user.id, MfaFactor.TOTP);

const client = await initExperienceClient();
await identifyUserWithUsernamePassword(client, username, password);

await expectRejects(
client.updateProfile({ type: SignInIdentifier.Username, value: 'username' }),
{
status: 403,
code: 'session.mfa.require_mfa_verification',
}
);
});

it('should update the profile successfully if the mfa is enabled and verified', async () => {
const { username, password } = generateNewUserProfile({ username: true, password: true });
const user = await userApi.create({ username, password });

const client = await initExperienceClient();
await identifyUserWithUsernamePassword(client, username, password);

const response = await createUserMfaVerification(user.id, MfaFactor.TOTP);

if (response.type !== MfaFactor.TOTP) {
throw new Error('unexpected mfa type');
}

const { secret } = response;
const code = authenticator.generate(secret);

await successfullyVerifyTotp(client, { code });

const email = generateEmail();

const { verificationId, code: verificationCode } = await successfullySendVerificationCode(
client,
{
identifier: { type: SignInIdentifier.Email, value: email },
interactionEvent: InteractionEvent.SignIn,
}
);

await successfullyVerifyVerificationCode(client, {
identifier: { type: SignInIdentifier.Email, value: email },
verificationId,
code: verificationCode,
});

await expect(
client.updateProfile({ type: SignInIdentifier.Email, verificationId })
).resolves.not.toThrow();
});
});
});
Loading

0 comments on commit 9d2770c

Please sign in to comment.