mirror of
https://github.com/logto-io/logto.git
synced 2025-01-13 21:30:30 -05:00
refactor(core): make password optional in NewPasswordIdentity (#6377)
refactor(core): make password optional in NewPasswordIdentity verification make password optioanl in NewPasswordIdentity verification
This commit is contained in:
parent
323a5650f0
commit
a1f6009cc5
8 changed files with 170 additions and 167 deletions
|
@ -483,8 +483,6 @@ export default class ExperienceInteraction {
|
||||||
const newProfile = await getNewUserProfileFromVerificationRecord(verificationRecord);
|
const newProfile = await getNewUserProfileFromVerificationRecord(verificationRecord);
|
||||||
await this.profile.profileValidator.guardProfileUniquenessAcrossUsers(newProfile);
|
await this.profile.profileValidator.guardProfileUniquenessAcrossUsers(newProfile);
|
||||||
|
|
||||||
await this.signInExperienceValidator.guardMandatoryPasswordOnRegister(verificationRecord);
|
|
||||||
|
|
||||||
const user = await this.provisionLibrary.createUser(newProfile);
|
const user = await this.provisionLibrary.createUser(newProfile);
|
||||||
|
|
||||||
this.userId = user.id;
|
this.userId = user.id;
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
|
import { type LogtoErrorCode } from '@logto/phrases';
|
||||||
import {
|
import {
|
||||||
InteractionEvent,
|
InteractionEvent,
|
||||||
type SignInExperience,
|
type SignInExperience,
|
||||||
|
@ -339,7 +340,11 @@ describe('SignInExperienceValidator', () => {
|
||||||
string,
|
string,
|
||||||
{
|
{
|
||||||
signInExperience: SignInExperience;
|
signInExperience: SignInExperience;
|
||||||
cases: Array<{ verificationRecord: VerificationRecord; accepted: boolean }>;
|
cases: Array<{
|
||||||
|
verificationRecord: VerificationRecord;
|
||||||
|
accepted: boolean;
|
||||||
|
errorCode?: LogtoErrorCode;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
> = Object.freeze({
|
> = Object.freeze({
|
||||||
'only username is enabled for sign-up': {
|
'only username is enabled for sign-up': {
|
||||||
|
@ -364,7 +369,7 @@ describe('SignInExperienceValidator', () => {
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
signUp: {
|
signUp: {
|
||||||
identifiers: [SignInIdentifier.Email],
|
identifiers: [SignInIdentifier.Email],
|
||||||
password: true,
|
password: false,
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -377,6 +382,10 @@ describe('SignInExperienceValidator', () => {
|
||||||
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone],
|
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone],
|
||||||
accepted: false,
|
accepted: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
verificationRecord: emailNewPasswordIdentityVerificationRecord,
|
||||||
|
accepted: false,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
'email and phone are enabled for sign-up': {
|
'email and phone are enabled for sign-up': {
|
||||||
|
@ -384,7 +393,7 @@ describe('SignInExperienceValidator', () => {
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
signUp: {
|
signUp: {
|
||||||
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||||
password: true,
|
password: false,
|
||||||
verify: true,
|
verify: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -399,6 +408,27 @@ describe('SignInExperienceValidator', () => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
'email are enabled for sign-up but password is required': {
|
||||||
|
signInExperience: {
|
||||||
|
...mockSignInExperience,
|
||||||
|
signUp: {
|
||||||
|
identifiers: [SignInIdentifier.Email],
|
||||||
|
password: true,
|
||||||
|
verify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
cases: [
|
||||||
|
{
|
||||||
|
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email],
|
||||||
|
accepted: false,
|
||||||
|
errorCode: 'user.password_required_in_profile',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
verificationRecord: emailNewPasswordIdentityVerificationRecord,
|
||||||
|
accepted: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
'enterprise sso enabled': {
|
'enterprise sso enabled': {
|
||||||
signInExperience: {
|
signInExperience: {
|
||||||
...mockSignInExperience,
|
...mockSignInExperience,
|
||||||
|
@ -428,30 +458,36 @@ describe('SignInExperienceValidator', () => {
|
||||||
describe.each(Object.keys(registerVerificationTestCases))(`%s`, (testCase) => {
|
describe.each(Object.keys(registerVerificationTestCases))(`%s`, (testCase) => {
|
||||||
const { signInExperience, cases } = registerVerificationTestCases[testCase]!;
|
const { signInExperience, cases } = registerVerificationTestCases[testCase]!;
|
||||||
|
|
||||||
it.each(cases)('guard verification record %p', async ({ verificationRecord, accepted }) => {
|
it.each(cases)(
|
||||||
signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience);
|
'guard verification record %p',
|
||||||
|
async ({ verificationRecord, accepted, errorCode }) => {
|
||||||
|
signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience);
|
||||||
|
|
||||||
const signInExperienceSettings = new SignInExperienceValidator(
|
const signInExperienceSettings = new SignInExperienceValidator(
|
||||||
mockTenant.libraries,
|
mockTenant.libraries,
|
||||||
mockTenant.queries
|
mockTenant.queries
|
||||||
);
|
);
|
||||||
|
|
||||||
await (accepted
|
await (accepted
|
||||||
? expect(
|
? expect(
|
||||||
signInExperienceSettings.verifyIdentificationMethod(
|
signInExperienceSettings.verifyIdentificationMethod(
|
||||||
InteractionEvent.Register,
|
InteractionEvent.Register,
|
||||||
verificationRecord
|
verificationRecord
|
||||||
)
|
)
|
||||||
).resolves.not.toThrow()
|
).resolves.not.toThrow()
|
||||||
: expect(
|
: expect(
|
||||||
signInExperienceSettings.verifyIdentificationMethod(
|
signInExperienceSettings.verifyIdentificationMethod(
|
||||||
InteractionEvent.Register,
|
InteractionEvent.Register,
|
||||||
verificationRecord
|
verificationRecord
|
||||||
)
|
)
|
||||||
).rejects.toMatchError(
|
).rejects.toMatchError(
|
||||||
new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 })
|
new RequestError({
|
||||||
));
|
code: errorCode ?? 'user.sign_up_method_not_enabled',
|
||||||
});
|
status: 422,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -518,119 +554,5 @@ describe('SignInExperienceValidator', () => {
|
||||||
).rejects.toMatchError(expectError);
|
).rejects.toMatchError(expectError);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('guardMandatoryPasswordOnRegister', () => {
|
|
||||||
const testCases: Record<
|
|
||||||
string,
|
|
||||||
{
|
|
||||||
signInExperience: SignInExperience;
|
|
||||||
cases: Array<{ verificationRecord: VerificationRecord; accepted: boolean }>;
|
|
||||||
}
|
|
||||||
> = Object.freeze({
|
|
||||||
'should throw error for CodeVerification Records if password is required': {
|
|
||||||
signInExperience: {
|
|
||||||
...mockSignInExperience,
|
|
||||||
signUp: {
|
|
||||||
identifiers: [SignInIdentifier.Email],
|
|
||||||
password: true,
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email],
|
|
||||||
accepted: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone],
|
|
||||||
accepted: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'should not throw error for CodeVerification Records if password is not required': {
|
|
||||||
signInExperience: {
|
|
||||||
...mockSignInExperience,
|
|
||||||
signUp: {
|
|
||||||
identifiers: [SignInIdentifier.Email],
|
|
||||||
password: false,
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Email],
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
verificationRecord: verificationCodeVerificationRecords[SignInIdentifier.Phone],
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'should not throw error for NewPasswordIdentity verification record': {
|
|
||||||
signInExperience: {
|
|
||||||
...mockSignInExperience,
|
|
||||||
signUp: {
|
|
||||||
identifiers: [SignInIdentifier.Username],
|
|
||||||
password: true,
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
verificationRecord: newPasswordIdentityVerificationRecord,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
verificationRecord: emailNewPasswordIdentityVerificationRecord,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
'should not throw error for Social and SSO verification records': {
|
|
||||||
signInExperience: {
|
|
||||||
...mockSignInExperience,
|
|
||||||
signUp: {
|
|
||||||
identifiers: [SignInIdentifier.Email],
|
|
||||||
password: true,
|
|
||||||
verify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
cases: [
|
|
||||||
{
|
|
||||||
verificationRecord: socialVerificationRecord,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
verificationRecord: enterpriseSsoVerificationRecords,
|
|
||||||
accepted: true,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
describe.each(Object.keys(testCases))(`%s`, (testCase) => {
|
|
||||||
const { signInExperience, cases } = testCases[testCase]!;
|
|
||||||
|
|
||||||
it.each(cases)('guard verification record %p', async ({ verificationRecord, accepted }) => {
|
|
||||||
signInExperiences.findDefaultSignInExperience.mockResolvedValueOnce(signInExperience);
|
|
||||||
|
|
||||||
const signInExperienceSettings = new SignInExperienceValidator(
|
|
||||||
mockTenant.libraries,
|
|
||||||
mockTenant.queries
|
|
||||||
);
|
|
||||||
|
|
||||||
await (accepted
|
|
||||||
? expect(
|
|
||||||
signInExperienceSettings.guardMandatoryPasswordOnRegister(verificationRecord)
|
|
||||||
).resolves.not.toThrow()
|
|
||||||
: expect(
|
|
||||||
signInExperienceSettings.guardMandatoryPasswordOnRegister(verificationRecord)
|
|
||||||
).rejects.toMatchError(
|
|
||||||
new RequestError({ code: 'user.password_required_in_profile', status: 422 })
|
|
||||||
));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
/* eslint-enable max-lines */
|
/* eslint-enable max-lines */
|
||||||
|
|
|
@ -162,25 +162,6 @@ export class SignInExperienceValidator {
|
||||||
return mandatoryUserProfile;
|
return mandatoryUserProfile;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* If password is enabled in the sign-up settings,
|
|
||||||
* guard the verification record contains password (NewPasswordIdentity).
|
|
||||||
*
|
|
||||||
* - Password is not required for social and SSO verification records.
|
|
||||||
*/
|
|
||||||
public async guardMandatoryPasswordOnRegister({ type }: VerificationRecord) {
|
|
||||||
const { signUp } = await this.getSignInExperienceData();
|
|
||||||
|
|
||||||
if (
|
|
||||||
signUp.password &&
|
|
||||||
[VerificationType.EmailVerificationCode, VerificationType.PhoneVerificationCode].includes(
|
|
||||||
type
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new RequestError({ code: 'user.password_required_in_profile', status: 422 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guard the verification records contains email identifier with SSO enabled
|
* Guard the verification records contains email identifier with SSO enabled
|
||||||
*
|
*
|
||||||
|
@ -283,6 +264,11 @@ export class SignInExperienceValidator {
|
||||||
signUp.identifiers.includes(type) && signUp.verify,
|
signUp.identifiers.includes(type) && signUp.verify,
|
||||||
new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 })
|
new RequestError({ code: 'user.sign_up_method_not_enabled', status: 422 })
|
||||||
);
|
);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
!signUp.password,
|
||||||
|
new RequestError({ code: 'user.password_required_in_profile', status: 422 })
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case VerificationType.Social: {
|
case VerificationType.Social: {
|
||||||
|
|
|
@ -93,12 +93,21 @@ export class NewPasswordIdentityVerification
|
||||||
*
|
*
|
||||||
* - Check if the identifier is unique across users
|
* - Check if the identifier is unique across users
|
||||||
* - Validate the password against the password policy
|
* - Validate the password against the password policy
|
||||||
|
*
|
||||||
|
* @throws {RequestError} with status 422 if the identifier is in use by another user
|
||||||
|
* @throws {RequestError} with status 422 if the password is not provided
|
||||||
|
* @throws {RequestError} with status 422 if the password does not meet the password policy
|
||||||
*/
|
*/
|
||||||
async verify(password: string) {
|
async verify(password?: string) {
|
||||||
const { identifier } = this;
|
const { identifier } = this;
|
||||||
const identifierProfile = interactionIdentifierToUserProfile(identifier);
|
const identifierProfile = interactionIdentifierToUserProfile(identifier);
|
||||||
await this.profileValidator.guardProfileUniquenessAcrossUsers(identifierProfile);
|
await this.profileValidator.guardProfileUniquenessAcrossUsers(identifierProfile);
|
||||||
|
|
||||||
|
assertThat(
|
||||||
|
password,
|
||||||
|
new RequestError({ code: 'user.password_required_in_profile', status: 422 })
|
||||||
|
);
|
||||||
|
|
||||||
const passwordPolicy = await this.signInExperienceValidator.getPasswordPolicy();
|
const passwordPolicy = await this.signInExperienceValidator.getPasswordPolicy();
|
||||||
const passwordValidator = new PasswordValidator(passwordPolicy);
|
const passwordValidator = new PasswordValidator(passwordPolicy);
|
||||||
await passwordValidator.validatePassword(password, identifierProfile);
|
await passwordValidator.validatePassword(password, identifierProfile);
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { passwordVerificationPayloadGuard, VerificationType } from '@logto/schemas';
|
import { interactionIdentifierGuard, VerificationType } from '@logto/schemas';
|
||||||
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
|
import { Action } from '@logto/schemas/lib/types/log/interaction.js';
|
||||||
import type Router from 'koa-router';
|
import type Router from 'koa-router';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
@ -17,7 +17,10 @@ export default function newPasswordIdentityVerificationRoutes<
|
||||||
router.post(
|
router.post(
|
||||||
`${experienceRoutes.verification}/new-password-identity`,
|
`${experienceRoutes.verification}/new-password-identity`,
|
||||||
koaGuard({
|
koaGuard({
|
||||||
body: passwordVerificationPayloadGuard,
|
body: z.object({
|
||||||
|
identifier: interactionIdentifierGuard,
|
||||||
|
password: z.string().optional(),
|
||||||
|
}),
|
||||||
status: [200, 400, 422],
|
status: [200, 400, 422],
|
||||||
response: z.object({
|
response: z.object({
|
||||||
verificationId: z.string(),
|
verificationId: z.string(),
|
||||||
|
|
|
@ -195,7 +195,9 @@ export class ExperienceClient extends MockClient {
|
||||||
.json<{ verificationId: string }>();
|
.json<{ verificationId: string }>();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createNewPasswordIdentityVerification(payload: PasswordVerificationPayload) {
|
public async createNewPasswordIdentityVerification(
|
||||||
|
payload: Pick<PasswordVerificationPayload, 'identifier'> & { password?: string }
|
||||||
|
) {
|
||||||
return api
|
return api
|
||||||
.post(`${experienceRoutes.verification}/new-password-identity`, {
|
.post(`${experienceRoutes.verification}/new-password-identity`, {
|
||||||
headers: { cookie: this.interactionCookie },
|
headers: { cookie: this.interactionCookie },
|
||||||
|
|
|
@ -52,13 +52,13 @@ devFeatureTest.describe('Register interaction with verification code happy path'
|
||||||
await Promise.all([setEmailConnector(), setSmsConnector()]);
|
await Promise.all([setEmailConnector(), setSmsConnector()]);
|
||||||
await enableAllVerificationCodeSignInMethods({
|
await enableAllVerificationCodeSignInMethods({
|
||||||
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||||
password: true,
|
password: false,
|
||||||
verify: true,
|
verify: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each(verificationIdentifierType)(
|
it.each(verificationIdentifierType)(
|
||||||
'Should fail to sign-up with existing %p identifier and directly sign-in instead ',
|
'Should fail to sign-up with existing %p identifier and directly sign-in instead',
|
||||||
async (identifierType) => {
|
async (identifierType) => {
|
||||||
const { userProfile, user } = await generateNewUser({
|
const { userProfile, user } = await generateNewUser({
|
||||||
[identifiersTypeToUserProfile[identifierType]]: true,
|
[identifiersTypeToUserProfile[identifierType]]: true,
|
||||||
|
@ -110,7 +110,7 @@ devFeatureTest.describe('Register interaction with verification code happy path'
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('fulfill password', () => {
|
describe('password enabled', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await enableAllVerificationCodeSignInMethods({
|
await enableAllVerificationCodeSignInMethods({
|
||||||
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
identifiers: [SignInIdentifier.Email, SignInIdentifier.Phone],
|
||||||
|
@ -163,6 +163,68 @@ devFeatureTest.describe('Register interaction with verification code happy path'
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
password: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const identifier: VerificationCodeIdentifier = {
|
||||||
|
type: identifierType,
|
||||||
|
value: userProfile[identifiersTypeToUserProfile[identifierType]]!,
|
||||||
|
};
|
||||||
|
|
||||||
|
const client = await initExperienceClient(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.password_required_in_profile`,
|
||||||
|
status: 422,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectRejects(
|
||||||
|
client.createNewPasswordIdentityVerification({
|
||||||
|
identifier,
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
it.each(verificationIdentifierType)(
|
it.each(verificationIdentifierType)(
|
||||||
'Should register with verification code using %p and fulfill the password successfully',
|
'Should register with verification code using %p and fulfill the password successfully',
|
||||||
async (identifier) => {
|
async (identifier) => {
|
||||||
|
|
|
@ -119,6 +119,27 @@ devFeatureTest.describe('password verifications', () => {
|
||||||
[username, 'userInfo'],
|
[username, 'userInfo'],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
it('should throw error if password is not provided', async () => {
|
||||||
|
const { primaryEmail } = generateNewUserProfile({
|
||||||
|
primaryEmail: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = await initExperienceClient();
|
||||||
|
|
||||||
|
await expectRejects(
|
||||||
|
client.createNewPasswordIdentityVerification({
|
||||||
|
identifier: {
|
||||||
|
type: SignInIdentifier.Email,
|
||||||
|
value: primaryEmail,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
code: 'user.password_required_in_profile',
|
||||||
|
status: 422,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it.each(invalidPasswords)('should reject invalid password %p', async (password) => {
|
it.each(invalidPasswords)('should reject invalid password %p', async (password) => {
|
||||||
const client = await initExperienceClient();
|
const client = await initExperienceClient();
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue