0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

feat(test): add password and passcode identifier tests (#2664)

This commit is contained in:
simeng-li 2022-12-15 11:38:15 +08:00 committed by GitHub
parent 5b647cf7cb
commit 6bead4a319
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 522 additions and 38 deletions

View file

@ -12,9 +12,10 @@
"scripts": {
"build": "rm -rf lib/ && tsc -p tsconfig.test.json --sourcemap",
"test:only": "NODE_OPTIONS=--experimental-vm-modules jest",
"test": "pnpm build && pnpm test:api && pnpm test:ui",
"test": "pnpm build && pnpm test:api && pnpm test:ui && pnpm test:interaction",
"test:api": "pnpm test:only -i ./lib/tests/api",
"test:ui": "pnpm test:only -i --config=jest.config.ui.js ./lib/tests/ui",
"test:interaction": "pnpm test:only -i ./lib/tests/interaction",
"lint": "eslint --ext .ts src",
"lint:report": "pnpm lint --format json --output-file report.json",
"start": "pnpm test"

View file

@ -1,4 +1,4 @@
import type { Connector, ConnectorResponse } from '@logto/schemas';
import type { Connector, ConnectorResponse, CreateConnector } from '@logto/schemas';
import { authedAdminApi } from './api.js';
@ -9,11 +9,13 @@ export const getConnector = async (connectorId: string) =>
authedAdminApi.get(`connectors/${connectorId}`).json<ConnectorResponse>();
// FIXME @Darcy: correct use of `id` and `connectorId`.
export const postConnector = async (connectorId: string, metadata?: Record<string, unknown>) =>
export const postConnector = async (
payload: Pick<CreateConnector, 'connectorId' | 'config' | 'metadata' | 'syncProfile'>
) =>
authedAdminApi
.post({
url: `connectors`,
json: { connectorId, metadata },
json: payload,
})
.json<Connector>();

View file

@ -1,11 +1,4 @@
import { Event } from '@logto/schemas';
import type {
IdentifierPayload,
PhonePasswordPayload,
EmailPasswordPayload,
Profile,
UsernamePasswordPayload,
} from '@logto/schemas';
import type { Event, IdentifierPayload, Profile } from '@logto/schemas';
import api from './api.js';
@ -19,19 +12,37 @@ export type interactionPayload = {
profile?: Profile;
};
export const signInWithPasswordIdentifiers = async (
identifier: UsernamePasswordPayload | EmailPasswordPayload | PhonePasswordPayload,
cookie: string
) =>
export const putInteraction = async (payload: interactionPayload, cookie: string) =>
api
.put('interaction', {
headers: {
cookie,
},
json: {
event: Event.SignIn,
identifier,
},
headers: { cookie },
json: payload,
followRedirect: false,
})
.json<RedirectResponse>();
export const patchInteraction = async (payload: interactionPayload, cookie: string) =>
api
.patch('interaction', {
headers: { cookie },
json: payload,
followRedirect: false,
})
.json<RedirectResponse>();
export type VerificationPasscodePayload =
| {
event: Event;
email: string;
}
| { event: Event; phone: string };
export const sendVerificationPasscode = async (
payload: VerificationPasscodePayload,
cookie: string
) =>
api.post('verification/passcode', {
headers: { cookie },
json: payload,
followRedirect: false,
});

View file

@ -69,7 +69,8 @@ describe('admin console user management', () => {
});
it('should delete user identities successfully', async () => {
const { id } = await postConnector(mockSocialConnectorId);
// @darcy FIXME: merge post and update
const { id } = await postConnector({ connectorId: mockSocialConnectorId });
await updateConnectorConfig(id, mockSocialConnectorConfig);
const createdUserId = await bindSocialToNewCreatedUser(id);

View file

@ -45,7 +45,8 @@ test('connector set-up flow', async () => {
{ connectorId: mockEmailConnectorId, config: mockEmailConnectorConfig },
{ connectorId: mockSocialConnectorId, config: mockSocialConnectorConfig },
].map(async ({ connectorId, config }) => {
const { id } = await postConnector(connectorId);
// @darcy FIXME: should call post method directly
const { id } = await postConnector({ connectorId });
connectorIdMap.set(connectorId, id);
const updatedConnector = await updateConnectorConfig(id, config);
expect(updatedConnector.config).toEqual(config);
@ -71,12 +72,14 @@ test('connector set-up flow', async () => {
/*
* Change to another SMS/Email connector
*/
const { id } = await postConnector(mockStandardEmailConnectorId, {
target: 'mock-standard-mail',
}); // TODO [LOG-4862]: update mock connector
// @darcy FIXME: should call post method directly
const { id } = await postConnector({
connectorId: mockStandardEmailConnectorId,
metadata: { target: 'mock-standard-mail' },
});
await updateConnectorConfig(id, mockStandardEmailConnectorConfig, {
target: 'mock-standard-mail',
}); // TODO [LOG-4862]: update mock connector
});
connectorIdMap.set(mockStandardEmailConnectorId, id);
const currentConnectors = await listConnectors();
expect(
@ -133,7 +136,7 @@ test('send SMS/email test message', async () => {
await Promise.all(
[{ connectorId: mockSmsConnectorId }, { connectorId: mockEmailConnectorId }].map(
async ({ connectorId }) => {
const { id } = await postConnector(connectorId);
const { id } = await postConnector({ connectorId });
connectorIdMap.set(connectorId, id);
}
)

View file

@ -68,11 +68,7 @@ describe('email and password flow', () => {
assert(localPart && domain, new Error('Email address local part or domain is empty'));
beforeAll(async () => {
const { id } = await postConnector(mockEmailConnectorId);
await updateConnectorConfig(id, mockEmailConnectorConfig);
connectorIdMap.set(mockEmailConnectorId, id);
await setSignUpIdentifier(signUpIdentifiers.email, true);
await setSignUpIdentifier(signUpIdentifiers.none, true);
await setSignInMethod([
{
identifier: SignInIdentifier.Email,
@ -109,7 +105,7 @@ describe('email passwordless flow', () => {
);
connectorIdMap.clear();
const { id } = await postConnector(mockEmailConnectorId);
const { id } = await postConnector({ connectorId: mockEmailConnectorId });
await updateConnectorConfig(id, mockEmailConnectorConfig);
connectorIdMap.set(mockEmailConnectorId, id);
@ -208,7 +204,7 @@ describe('sms passwordless flow', () => {
);
connectorIdMap.clear();
const { id } = await postConnector(mockSmsConnectorId);
const { id } = await postConnector({ connectorId: mockSmsConnectorId });
await updateConnectorConfig(id, mockSmsConnectorConfig);
connectorIdMap.set(mockSmsConnectorId, id);

View file

@ -32,7 +32,7 @@ describe('social sign-in and register', () => {
const socialUserId = crypto.randomUUID();
beforeAll(async () => {
const { id } = await postConnector(mockSocialConnectorId);
const { id } = await postConnector({ connectorId: mockSocialConnectorId });
connectorIdMap.set(mockSocialConnectorId, id);
await updateConnectorConfig(id, mockSocialConnectorConfig);

View file

@ -0,0 +1,213 @@
import { ConnectorType, Event, SignInIdentifier } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import {
sendVerificationPasscode,
putInteraction,
patchInteraction,
deleteUser,
updateSignInExperience,
} from '#src/api/index.js';
import { readPasscode } from '#src/helpers.js';
import { generateEmail, generatePhone } from '#src/utils.js';
import { initClient, processSessionAndLogout } from './utils/client.js';
import { clearConnectorsByTypes, setEmailConnector, setSmsConnector } from './utils/connector.js';
import { enableAllPasscodeSignInMethods } from './utils/sign-in-experience.js';
import { generateNewUser } from './utils/user.js';
describe('Sign-In flow using passcode identifiers', () => {
beforeAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
await setEmailConnector();
await setSmsConnector();
await enableAllPasscodeSignInMethods();
});
afterAll(async () => {
await clearConnectorsByTypes([ConnectorType.Email, ConnectorType.Sms]);
});
it('sign-in with email and passcode', async () => {
const { userProfile, user } = await generateNewUser({ primaryEmail: true });
const client = await initClient();
assert(client.interactionCookie, new Error('Session not found'));
await expect(
sendVerificationPasscode(
{
event: Event.SignIn,
email: userProfile.primaryEmail,
},
client.interactionCookie
)
).resolves.not.toThrow();
const passcodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
address: userProfile.primaryEmail,
type: Event.SignIn,
});
const { code } = passcodeRecord;
const { redirectTo } = await putInteraction(
{
event: Event.SignIn,
identifier: {
email: userProfile.primaryEmail,
passcode: code,
},
},
client.interactionCookie
);
await processSessionAndLogout(client, redirectTo);
await deleteUser(user.id);
});
it('sign-in with phone and passcode', async () => {
const { userProfile, user } = await generateNewUser({ primaryPhone: true });
const client = await initClient();
assert(client.interactionCookie, new Error('Session not found'));
await expect(
sendVerificationPasscode(
{
event: Event.SignIn,
phone: userProfile.primaryPhone,
},
client.interactionCookie
)
).resolves.not.toThrow();
const passcodeRecord = await readPasscode();
expect(passcodeRecord).toMatchObject({
phone: userProfile.primaryPhone,
type: Event.SignIn,
});
const { code } = passcodeRecord;
const { redirectTo } = await putInteraction(
{
event: Event.SignIn,
identifier: {
phone: userProfile.primaryPhone,
passcode: code,
},
},
client.interactionCookie
);
await processSessionAndLogout(client, redirectTo);
await deleteUser(user.id);
});
it('sign-in with non-exist email account with passcode', async () => {
const newEmail = generateEmail();
// Enable email sign-up
await updateSignInExperience({
signUp: { identifiers: [SignInIdentifier.Email], password: false, verify: true },
});
const client = await initClient();
assert(client.interactionCookie, new Error('Session not found'));
await expect(
sendVerificationPasscode(
{
event: Event.SignIn,
email: newEmail,
},
client.interactionCookie
)
).resolves.not.toThrow();
const passcodeRecord = await readPasscode();
const { code } = passcodeRecord;
// TODO: @simeng use expectRequestError after https://github.com/logto-io/logto/pull/2639/ PR merged
await expect(
putInteraction(
{
event: Event.SignIn,
identifier: {
email: newEmail,
passcode: code,
},
},
client.interactionCookie
)
).rejects.toThrow();
const { redirectTo } = await patchInteraction(
{
event: Event.Register,
profile: {
email: newEmail,
},
},
client.interactionCookie
);
await processSessionAndLogout(client, redirectTo);
});
it('sign-in with non-exist phone account with passcode', async () => {
const newPhone = generatePhone();
// Enable phone sign-up
await updateSignInExperience({
signUp: { identifiers: [SignInIdentifier.Sms], password: false, verify: true },
});
const client = await initClient();
assert(client.interactionCookie, new Error('Session not found'));
await expect(
sendVerificationPasscode(
{
event: Event.SignIn,
phone: newPhone,
},
client.interactionCookie
)
).resolves.not.toThrow();
const passcodeRecord = await readPasscode();
const { code } = passcodeRecord;
// TODO: @simeng use expectRequestError after https://github.com/logto-io/logto/pull/2639/ PR merged
await expect(
putInteraction(
{
event: Event.SignIn,
identifier: {
phone: newPhone,
passcode: code,
},
},
client.interactionCookie
)
).rejects.toThrow();
const { redirectTo } = await patchInteraction(
{
event: Event.Register,
profile: {
phone: newPhone,
},
},
client.interactionCookie
);
await processSessionAndLogout(client, redirectTo);
});
});

View file

@ -0,0 +1,98 @@
import { Event } from '@logto/schemas';
import { assert } from '@silverhand/essentials';
import { putInteraction, deleteUser } from '#src/api/index.js';
import MockClient from '#src/client/index.js';
import { enableAllPasswordSignInMethods } from './utils/sign-in-experience.js';
import { generateNewUser } from './utils/user.js';
describe('Sign-In flow using password identifiers', () => {
beforeAll(async () => {
await enableAllPasswordSignInMethods();
});
it('sign-in with username and password', async () => {
const { userProfile, user } = await generateNewUser({ username: true });
const client = new MockClient();
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await putInteraction(
{
event: Event.SignIn,
identifier: {
username: userProfile.username,
password: userProfile.password,
},
},
client.interactionCookie
);
await client.processSession(redirectTo);
await expect(client.isAuthenticated()).resolves.toBe(true);
await client.signOut();
await expect(client.isAuthenticated()).resolves.toBe(false);
await deleteUser(user.id);
});
it('sign-in with email and password', async () => {
const { userProfile, user } = await generateNewUser({ primaryEmail: true });
const client = new MockClient();
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await putInteraction(
{
event: Event.SignIn,
identifier: {
email: userProfile.primaryEmail,
password: userProfile.password,
},
},
client.interactionCookie
);
await client.processSession(redirectTo);
await expect(client.isAuthenticated()).resolves.toBe(true);
await client.signOut();
await expect(client.isAuthenticated()).resolves.toBe(false);
await deleteUser(user.id);
});
it('sign-in with phone and password', async () => {
const { userProfile, user } = await generateNewUser({ primaryPhone: true });
const client = new MockClient();
await client.initSession();
assert(client.interactionCookie, new Error('Session not found'));
const { redirectTo } = await putInteraction(
{
event: Event.SignIn,
identifier: {
phone: userProfile.primaryPhone,
password: userProfile.password,
},
},
client.interactionCookie
);
await client.processSession(redirectTo);
await expect(client.isAuthenticated()).resolves.toBe(true);
await client.signOut();
await expect(client.isAuthenticated()).resolves.toBe(false);
await deleteUser(user.id);
});
});

View file

@ -0,0 +1,18 @@
import MockClient from '#src/client/index.js';
export const initClient = async () => {
const client = new MockClient();
await client.initSession();
return client;
};
export const processSessionAndLogout = async (client: MockClient, redirectTo: string) => {
await client.processSession(redirectTo);
await expect(client.isAuthenticated()).resolves.toBe(true);
await client.signOut();
await expect(client.isAuthenticated()).resolves.toBe(false);
};

View file

@ -0,0 +1,27 @@
import type { ConnectorType } from '@logto/schemas';
import {
mockEmailConnectorConfig,
mockEmailConnectorId,
mockSmsConnectorConfig,
mockSmsConnectorId,
} from '#src/__mocks__/connectors-mock.js';
import { listConnectors, deleteConnectorById, postConnector } from '#src/api/index.js';
export const clearConnectorsByTypes = async (types: ConnectorType[]) => {
const connectors = await listConnectors();
const targetConnectors = connectors.filter((connector) => types.includes(connector.type));
await Promise.all(targetConnectors.map(async (connector) => deleteConnectorById(connector.id)));
};
export const setEmailConnector = async () =>
postConnector({
connectorId: mockEmailConnectorId,
config: mockEmailConnectorConfig,
});
export const setSmsConnector = async () =>
postConnector({
connectorId: mockSmsConnectorId,
config: mockSmsConnectorConfig,
});

View file

@ -0,0 +1,74 @@
import type { SignInExperience } from '@logto/schemas';
import { SignInMode, SignInIdentifier } from '@logto/schemas';
import { updateSignInExperience } from '#src/api/index.js';
const defaultSignUpMethod = {
identifiers: [],
password: false,
verify: false,
};
const defaultPasswordSignInMethods = [
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: false,
},
{
identifier: SignInIdentifier.Email,
password: true,
verificationCode: false,
isPasswordPrimary: false,
},
{
identifier: SignInIdentifier.Sms,
password: true,
verificationCode: false,
isPasswordPrimary: false,
},
];
const defaultPasscodeSignInMethods = [
{
identifier: SignInIdentifier.Username,
password: true,
verificationCode: false,
isPasswordPrimary: false,
},
{
identifier: SignInIdentifier.Email,
password: true,
verificationCode: true,
isPasswordPrimary: false,
},
{
identifier: SignInIdentifier.Sms,
password: true,
verificationCode: true,
isPasswordPrimary: false,
},
];
export const enableAllPasswordSignInMethods = async (
signUp: SignInExperience['signUp'] = defaultSignUpMethod
) =>
updateSignInExperience({
signInMode: SignInMode.SignInAndRegister,
signUp,
signIn: {
methods: defaultPasswordSignInMethods,
},
});
export const enableAllPasscodeSignInMethods = async (
signUp: SignInExperience['signUp'] = defaultSignUpMethod
) =>
updateSignInExperience({
signInMode: SignInMode.SignInAndRegister,
signUp,
signIn: {
methods: defaultPasscodeSignInMethods,
},
});

View file

@ -0,0 +1,40 @@
import { createUser } from '#src/api/index.js';
import {
generateUsername,
generateEmail,
generatePhone,
generatePassword,
generateName,
} from '#src/utils.js';
export type NewUserProfileOptions = {
username?: true;
primaryEmail?: true;
primaryPhone?: true;
};
export const generateNewUser = async <T extends NewUserProfileOptions>({
username,
primaryEmail,
primaryPhone,
}: T) => {
type UserProfile = {
password: string;
name: string;
} & {
[K in keyof T]: T[K] extends true ? string : never;
};
// @ts-expect-error - TS can't map the type of userProfile to the UserProfile defined above
const userProfile: UserProfile = {
password: generatePassword(),
name: generateName(),
...(username ? { username: generateUsername() } : {}),
...(primaryEmail ? { primaryEmail: generateEmail() } : {}),
...(primaryPhone ? { primaryPhone: generatePhone() } : {}),
};
const user = await createUser(userProfile);
return { user, userProfile };
};