0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

fix(core): checkRequiredProfile should work correctly when sign up identifier is emailOrSms (#2572)

This commit is contained in:
Xiao Yijun 2022-12-02 15:08:43 +08:00 committed by GitHub
parent d1427e56a0
commit aafae8e02b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 186 additions and 14 deletions

View file

@ -7,7 +7,7 @@ import { Provider } from 'oidc-provider';
import { mockSignInExperience, mockSignInMethod, mockUser } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { signInWithPassword } from './utils.js';
import { checkRequiredProfile, signInWithPassword } from './utils.js';
const insertUser = jest.fn(async (..._args: unknown[]) => ({ id: 'id' }));
const findUserById = jest.fn(async (): Promise<User> => mockUser);
@ -108,6 +108,178 @@ afterEach(() => {
interactionResult.mockClear();
});
describe('checkRequiredProfile', () => {
// eslint-disable-next-line @silverhand/fp/no-let
let mockDate: jest.SpyInstance;
const mockedExpiredAt = '2022-02-02';
beforeEach(() => {
interactionDetails.mockResolvedValueOnce({ params: {} });
// eslint-disable-next-line @silverhand/fp/no-mutation
mockDate = jest.spyOn(Date.prototype, 'toISOString').mockReturnValue(mockedExpiredAt);
});
afterEach(() => {
mockDate.mockRestore();
});
it("throw if password is required but the user's password is not set", async () => {
const user = {
...mockUser,
passwordEncrypted: null,
passwordEncryptionMethod: null,
identities: {},
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
password: true,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).rejects.toThrowError(new RequestError({ code: 'user.require_password', status: 422 }));
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ continueSignIn: { userId: user.id, expiresAt: mockedExpiredAt } })
);
});
it("throw if the sign up identifier is ['username'] but the user's username is missing", async () => {
const user = {
...mockUser,
username: null,
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifiers: [SignInIdentifier.Username],
password: true,
verify: false,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).rejects.toThrowError(new RequestError({ code: 'user.require_username', status: 422 }));
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ continueSignIn: { userId: user.id, expiresAt: mockedExpiredAt } })
);
});
it("throw if the sign up identifier is ['email'] but the user's email is missing", async () => {
const user = {
...mockUser,
primaryEmail: null,
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifiers: [SignInIdentifier.Email],
password: true,
verify: true,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).rejects.toThrowError(new RequestError({ code: 'user.require_email', status: 422 }));
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ continueSignIn: { userId: user.id, expiresAt: mockedExpiredAt } })
);
});
it("throw if the sign up identifier is ['sms'] but the user's phone is missing", async () => {
const user = {
...mockUser,
primaryPhone: null,
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifiers: [SignInIdentifier.Sms],
password: true,
verify: true,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).rejects.toThrowError(new RequestError({ code: 'user.require_sms', status: 422 }));
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ continueSignIn: { userId: user.id, expiresAt: mockedExpiredAt } })
);
});
it("throw if the sign up identifier is ['email', 'sms'] but the user's email and phone are missing", async () => {
const user = {
...mockUser,
primaryEmail: null,
primaryPhone: null,
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
password: true,
verify: true,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).rejects.toThrowError(new RequestError({ code: 'user.require_email_or_sms', status: 422 }));
expect(interactionResult).toHaveBeenCalledWith(
expect.anything(),
expect.anything(),
expect.objectContaining({ continueSignIn: { userId: user.id, expiresAt: mockedExpiredAt } })
);
});
it.each([{ primaryEmail: null }, { primaryPhone: null }])(
"check successfully if the sign up identifier is ['email', 'sms'] and the user has an email or phone",
async (userProfile) => {
const user = {
...mockUser,
...userProfile,
};
const signInExperience = {
...mockSignInExperience,
signUp: {
...mockSignInExperience.signUp,
identifiers: [SignInIdentifier.Email, SignInIdentifier.Sms],
password: true,
verify: true,
},
};
await expect(
checkRequiredProfile(createContext(), createProvider(), user, signInExperience)
).resolves.not.toThrow();
expect(interactionResult).not.toBeCalled();
}
);
});
describe('signInWithPassword()', () => {
it('assign result', async () => {
interactionDetails.mockResolvedValueOnce({ params: {} });

View file

@ -1,6 +1,7 @@
import type { LogPayload, LogType, PasscodeType, SignInExperience, User } from '@logto/schemas';
import { SignInIdentifier, logTypeGuard } from '@logto/schemas';
import type { Nullable, Truthy } from '@silverhand/essentials';
import { isSameArray } from '@silverhand/essentials';
import { addSeconds, isAfter, isValid } from 'date-fns';
import type { Context } from 'koa';
import type { Provider } from 'oidc-provider';
@ -169,30 +170,29 @@ export const checkRequiredProfile = async (
throw new RequestError({ code: 'user.require_password', status: 422 });
}
if (signUp.identifiers.includes(SignInIdentifier.Username) && !username) {
if (isSameArray(signUp.identifiers, [SignInIdentifier.Username]) && !username) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_username', status: 422 });
}
if (isSameArray(signUp.identifiers, [SignInIdentifier.Email]) && !primaryEmail) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email', status: 422 });
}
if (isSameArray(signUp.identifiers, [SignInIdentifier.Sms]) && !primaryPhone) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_sms', status: 422 });
}
if (
signUp.identifiers.includes(SignInIdentifier.Email) &&
signUp.identifiers.includes(SignInIdentifier.Sms) &&
isSameArray(signUp.identifiers, [SignInIdentifier.Email, SignInIdentifier.Sms]) &&
!primaryEmail &&
!primaryPhone
) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email_or_sms', status: 422 });
}
if (signUp.identifiers.includes(SignInIdentifier.Email) && !primaryEmail) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_email', status: 422 });
}
if (signUp.identifiers.includes(SignInIdentifier.Sms) && !primaryPhone) {
await assignContinueSignInResult(ctx, provider, { userId: id });
throw new RequestError({ code: 'user.require_sms', status: 422 });
}
};
/* eslint-enable complexity */