mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: add and fix tests
This commit is contained in:
parent
f993da8795
commit
84014b37d2
9 changed files with 85 additions and 30 deletions
|
@ -1,4 +1,4 @@
|
|||
import { mockEsmDefault, pickDefault } from '@logto/shared/esm';
|
||||
import { mockEsm, pickDefault } from '@logto/shared/esm';
|
||||
import Koa from 'koa';
|
||||
|
||||
import { emptyMiddleware } from '#src/utils/test-utils.js';
|
||||
|
@ -14,7 +14,10 @@ const middlewareList = [
|
|||
'spa-proxy',
|
||||
].map((name) => {
|
||||
const mock = jest.fn(() => emptyMiddleware);
|
||||
mockEsmDefault(`#src/middleware/koa-${name}.js`, () => mock);
|
||||
mockEsm(`#src/middleware/koa-${name}.js`, () => ({
|
||||
default: mock,
|
||||
...(name === 'audit-log' && { LogEntry: jest.fn() }),
|
||||
}));
|
||||
|
||||
return mock;
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { InteractionEvent, LogResult } 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';
|
||||
|
@ -24,8 +25,20 @@ 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() })));
|
||||
const post = jest
|
||||
.spyOn(got, 'post')
|
||||
// @ts-expect-error for testing
|
||||
.mockImplementation(jest.fn(async () => ({ statusCode: 200, body: '{"message":"ok"}' })));
|
||||
|
||||
// TODO: @Gao fix `mockEsm()` import issue
|
||||
const nanoIdMock = 'mockId';
|
||||
jest.unstable_mockModule('@logto/core-kit', () => ({
|
||||
generateStandardId: () => nanoIdMock,
|
||||
}));
|
||||
|
||||
const { insertLog } = mockEsm('#src/queries/log.js', () => ({
|
||||
insertLog: jest.fn(),
|
||||
}));
|
||||
|
||||
mockEsm('#src/queries/user.js', () => ({
|
||||
findUserById: () => ({ id: 'user_id', username: 'user', extraField: 'not_ok' }),
|
||||
|
@ -46,7 +59,7 @@ describe('triggerInteractionHooksIfNeeded()', () => {
|
|||
});
|
||||
|
||||
it('should return if no user ID found', async () => {
|
||||
await triggerInteractionHooksIfNeeded(Event.SignIn);
|
||||
await triggerInteractionHooksIfNeeded(InteractionEvent.SignIn);
|
||||
|
||||
expect(queryFunction).not.toBeCalled();
|
||||
});
|
||||
|
@ -55,7 +68,7 @@ describe('triggerInteractionHooksIfNeeded()', () => {
|
|||
jest.useFakeTimers().setSystemTime(100_000);
|
||||
|
||||
await triggerInteractionHooksIfNeeded(
|
||||
Event.SignIn,
|
||||
InteractionEvent.SignIn,
|
||||
// @ts-expect-error for testing
|
||||
{
|
||||
jti: 'some_jti',
|
||||
|
@ -80,6 +93,18 @@ describe('triggerInteractionHooksIfNeeded()', () => {
|
|||
retry: { limit: 3 },
|
||||
timeout: { request: 10_000 },
|
||||
});
|
||||
|
||||
const calledPayload: unknown = insertLog.mock.calls[0][0];
|
||||
expect(calledPayload).toHaveProperty('id', nanoIdMock);
|
||||
expect(calledPayload).toHaveProperty('key', 'TriggerHook.' + HookEvent.PostSignIn);
|
||||
expect(calledPayload).toHaveProperty('payload.result', LogResult.Success);
|
||||
expect(calledPayload).toHaveProperty('payload.hookId', 'foo');
|
||||
expect(calledPayload).toHaveProperty('payload.json.event', HookEvent.PostSignIn);
|
||||
expect(calledPayload).toHaveProperty('payload.json.interactionEvent', InteractionEvent.SignIn);
|
||||
expect(calledPayload).toHaveProperty('payload.json.hookId', 'foo');
|
||||
expect(calledPayload).toHaveProperty('payload.json.userId', '123');
|
||||
expect(calledPayload).toHaveProperty('payload.response.statusCode', 200);
|
||||
expect(calledPayload).toHaveProperty('payload.response.body.message', 'ok');
|
||||
jest.useRealTimers();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -28,7 +28,7 @@ const eventToHook: Record<InteractionEvent, HookEvent> = {
|
|||
export type Interaction = Awaited<ReturnType<Provider['interactionDetails']>>;
|
||||
|
||||
export const triggerInteractionHooksIfNeeded = async (
|
||||
event: Event,
|
||||
event: InteractionEvent,
|
||||
details?: Interaction,
|
||||
userAgent?: string
|
||||
) => {
|
||||
|
@ -87,7 +87,8 @@ export const triggerInteractionHooksIfNeeded = async (
|
|||
.catch(async (error) => {
|
||||
logEntry.append({
|
||||
result: LogResult.Error,
|
||||
response: error instanceof HTTPError ? parseResponse(error.response) : String(error),
|
||||
response: conditional(error instanceof HTTPError && parseResponse(error.response)),
|
||||
error: conditional(error instanceof Error && String(error)),
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -10,14 +10,14 @@ import type { WithLogContext, LogPayload } from './koa-audit-log.js';
|
|||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const nanoIdMock = 'mockId';
|
||||
|
||||
const { insertLog } = mockEsm('#src/queries/log.js', () => ({
|
||||
insertLog: jest.fn(),
|
||||
}));
|
||||
|
||||
mockEsm('nanoid', () => ({
|
||||
nanoid: () => nanoIdMock,
|
||||
// TODO: @Gao fix `mockEsm()` import issue
|
||||
const nanoIdMock = 'mockId';
|
||||
jest.unstable_mockModule('@logto/core-kit', () => ({
|
||||
generateStandardId: () => nanoIdMock,
|
||||
}));
|
||||
|
||||
const koaLog = await pickDefault(import('./koa-audit-log.js'));
|
||||
|
|
|
@ -92,15 +92,17 @@ const { sendPasscodeToIdentifier } = await mockEsmWithActual(
|
|||
|
||||
const { createLog, prependAllLogEntries } = createMockLogContext();
|
||||
|
||||
mockEsmDefault(
|
||||
await mockEsmWithActual(
|
||||
'#src/middleware/koa-audit-log.js',
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
(): typeof koaAuditLog => () => async (ctx, next) => {
|
||||
ctx.createLog = createLog;
|
||||
ctx.prependAllLogEntries = prependAllLogEntries;
|
||||
(): { default: typeof koaAuditLog } => ({
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
default: () => async (ctx, next) => {
|
||||
ctx.createLog = createLog;
|
||||
ctx.prependAllLogEntries = prependAllLogEntries;
|
||||
|
||||
return next();
|
||||
}
|
||||
return next();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const {
|
||||
|
@ -272,7 +274,7 @@ describe('session -> interactionRoutes', () => {
|
|||
});
|
||||
|
||||
it('should not call validateMandatoryUserProfile for forgot password request', async () => {
|
||||
getInteractionStorage.mockReturnValueOnce({
|
||||
getInteractionStorage.mockReturnValue({
|
||||
event: InteractionEvent.ForgotPassword,
|
||||
});
|
||||
|
||||
|
|
|
@ -218,7 +218,6 @@ export default function interactionRoutes<T extends AnonymousRouter>(
|
|||
async (ctx, next) => {
|
||||
const { interactionDetails } = ctx;
|
||||
const interactionStorage = getInteractionStorage(interactionDetails.result);
|
||||
|
||||
const { event } = interactionStorage;
|
||||
|
||||
const accountVerifiedInteraction = await verifyIdentifier(ctx, provider, interactionStorage);
|
||||
|
|
|
@ -16,4 +16,4 @@ global.TextDecoder = TextDecoder;
|
|||
global.TextEncoder = TextEncoder;
|
||||
/* eslint-enable @silverhand/fp/no-mutation */
|
||||
|
||||
jest.setTimeout(10_000);
|
||||
jest.setTimeout(15_000);
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import type { LogKey } from '@logto/schemas';
|
||||
import { LogResult, Event } from '@logto/schemas';
|
||||
import { SignInIdentifier, LogResult, InteractionEvent } from '@logto/schemas';
|
||||
import type { Hooks } from '@logto/schemas/models';
|
||||
import { HookEvent } from '@logto/schemas/models';
|
||||
import type { InferModelType } from '@withtyped/server';
|
||||
|
||||
import { authedAdminApi, deleteUser, getLogs, putInteraction } from '#src/api/index.js';
|
||||
import { createMockServer } from '#src/helpers.js';
|
||||
import { waitFor } from '#src/utils.js';
|
||||
|
||||
import { initClient, processSession } from './interaction/utils/client.js';
|
||||
import { enableAllPasswordSignInMethods } from './interaction/utils/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from './interaction/utils/user.js';
|
||||
|
||||
type Hook = InferModelType<typeof Hooks>;
|
||||
|
@ -22,6 +24,21 @@ const createPayload = (event: HookEvent, url = 'not_work_url'): Partial<Hook> =>
|
|||
});
|
||||
|
||||
describe('hooks', () => {
|
||||
const { listen, close } = createMockServer(9999);
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
await listen();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await close();
|
||||
});
|
||||
|
||||
it('should be able to create, query, and delete a hook', async () => {
|
||||
const payload = createPayload(HookEvent.PostRegister);
|
||||
const created = await authedAdminApi.post('hooks', { json: payload }).json<Hook>();
|
||||
|
@ -51,19 +68,23 @@ describe('hooks', () => {
|
|||
} = await generateNewUser({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.successSend(putInteraction, {
|
||||
event: Event.SignIn,
|
||||
event: InteractionEvent.SignIn,
|
||||
identifier: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
await client.submitInteraction();
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) => hookId === createdHook.id && result === LogResult.Error
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === createdHook.id &&
|
||||
result === LogResult.Error &&
|
||||
error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
|
@ -80,14 +101,12 @@ describe('hooks', () => {
|
|||
.json<Hook>(),
|
||||
]);
|
||||
const logKey: LogKey = 'TriggerHook.PostRegister';
|
||||
const { listen, close } = createMockServer(9999);
|
||||
await listen(); // Start mock server
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: Event.Register,
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
|
@ -95,13 +114,14 @@ describe('hooks', () => {
|
|||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
await close(); // Stop mock server early
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) => hookId === hook1.id && result === LogResult.Error
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === hook1.id && result === LogResult.Error && error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
|
|
|
@ -12,3 +12,8 @@ export const generatePhone = () => {
|
|||
|
||||
return crypto.getRandomValues(array).join('');
|
||||
};
|
||||
|
||||
export const waitFor = async (ms: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue