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

refactor: fix tests

This commit is contained in:
Gao Sun 2024-06-06 19:09:40 +08:00
parent ab4867d310
commit afb1091603
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
15 changed files with 82 additions and 67 deletions

View file

@ -43,6 +43,9 @@ type UserContext = {
user: User;
};
/**
* A map of data hook event to its context type for better type hinting.
*/
export type DataHookContextMap = {
'Organization.Membership.Updated': { organizationId: string };
'User.Created': UserContext;

View file

@ -198,10 +198,7 @@ describe('triggerDataHooks()', () => {
const hookData = { path: '/test', method: 'POST', data: { success: true } };
const hooksManager = new DataHookContextManager(metadata);
hooksManager.appendContext({
event: 'Role.Created',
...hookData,
});
hooksManager.appendContext('Role.Created', hookData);
await triggerDataHooks(new ConsoleLog(), hooksManager);
@ -257,8 +254,7 @@ describe('triggerDataHooks()', () => {
const hooksManager = new DataHookContextManager(metadata);
hooksManager.appendContext({
event: 'Role.Created',
hooksManager.appendContext('Role.Created', {
data: { id: 'user_id', username: 'user' },
});

View file

@ -70,6 +70,14 @@ const converBindMfaToMfaVerification = (bindMfa: BindMfa): MfaVerification => {
};
};
export type InsertUserResult = [
User,
{
/** The organization IDs that the user has been provisioned into. */
organizationIds: readonly string[];
},
];
export type UserLibrary = ReturnType<typeof createUserLibrary>;
export const createUserLibrary = (queries: Queries) => {
@ -107,14 +115,6 @@ export const createUserLibrary = (queries: Queries) => {
{ retries, factor: 0 } // No need for exponential backoff
);
type InsertUserResult = [
User,
{
/** The organization IDs that the user has been provisioned into. */
organizationsIds: readonly string[];
},
];
const insertUser = async (
data: OmitAutoSetFields<CreateUser>,
additionalRoleNames: string[]
@ -166,7 +166,7 @@ export const createUserLibrary = (queries: Queries) => {
return [];
};
return [user, { organizationsIds: await provisionOrganizations() }];
return [user, { organizationIds: await provisionOrganizations() }];
});
};

View file

@ -5,6 +5,7 @@ import { removeUndefinedKeys } from '@silverhand/essentials';
import { mockUser, mockUserResponse } from '#src/__mocks__/index.js';
import RequestError from '#src/errors/RequestError/index.js';
import { type InsertUserResult } from '#src/libraries/user.js';
import { koaManagementApiHooks } from '#src/middleware/koa-management-api-hooks.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
@ -81,10 +82,13 @@ const signOutUser = jest.fn();
const usersLibraries = {
generateUserId: jest.fn(async () => 'fooId'),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
})
async (user: CreateUser): Promise<InsertUserResult> => [
{
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
},
{ organizationIds: [] },
]
),
verifyUserPassword,
signOutUser,

View file

@ -200,7 +200,7 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
const id = await generateUserId();
const [user, { organizationsIds }] = await insertUser(
const [user, { organizationIds }] = await insertUser(
{
id,
primaryEmail,
@ -221,7 +221,7 @@ export default function adminUserBasicsRoutes<T extends ManagementApiRouter>(
[]
);
for (const organizationId of organizationsIds) {
for (const organizationId of organizationIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
...buildManagementApiContext(ctx),
organizationId,

View file

@ -8,6 +8,7 @@ import {
mockUserTotpMfaVerification,
mockUserWithMfaVerifications,
} from '#src/__mocks__/index.js';
import { type InsertUserResult } from '#src/libraries/user.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
@ -49,10 +50,13 @@ await mockEsmWithActual('../interaction/utils/backup-code-validation.js', () =>
const usersLibraries = {
generateUserId: jest.fn(async () => 'fooId'),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
})
async (user: CreateUser): Promise<InsertUserResult> => [
{
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
},
{ organizationIds: [] },
]
),
} satisfies Partial<Libraries['users']>;

View file

@ -1,9 +1,10 @@
import type { CreateUser, Role, User } from '@logto/schemas';
import { userInfoSelectFields, RoleType } from '@logto/schemas';
import { pickDefault } from '@logto/shared/esm';
import { pick } from '@silverhand/essentials';
import { pick, removeUndefinedKeys } from '@silverhand/essentials';
import { mockUser, mockUserList, mockUserListResponse } from '#src/__mocks__/index.js';
import { type InsertUserResult } from '#src/libraries/user.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
@ -52,10 +53,13 @@ const mockedQueries = {
const usersLibraries = {
generateUserId: jest.fn(async () => 'fooId'),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
async (user: CreateUser): Promise<InsertUserResult> => [
{
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
},
{ organizationIds: [] },
]
),
} satisfies Partial<Libraries['users']>;

View file

@ -1,5 +1,6 @@
import { ConnectorType, type CreateUser, type User } from '@logto/schemas';
import { pickDefault } from '@logto/shared/esm';
import { removeUndefinedKeys } from '@silverhand/essentials';
import {
mockConnector0,
@ -9,6 +10,7 @@ import {
} from '#src/__mocks__/index.js';
import { mockUser } from '#src/__mocks__/user.js';
import RequestError from '#src/errors/RequestError/index.js';
import { type InsertUserResult } from '#src/libraries/user.js';
import type Libraries from '#src/tenants/Libraries.js';
import type Queries from '#src/tenants/Queries.js';
import { MockTenant, type Partial2 } from '#src/test-utils/tenant.js';
@ -47,10 +49,13 @@ const mockedQueries = {
const usersLibraries = {
generateUserId: jest.fn(async () => 'fooId'),
insertUser: jest.fn(
async (user: CreateUser): Promise<User> => ({
...mockUser,
...user,
})
async (user: CreateUser): Promise<InsertUserResult> => [
{
...mockUser,
...removeUndefinedKeys(user), // No undefined values will be returned from database
},
{ organizationIds: [] },
]
),
} satisfies Partial<Libraries['users']>;

View file

@ -52,7 +52,10 @@ const userQueries = {
const { hasActiveUsers, updateUserById } = userQueries;
const userLibraries = { generateUserId: jest.fn().mockResolvedValue('uid'), insertUser: jest.fn() };
const userLibraries = {
generateUserId: jest.fn().mockResolvedValue('uid'),
insertUser: jest.fn().mockResolvedValue([{}, { organizationIds: [] }]),
};
const { generateUserId, insertUser } = userLibraries;
const submitInteraction = await pickDefault(import('./submit-interaction.js'));
@ -73,7 +76,7 @@ describe('submit action', () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
interactionDetails: { params: {} } as Awaited<ReturnType<Provider['interactionDetails']>>,
assignInteractionHookResult: jest.fn(),
assignDataHookContext: jest.fn(),
appendDataHookContext: jest.fn(),
};
const profile = {
username: 'username',

View file

@ -9,6 +9,7 @@ import {
import { createMockUtils, pickDefault } from '@logto/shared/esm';
import type Provider from 'oidc-provider';
import { type InsertUserResult } from '#src/libraries/user.js';
import { createMockLogContext } from '#src/test-utils/koa-audit-log.js';
import { MockTenant } from '#src/test-utils/tenant.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
@ -62,7 +63,9 @@ const { hasActiveUsers, updateUserById, hasUserWithEmail, hasUserWithPhone } = u
const userLibraries = {
generateUserId: jest.fn().mockResolvedValue('uid'),
insertUser: jest.fn(async (user: CreateUser) => user as User),
insertUser: jest.fn(
async (user: CreateUser): Promise<InsertUserResult> => [user as User, { organizationIds: [] }]
),
};
const { generateUserId, insertUser } = userLibraries;
@ -84,7 +87,7 @@ describe('submit action', () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
interactionDetails: { params: {} } as Awaited<ReturnType<Provider['interactionDetails']>>,
assignInteractionHookResult: jest.fn(),
assignDataHookContext: jest.fn(),
appendDataHookContext: jest.fn(),
};
const profile = {
username: 'username',
@ -153,8 +156,7 @@ describe('submit action', () => {
login: { accountId: 'uid' },
});
expect(ctx.assignDataHookContext).toBeCalledWith({
event: 'User.Created',
expect(ctx.appendDataHookContext).toBeCalledWith('User.Created', {
user: {
id: 'uid',
...upsertProfile,
@ -188,8 +190,7 @@ describe('submit action', () => {
login: { accountId: 'pending-account-id' },
});
expect(ctx.assignDataHookContext).toBeCalledWith({
event: 'User.Created',
expect(ctx.appendDataHookContext).toBeCalledWith('User.Created', {
user: {
id: 'pending-account-id',
...upsertProfile,
@ -336,7 +337,7 @@ describe('submit action', () => {
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
login: { accountId: 'foo' },
});
expect(ctx.assignDataHookContext).not.toBeCalled();
expect(ctx.appendDataHookContext).not.toBeCalled();
});
it('sign-in with new profile', async () => {
@ -371,8 +372,7 @@ describe('submit action', () => {
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
login: { accountId: 'foo' },
});
expect(ctx.assignDataHookContext).toBeCalledWith({
event: 'User.Data.Updated',
expect(ctx.appendDataHookContext).toBeCalledWith('User.Data.Updated', {
user: updateProfile,
});
});
@ -432,8 +432,7 @@ describe('submit action', () => {
expect(assignInteractionResults).toBeCalledWith(ctx, tenant.provider, {
login: { accountId: 'foo' },
});
expect(ctx.assignDataHookContext).toBeCalledWith({
event: 'User.Data.Updated',
expect(ctx.appendDataHookContext).toBeCalledWith('User.Data.Updated', {
user: {
primaryEmail: 'email',
name: userInfo.name,
@ -458,8 +457,7 @@ describe('submit action', () => {
passwordEncryptionMethod: 'plain',
});
expect(assignInteractionResults).not.toBeCalled();
expect(ctx.assignDataHookContext).toBeCalledWith({
event: 'User.Data.Updated',
expect(ctx.appendDataHookContext).toBeCalledWith('User.Data.Updated', {
user: {
passwordEncrypted: 'passwordEncrypted',
passwordEncryptionMethod: 'plain',

View file

@ -135,7 +135,7 @@ async function handleSubmitRegister(
(invitation) => invitation.status === OrganizationInvitationStatus.Pending
);
const [user, { organizationsIds }] = await insertUser(
const [user, { organizationIds }] = await insertUser(
{
id,
...userProfile,
@ -190,7 +190,7 @@ async function handleSubmitRegister(
ctx.assignInteractionHookResult({ userId: id });
ctx.appendDataHookContext('User.Created', { user });
for (const organizationId of organizationsIds) {
for (const organizationId of organizationIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
organizationId,
});

View file

@ -13,6 +13,7 @@ import { MockTenant } from '#src/test-utils/tenant.js';
import { createContextWithRouteParameters } from '#src/utils/test-utils.js';
import { type WithInteractionDetailsContext } from '../middleware/koa-interaction-details.js';
import { type WithInteractionHooksContext } from '../middleware/koa-interaction-hooks.js';
const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);
@ -25,7 +26,7 @@ const updateUserSsoIdentityMock = jest.fn();
const insertUserSsoIdentityMock = jest.fn();
const updateUserMock = jest.fn();
const findUserByEmailMock = jest.fn();
const insertUserMock = jest.fn();
const insertUserMock = jest.fn().mockResolvedValue([{ id: 'foo' }, { organizationIds: [] }]);
const generateUserIdMock = jest.fn().mockResolvedValue('foo');
const getAvailableSsoConnectorsMock = jest.fn();
@ -59,12 +60,14 @@ const {
} = await import('./single-sign-on.js');
describe('Single sign on util methods tests', () => {
const mockContext: WithLogContext & WithInteractionDetailsContext = {
const mockContext = {
...createContextWithRouteParameters(),
...createMockLogContext(),
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
interactionDetails: { jti: 'foo' } as Awaited<ReturnType<Provider['interactionDetails']>>,
};
assignInteractionHookResult: jest.fn(),
appendDataHookContext: jest.fn(),
} satisfies WithInteractionHooksContext<WithLogContext<WithInteractionDetailsContext>>;
const mockProvider = createMockProvider();
@ -288,7 +291,7 @@ describe('Single sign on util methods tests', () => {
describe('registerWithSsoAuthentication tests', () => {
it('should register if no related user account found', async () => {
insertUserMock.mockResolvedValueOnce({ id: 'foo' });
insertUserMock.mockResolvedValueOnce([{ id: 'foo' }, { organizationIds: [] }]);
const { id } = await registerWithSsoAuthentication(mockContext, tenant, {
connectorId: wellConfiguredSsoConnector.id,

View file

@ -310,7 +310,7 @@ export const registerWithSsoAuthentication = async (
};
// Insert new user
const [user, { organizationsIds }] = await usersLibrary.insertUser(
const [user, { organizationIds }] = await usersLibrary.insertUser(
{
id: await usersLibrary.generateUserId(),
...syncingProfile,
@ -318,7 +318,7 @@ export const registerWithSsoAuthentication = async (
},
[]
);
for (const organizationId of organizationsIds) {
for (const organizationId of organizationIds) {
ctx.appendDataHookContext('Organization.Membership.Updated', {
organizationId,
});

View file

@ -1,8 +1,3 @@
/**
* @fileoverview
* Tests for manual-triggered data hook events.
*/
import { SignInIdentifier, hookEvents, userInfoSelectFields } from '@logto/schemas';
import { pick } from '@silverhand/essentials';
@ -10,6 +5,7 @@ import { deleteUser } from '#src/api/admin-user.js';
import { createResource, deleteResource } from '#src/api/resource.js';
import { createRole } from '#src/api/role.js';
import { createScope } from '#src/api/scope.js';
import { setEmailConnector, setSmsConnector } from '#src/helpers/connector.js';
import { WebHookApiTest } from '#src/helpers/hook.js';
import { registerWithEmail } from '#src/helpers/interactions.js';
import { OrganizationApiTest } from '#src/helpers/organization.js';
@ -133,7 +129,11 @@ describe('manual data hook tests', () => {
await assertOrganizationMembershipUpdated(organization.id);
});
// TODO: Add user deletion test case
it('should trigger `Organization.Membership.Updated` event when user is provisioned by experience', async () => {
await setEmailConnector();
await setSmsConnector();
await enableAllVerificationCodeSignInMethods({
identifiers: [SignInIdentifier.Email],
password: false,

View file

@ -1,8 +1,3 @@
/**
* @fileoverview
* Tests for automatic data hook events triggered by the Management API.
*/
import {
RoleType,
hookEventGuard,