0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-17 22:31:28 -05:00

refactor: add unit tests

This commit is contained in:
Gao Sun 2022-12-22 17:15:50 +08:00 committed by simeng-li
parent a6342213a9
commit 8745886aa0
No known key found for this signature in database
GPG key ID: 14EA7BB1541E8075
3 changed files with 113 additions and 18 deletions

View 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();
});
});

View file

@ -1,5 +1,5 @@
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 { conditional } from '@silverhand/essentials';
import { got } from 'got';
@ -17,19 +17,6 @@ const eventToHook: Record<Event, HookEvent> = {
[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`
const pick = <T, Keys extends Array<keyof 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 (
interactionPayload: InteractionPayload,
details?: Awaited<ReturnType<Provider['interactionDetails']>>,
details?: Interaction,
userAgent?: string
) => {
const userId = details?.result?.login?.accountId;
@ -70,9 +59,9 @@ export const triggerInteractionHooksIfNeeded = async (
)
),
]);
const payload: Omit<HookEventPayload, 'hookId'> = {
const payload = {
event: hookEvent,
Event: event,
interactionEvent: event,
createdAt: new Date().toISOString(),
sessionId,
userAgent,
@ -82,7 +71,7 @@ export const triggerInteractionHooksIfNeeded = async (
connectors: connector && [
pick(connector.metadata, 'id', 'name', 'description', 'platform', 'target', 'isStandard'),
],
};
} satisfies Omit<HookEventPayload, 'hookId'>;
await Promise.all(
rows

View file

@ -2,12 +2,29 @@ import { generateStandardId } from '@logto/core-kit';
import { createModel } from '@withtyped/server';
import { z } from 'zod';
import type { Application, Connector, User } from '../db-entries/index.js';
import type { userInfoSelectFields } from '../types/index.js';
export enum HookEvent {
PostRegister = 'PostRegister',
PostSignIn = 'PostSignIn',
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 = {
/** We don't need `type` since v1 only has web hook */
// type: 'web';