mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor: add unit tests
This commit is contained in:
parent
a6342213a9
commit
8745886aa0
3 changed files with 113 additions and 18 deletions
89
packages/core/src/libraries/hook.test.ts
Normal file
89
packages/core/src/libraries/hook.test.ts
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import { Event } from '@logto/schemas';
|
||||||
|
import { HookEvent } from '@logto/schemas/lib/models/hooks.js';
|
||||||
|
import { mockEsm, mockEsmDefault } from '@logto/shared/esm';
|
||||||
|
import type { InferModelType } from '@withtyped/server';
|
||||||
|
import { got } from 'got';
|
||||||
|
|
||||||
|
import modelRouters from '#src/model-routers/index.js';
|
||||||
|
import { MockQueryClient } from '#src/test-utils/query-client.js';
|
||||||
|
|
||||||
|
import type { Interaction } from './hook.js';
|
||||||
|
|
||||||
|
const { jest } = import.meta;
|
||||||
|
|
||||||
|
const queryClient = new MockQueryClient();
|
||||||
|
const queryFunction = jest.fn();
|
||||||
|
|
||||||
|
const url = 'https://logto.gg';
|
||||||
|
const hook: InferModelType<typeof modelRouters.hook.model> = {
|
||||||
|
id: 'foo',
|
||||||
|
event: HookEvent.PostSignIn,
|
||||||
|
config: { headers: { bar: 'baz' }, url, retries: 3 },
|
||||||
|
createdAt: new Date(),
|
||||||
|
};
|
||||||
|
const readAll = jest
|
||||||
|
.spyOn(modelRouters.hook.client, 'readAll')
|
||||||
|
.mockResolvedValue({ rows: [hook], rowCount: 1 });
|
||||||
|
|
||||||
|
// @ts-expect-error for testing
|
||||||
|
const post = jest.spyOn(got, 'post').mockImplementation(jest.fn(() => ({ json: jest.fn() })));
|
||||||
|
|
||||||
|
mockEsm('#src/queries/user.js', () => ({
|
||||||
|
findUserById: () => ({ id: 'user_id', username: 'user', extraField: 'not_ok' }),
|
||||||
|
}));
|
||||||
|
mockEsm('#src/queries/application.js', () => ({
|
||||||
|
findApplicationById: () => ({ id: 'app_id', extraField: 'not_ok' }),
|
||||||
|
}));
|
||||||
|
mockEsm('#src/connectors/index.js', () => ({
|
||||||
|
getLogtoConnectorById: () => ({ metadata: { id: 'connector_id', extraField: 'not_ok' } }),
|
||||||
|
}));
|
||||||
|
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||||
|
mockEsmDefault('#src/env-set/create-query-client-by-env.js', () => () => queryClient);
|
||||||
|
jest.spyOn(queryClient, 'query').mockImplementation(queryFunction);
|
||||||
|
|
||||||
|
const { triggerInteractionHooksIfNeeded } = await import('./hook.js');
|
||||||
|
|
||||||
|
describe('triggerInteractionHooksIfNeeded()', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return if no user ID found', async () => {
|
||||||
|
await triggerInteractionHooksIfNeeded({ event: Event.SignIn });
|
||||||
|
|
||||||
|
expect(queryFunction).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set correct payload when hook triggered', async () => {
|
||||||
|
jest.useFakeTimers().setSystemTime(100_000);
|
||||||
|
|
||||||
|
await triggerInteractionHooksIfNeeded(
|
||||||
|
{ event: Event.SignIn, identifier: { connectorId: 'bar' } },
|
||||||
|
// @ts-expect-error for testing
|
||||||
|
{
|
||||||
|
jti: 'some_jti',
|
||||||
|
result: { login: { accountId: '123' } },
|
||||||
|
params: { client_id: 'some_client' },
|
||||||
|
} as Interaction
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(readAll).toHaveBeenCalled();
|
||||||
|
expect(post).toHaveBeenCalledWith(url, {
|
||||||
|
headers: { 'user-agent': 'Logto (https://logto.io)', bar: 'baz' },
|
||||||
|
json: {
|
||||||
|
hookId: 'foo',
|
||||||
|
event: 'PostSignIn',
|
||||||
|
interactionEvent: 'SignIn',
|
||||||
|
sessionId: 'some_jti',
|
||||||
|
userId: '123',
|
||||||
|
user: { id: 'user_id', username: 'user' },
|
||||||
|
application: { id: 'app_id' },
|
||||||
|
connectors: [{ id: 'connector_id' }],
|
||||||
|
createdAt: new Date(100_000).toISOString(),
|
||||||
|
},
|
||||||
|
retry: { limit: 3 },
|
||||||
|
timeout: { request: 10_000 },
|
||||||
|
});
|
||||||
|
jest.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
|
@ -1,5 +1,5 @@
|
||||||
import { Event, userInfoSelectFields } from '@logto/schemas';
|
import { Event, userInfoSelectFields } from '@logto/schemas';
|
||||||
import { HookEvent } from '@logto/schemas/models';
|
import { HookEventPayload, HookEvent } from '@logto/schemas/models';
|
||||||
import { trySafe } from '@logto/shared';
|
import { trySafe } from '@logto/shared';
|
||||||
import { conditional } from '@silverhand/essentials';
|
import { conditional } from '@silverhand/essentials';
|
||||||
import { got } from 'got';
|
import { got } from 'got';
|
||||||
|
@ -17,19 +17,6 @@ const eventToHook: Record<Event, HookEvent> = {
|
||||||
[Event.ForgotPassword]: HookEvent.PostResetPassword,
|
[Event.ForgotPassword]: HookEvent.PostResetPassword,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HookEventPayload = {
|
|
||||||
hookId: string;
|
|
||||||
event: HookEvent;
|
|
||||||
Event: Event;
|
|
||||||
createdAt: string;
|
|
||||||
sessionId?: string;
|
|
||||||
userAgent?: string;
|
|
||||||
userId?: string;
|
|
||||||
user?: Record<string, unknown>;
|
|
||||||
application?: Record<string, unknown>;
|
|
||||||
connectors?: Array<Record<string, unknown>>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: replace `lodash.pick`
|
// TODO: replace `lodash.pick`
|
||||||
const pick = <T, Keys extends Array<keyof T>>(
|
const pick = <T, Keys extends Array<keyof T>>(
|
||||||
object: T,
|
object: T,
|
||||||
|
@ -41,9 +28,11 @@ const pick = <T, Keys extends Array<keyof T>>(
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Interaction = Awaited<ReturnType<Provider['interactionDetails']>>;
|
||||||
|
|
||||||
export const triggerInteractionHooksIfNeeded = async (
|
export const triggerInteractionHooksIfNeeded = async (
|
||||||
interactionPayload: InteractionPayload,
|
interactionPayload: InteractionPayload,
|
||||||
details?: Awaited<ReturnType<Provider['interactionDetails']>>,
|
details?: Interaction,
|
||||||
userAgent?: string
|
userAgent?: string
|
||||||
) => {
|
) => {
|
||||||
const userId = details?.result?.login?.accountId;
|
const userId = details?.result?.login?.accountId;
|
||||||
|
@ -70,9 +59,9 @@ export const triggerInteractionHooksIfNeeded = async (
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
const payload: Omit<HookEventPayload, 'hookId'> = {
|
const payload = {
|
||||||
event: hookEvent,
|
event: hookEvent,
|
||||||
Event: event,
|
interactionEvent: event,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
sessionId,
|
sessionId,
|
||||||
userAgent,
|
userAgent,
|
||||||
|
@ -82,7 +71,7 @@ export const triggerInteractionHooksIfNeeded = async (
|
||||||
connectors: connector && [
|
connectors: connector && [
|
||||||
pick(connector.metadata, 'id', 'name', 'description', 'platform', 'target', 'isStandard'),
|
pick(connector.metadata, 'id', 'name', 'description', 'platform', 'target', 'isStandard'),
|
||||||
],
|
],
|
||||||
};
|
} satisfies Omit<HookEventPayload, 'hookId'>;
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
rows
|
rows
|
||||||
|
|
|
@ -2,12 +2,29 @@ import { generateStandardId } from '@logto/core-kit';
|
||||||
import { createModel } from '@withtyped/server';
|
import { createModel } from '@withtyped/server';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import type { Application, Connector, User } from '../db-entries/index.js';
|
||||||
|
import type { userInfoSelectFields } from '../types/index.js';
|
||||||
|
|
||||||
export enum HookEvent {
|
export enum HookEvent {
|
||||||
PostRegister = 'PostRegister',
|
PostRegister = 'PostRegister',
|
||||||
PostSignIn = 'PostSignIn',
|
PostSignIn = 'PostSignIn',
|
||||||
PostResetPassword = 'PostResetPassword',
|
PostResetPassword = 'PostResetPassword',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HookEventPayload = {
|
||||||
|
hookId: string;
|
||||||
|
event: HookEvent;
|
||||||
|
createdAt: string;
|
||||||
|
sessionId?: string;
|
||||||
|
userAgent?: string;
|
||||||
|
userId?: string;
|
||||||
|
user?: Pick<User, typeof userInfoSelectFields[number]>;
|
||||||
|
application?: Pick<Application, 'id' | 'type' | 'name' | 'description'>;
|
||||||
|
connectors?: Array<
|
||||||
|
Pick<Connector, 'id'> & Pick<Connector['metadata'], 'name'> & Record<string, unknown>
|
||||||
|
>;
|
||||||
|
} & Record<string, unknown>;
|
||||||
|
|
||||||
export type HookConfig = {
|
export type HookConfig = {
|
||||||
/** We don't need `type` since v1 only has web hook */
|
/** We don't need `type` since v1 only has web hook */
|
||||||
// type: 'web';
|
// type: 'web';
|
||||||
|
|
Loading…
Add table
Reference in a new issue