mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor: update webhook API (#3819)
This commit is contained in:
parent
a265f1f48e
commit
9423b273b6
38 changed files with 589 additions and 38 deletions
65
packages/core/src/__mocks__/hook.ts
Normal file
65
packages/core/src/__mocks__/hook.ts
Normal file
|
@ -0,0 +1,65 @@
|
|||
import { type Hook, HookEvent } from '@logto/schemas';
|
||||
|
||||
export const mockNanoIdForHook = 'random_string';
|
||||
|
||||
export const mockCreatedAtForHook = 1_650_969_000_000;
|
||||
|
||||
export const mockTenantIdForHook = 'fake_tenant';
|
||||
|
||||
export const mockHook: Hook = {
|
||||
tenantId: mockTenantIdForHook,
|
||||
id: mockNanoIdForHook,
|
||||
name: 'foo',
|
||||
event: null,
|
||||
events: [HookEvent.PostRegister],
|
||||
config: {
|
||||
url: 'https://example.com',
|
||||
},
|
||||
signingKey: mockNanoIdForHook,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
};
|
||||
|
||||
const mockHookData1: Hook = {
|
||||
tenantId: mockTenantIdForHook,
|
||||
id: 'hook_id_1',
|
||||
name: 'foo',
|
||||
event: null,
|
||||
events: [HookEvent.PostRegister],
|
||||
config: {
|
||||
url: 'https://example1.com',
|
||||
},
|
||||
signingKey: mockNanoIdForHook,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
};
|
||||
|
||||
const mockHookData2: Hook = {
|
||||
tenantId: mockTenantIdForHook,
|
||||
id: 'hook_id_2',
|
||||
name: 'bar',
|
||||
event: null,
|
||||
events: [HookEvent.PostResetPassword],
|
||||
config: {
|
||||
url: 'https://example2.com',
|
||||
},
|
||||
signingKey: mockNanoIdForHook,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
};
|
||||
|
||||
const mockHookData3: Hook = {
|
||||
tenantId: mockTenantIdForHook,
|
||||
id: 'hook_id_3',
|
||||
name: 'baz',
|
||||
event: null,
|
||||
events: [HookEvent.PostSignIn],
|
||||
config: {
|
||||
url: 'https://example3.com',
|
||||
},
|
||||
signingKey: mockNanoIdForHook,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
};
|
||||
|
||||
export const mockHookList: Hook[] = [mockHookData1, mockHookData2, mockHookData3];
|
|
@ -53,7 +53,10 @@ export const createHookLibrary = (queries: Queries) => {
|
|||
|
||||
const hookEvent = eventToHook[event];
|
||||
const found = await findAllHooks();
|
||||
const rows = found.filter(({ event }) => event === hookEvent);
|
||||
const rows = found.filter(
|
||||
({ event, events, enabled }) =>
|
||||
enabled && (events.length > 0 ? events.includes(hookEvent) : event === hookEvent) // For backward compatibility
|
||||
);
|
||||
|
||||
if (rows.length === 0) {
|
||||
return;
|
||||
|
@ -90,7 +93,7 @@ export const createHookLibrary = (queries: Queries) => {
|
|||
.post(url, {
|
||||
headers: { 'user-agent': 'Logto (https://logto.io)', ...headers },
|
||||
json,
|
||||
retry: { limit: retries },
|
||||
retry: { limit: retries ?? 3 },
|
||||
timeout: { request: 10_000 },
|
||||
})
|
||||
.then(async (response) => {
|
||||
|
|
236
packages/core/src/routes/hook.test.ts
Normal file
236
packages/core/src/routes/hook.test.ts
Normal file
|
@ -0,0 +1,236 @@
|
|||
import {
|
||||
HookEvent,
|
||||
type Hook,
|
||||
type HookEvents,
|
||||
type HookConfig,
|
||||
type CreateHook,
|
||||
} from '@logto/schemas';
|
||||
import { pickDefault } from '@logto/shared/esm';
|
||||
|
||||
import {
|
||||
mockCreatedAtForHook,
|
||||
mockHook,
|
||||
mockHookList,
|
||||
mockNanoIdForHook,
|
||||
mockTenantIdForHook,
|
||||
} from '#src/__mocks__/hook.js';
|
||||
import { MockTenant } from '#src/test-utils/tenant.js';
|
||||
import { createRequester } from '#src/utils/test-utils.js';
|
||||
|
||||
const { jest } = import.meta;
|
||||
|
||||
const hooks = {
|
||||
findAllHooks: async (): Promise<Hook[]> => mockHookList,
|
||||
insertHook: async (data: CreateHook): Promise<Hook> => ({
|
||||
...mockHook,
|
||||
...data,
|
||||
}),
|
||||
findHookById: async (id: string): Promise<Hook> => {
|
||||
const hook = mockHookList.find((hook) => hook.id === id);
|
||||
if (!hook) {
|
||||
throw new Error('Not found');
|
||||
}
|
||||
return hook;
|
||||
},
|
||||
updateHookById: async (id: string, data: Partial<CreateHook>): Promise<Hook> => {
|
||||
const targetHook = mockHookList.find((hook) => hook.id === id) ?? mockHook;
|
||||
return {
|
||||
...targetHook,
|
||||
...data,
|
||||
};
|
||||
},
|
||||
deleteHookById: jest.fn(),
|
||||
};
|
||||
|
||||
const tenantContext = new MockTenant(undefined, { hooks });
|
||||
|
||||
const hookRoutes = await pickDefault(import('./hook.js'));
|
||||
|
||||
describe('hook routes', () => {
|
||||
const hookRequest = createRequester({ authedRoutes: hookRoutes, tenantContext });
|
||||
|
||||
it('GET /hooks', async () => {
|
||||
const response = await hookRequest.get('/hooks');
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(mockHookList);
|
||||
expect(response.header).not.toHaveProperty('total-number');
|
||||
});
|
||||
|
||||
it('GET /hooks/:id', async () => {
|
||||
const hookIdInMockList = mockHookList[0]?.id ?? '';
|
||||
const response = await hookRequest.get(`/hooks/${hookIdInMockList}`);
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body.id).toBe(hookIdInMockList);
|
||||
});
|
||||
|
||||
it('POST /hooks', async () => {
|
||||
const name = 'fooName';
|
||||
const events: HookEvents = [HookEvent.PostRegister];
|
||||
const config: HookConfig = {
|
||||
url: 'https://example.com',
|
||||
};
|
||||
|
||||
const response = await hookRequest.post('/hooks').send({ name, events, config });
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body.id).toBeTruthy();
|
||||
expect(response.body.signingKey).toBeTruthy();
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
tenantId: mockTenantIdForHook,
|
||||
name,
|
||||
events,
|
||||
config,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /hooks should be able to create a hook with multi events', async () => {
|
||||
const name = 'anyName';
|
||||
const events: HookEvents = [HookEvent.PostSignIn, HookEvent.PostRegister];
|
||||
const config: HookConfig = {
|
||||
url: 'https://example.com',
|
||||
};
|
||||
|
||||
const response = await hookRequest.post('/hooks').send({ name, events, config });
|
||||
expect(response.status).toEqual(201);
|
||||
expect(response.body.id).toBeTruthy();
|
||||
expect(response.body.signingKey).toBeTruthy();
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
tenantId: mockTenantIdForHook,
|
||||
name,
|
||||
events,
|
||||
config,
|
||||
enabled: true,
|
||||
createdAt: mockCreatedAtForHook,
|
||||
});
|
||||
});
|
||||
|
||||
it('POST /hooks should fail when no events are provided', async () => {
|
||||
const payload: Partial<Hook> = {
|
||||
name: 'hook_name',
|
||||
config: {
|
||||
url: 'https://example.com',
|
||||
},
|
||||
};
|
||||
await expect(hookRequest.post('/hooks').send(payload)).resolves.toHaveProperty('status', 400);
|
||||
});
|
||||
|
||||
it('POST /hooks should success when create a hook with the old payload format', async () => {
|
||||
const payload: Partial<Hook> = {
|
||||
event: HookEvent.PostRegister,
|
||||
config: {
|
||||
url: 'https://example.com',
|
||||
retries: 2,
|
||||
},
|
||||
};
|
||||
const response = await hookRequest.post('/hooks').send(payload);
|
||||
expect(response.status).toEqual(201);
|
||||
const generatedId = response.body.id as string;
|
||||
|
||||
expect(response.body).toMatchObject({
|
||||
tenantId: mockTenantIdForHook,
|
||||
id: generatedId,
|
||||
event: HookEvent.PostRegister,
|
||||
config: {
|
||||
url: 'https://example.com',
|
||||
retries: 2,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id', async () => {
|
||||
const targetMockHook = mockHookList[0] ?? mockHook;
|
||||
const name = 'newName';
|
||||
const events: HookEvents = [HookEvent.PostSignIn];
|
||||
const config: HookConfig = {
|
||||
url: 'https://new.com',
|
||||
};
|
||||
|
||||
const response = await hookRequest
|
||||
.patch(`/hooks/${targetMockHook.id}`)
|
||||
.send({ name, events, config, enabled: false });
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchObject({
|
||||
name,
|
||||
events,
|
||||
config,
|
||||
enabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id should success when update a hook with multi events', async () => {
|
||||
const targetMockHook = mockHookList[0] ?? mockHook;
|
||||
const events = [HookEvent.PostSignIn, HookEvent.PostResetPassword];
|
||||
const response = await hookRequest.patch(`/hooks/${targetMockHook.id}`).send({ events });
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchObject({
|
||||
events,
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id should success when update a hook with the old payload format', async () => {
|
||||
const targetMockHook = mockHookList[0] ?? mockHook;
|
||||
const event = HookEvent.PostSignIn;
|
||||
const response = await hookRequest.patch(`/hooks/${targetMockHook.id}`).send({
|
||||
event,
|
||||
config: {
|
||||
url: 'https://example2.com',
|
||||
retries: 1,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toMatchObject({
|
||||
event,
|
||||
config: {
|
||||
url: 'https://example2.com',
|
||||
retries: 1,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id with empty events list should fail', async () => {
|
||||
const invalidEvents: HookEvent[] = [];
|
||||
const response = await hookRequest
|
||||
.patch(`/hooks/${mockNanoIdForHook}`)
|
||||
.send({ events: invalidEvents });
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id should not update signing key', async () => {
|
||||
const targetMockHook = mockHookList[0] ?? mockHook;
|
||||
const newSigningKey = `New-${targetMockHook.signingKey}`;
|
||||
const response = await hookRequest
|
||||
.patch(`/hooks/${targetMockHook.id}`)
|
||||
.send({ signingKey: newSigningKey });
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual(targetMockHook);
|
||||
expect(response.body.config.signingKey).not.toEqual(newSigningKey);
|
||||
});
|
||||
|
||||
it('PATCH /hooks/:id/signing-key should update the singing key of a hook', async () => {
|
||||
const targetMockHook = mockHookList[0] ?? mockHook;
|
||||
const originalSigningKey = targetMockHook.signingKey;
|
||||
const response = await hookRequest.patch(`/hooks/${targetMockHook.id}/signing-key`).send();
|
||||
expect(response.status).toEqual(200);
|
||||
|
||||
const newSigningKey = response.body.signingKey as string;
|
||||
expect(originalSigningKey).not.toEqual(newSigningKey);
|
||||
expect(response.body).toEqual({
|
||||
...targetMockHook,
|
||||
signingKey: newSigningKey,
|
||||
});
|
||||
});
|
||||
|
||||
it('DELETE /hooks/:id', async () => {
|
||||
await expect(hookRequest.delete(`/hooks/${mockNanoIdForHook}`)).resolves.toHaveProperty(
|
||||
'status',
|
||||
204
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,11 +1,18 @@
|
|||
import { Hooks } from '@logto/schemas';
|
||||
import { Hooks, hookEventsGuard } from '@logto/schemas';
|
||||
import { generateStandardId } from '@logto/shared';
|
||||
import { conditional, deduplicate } from '@silverhand/essentials';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import assertThat from '#src/utils/assert-that.js';
|
||||
|
||||
import type { AuthedRouter, RouterInitArgs } from './types.js';
|
||||
|
||||
const nonemptyUniqueHookEventsGuard = hookEventsGuard
|
||||
.nonempty()
|
||||
.transform((events) => deduplicate(events));
|
||||
|
||||
export default function hookRoutes<T extends AuthedRouter>(
|
||||
...[router, { queries }]: RouterInitArgs<T>
|
||||
) {
|
||||
|
@ -21,23 +28,10 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/hooks',
|
||||
koaGuard({ body: Hooks.createGuard.omit({ id: true }), response: Hooks.guard, status: 200 }),
|
||||
async (ctx, next) => {
|
||||
ctx.body = await insertHook({
|
||||
id: generateStandardId(),
|
||||
...ctx.guard.body,
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
'/hooks/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
params: z.object({ id: z.string() }),
|
||||
response: Hooks.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
|
@ -52,11 +46,44 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
}
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/hooks',
|
||||
koaGuard({
|
||||
body: Hooks.createGuard.omit({ id: true, signingKey: true }).extend({
|
||||
events: nonemptyUniqueHookEventsGuard.optional(),
|
||||
}),
|
||||
response: Hooks.guard,
|
||||
status: [201, 400],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const { event, events, enabled, ...rest } = ctx.guard.body;
|
||||
assertThat(events ?? event, new RequestError({ code: 'hook.missing_events', status: 400 }));
|
||||
|
||||
ctx.body = await insertHook({
|
||||
...rest,
|
||||
id: generateStandardId(),
|
||||
signingKey: generateStandardId(),
|
||||
events: events ?? [],
|
||||
enabled: enabled ?? true,
|
||||
...conditional(event && { event }),
|
||||
});
|
||||
|
||||
ctx.status = 201;
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/hooks/:id',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: Hooks.createGuard.omit({ id: true }).partial(),
|
||||
params: z.object({ id: z.string() }),
|
||||
body: Hooks.createGuard
|
||||
.omit({ id: true, signingKey: true })
|
||||
.extend({
|
||||
events: nonemptyUniqueHookEventsGuard,
|
||||
})
|
||||
.partial(),
|
||||
response: Hooks.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
|
@ -66,7 +93,27 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
body,
|
||||
} = ctx.guard;
|
||||
|
||||
ctx.body = await updateHookById(id, body);
|
||||
ctx.body = await updateHookById(id, body, 'replace');
|
||||
|
||||
return next();
|
||||
}
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/hooks/:id/signing-key',
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string() }),
|
||||
response: Hooks.guard,
|
||||
status: [200, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
params: { id },
|
||||
} = ctx.guard;
|
||||
|
||||
ctx.body = await updateHookById(id, {
|
||||
signingKey: generateStandardId(),
|
||||
});
|
||||
|
||||
return next();
|
||||
}
|
||||
|
@ -74,7 +121,7 @@ export default function hookRoutes<T extends AuthedRouter>(
|
|||
|
||||
router.delete(
|
||||
'/hooks/:id',
|
||||
koaGuard({ params: z.object({ id: z.string().min(1) }), status: [204, 404] }),
|
||||
koaGuard({ params: z.object({ id: z.string() }), status: [204, 404] }),
|
||||
async (ctx, next) => {
|
||||
const { id } = ctx.guard.params;
|
||||
await deleteHookById(id);
|
||||
|
|
|
@ -252,12 +252,18 @@ export const zodTypeToSwagger = (
|
|||
};
|
||||
}
|
||||
|
||||
// TO-DO: Improve swagger output for zod schema with refinement (validate through JS functions)
|
||||
if (config instanceof ZodEffects && config._def.effect.type === 'refinement') {
|
||||
return {
|
||||
type: 'object',
|
||||
description: 'Validator function',
|
||||
};
|
||||
if (config instanceof ZodEffects) {
|
||||
if (config._def.effect.type === 'transform') {
|
||||
return zodTypeToSwagger(config._def.schema);
|
||||
}
|
||||
|
||||
// TO-DO: Improve swagger output for zod schema with refinement (validate through JS functions)
|
||||
if (config._def.effect.type === 'refinement') {
|
||||
return {
|
||||
type: 'object',
|
||||
description: 'Validator function',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new RequestError('swagger.invalid_zod_type', config);
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import type { Hook, LogKey } from '@logto/schemas';
|
||||
import type { Hook, HookConfig, LogKey } from '@logto/schemas';
|
||||
import { HookEvent, SignInIdentifier, LogResult, InteractionEvent } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi, deleteUser, getLogs, putInteraction } from '#src/api/index.js';
|
||||
import { createResponseWithCode } from '#src/helpers/admin-tenant.js';
|
||||
import { initClient, processSession } from '#src/helpers/client.js';
|
||||
import { createMockServer } from '#src/helpers/index.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
|
||||
import { waitFor } from '#src/utils.js';
|
||||
|
||||
const createPayload = (event: HookEvent, url = 'not_work_url'): Partial<Hook> => ({
|
||||
event,
|
||||
type CreateHookPayload = Pick<Hook, 'name' | 'events'> & {
|
||||
config: HookConfig;
|
||||
};
|
||||
|
||||
const createPayload = (event: HookEvent, url = 'not_work_url'): CreateHookPayload => ({
|
||||
name: 'hook_name',
|
||||
events: [event],
|
||||
config: {
|
||||
url,
|
||||
headers: { foo: 'bar' },
|
||||
retries: 3,
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -37,8 +42,33 @@ describe('hooks', () => {
|
|||
const payload = createPayload(HookEvent.PostRegister);
|
||||
const created = await authedAdminApi.post('hooks', { json: payload }).json<Hook>();
|
||||
|
||||
expect(payload.event).toEqual(created.event);
|
||||
expect(payload.config).toEqual(created.config);
|
||||
expect(created).toMatchObject(payload);
|
||||
|
||||
expect(await authedAdminApi.get('hooks').json<Hook[]>()).toContainEqual(created);
|
||||
expect(await authedAdminApi.get(`hooks/${created.id}`).json<Hook>()).toEqual(created);
|
||||
expect(
|
||||
await authedAdminApi
|
||||
.patch(`hooks/${created.id}`, { json: { events: [HookEvent.PostSignIn] } })
|
||||
.json<Hook>()
|
||||
).toMatchObject({ ...created, events: [HookEvent.PostSignIn] });
|
||||
expect(await authedAdminApi.delete(`hooks/${created.id}`)).toHaveProperty('statusCode', 204);
|
||||
await expect(authedAdminApi.get(`hooks/${created.id}`)).rejects.toHaveProperty(
|
||||
'response.statusCode',
|
||||
404
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to create, query, update, and delete a hook by the original API', async () => {
|
||||
const payload = {
|
||||
event: HookEvent.PostRegister,
|
||||
config: {
|
||||
url: 'not_work_url',
|
||||
retries: 2,
|
||||
},
|
||||
};
|
||||
const created = await authedAdminApi.post('hooks', { json: payload }).json<Hook>();
|
||||
|
||||
expect(created).toMatchObject(payload);
|
||||
|
||||
expect(await authedAdminApi.get('hooks').json<Hook[]>()).toContainEqual(created);
|
||||
expect(await authedAdminApi.get(`hooks/${created.id}`).json<Hook>()).toEqual(created);
|
||||
|
@ -46,7 +76,10 @@ describe('hooks', () => {
|
|||
await authedAdminApi
|
||||
.patch(`hooks/${created.id}`, { json: { event: HookEvent.PostSignIn } })
|
||||
.json<Hook>()
|
||||
).toEqual({ ...created, event: HookEvent.PostSignIn });
|
||||
).toMatchObject({
|
||||
...created,
|
||||
event: HookEvent.PostSignIn,
|
||||
});
|
||||
expect(await authedAdminApi.delete(`hooks/${created.id}`)).toHaveProperty('statusCode', 204);
|
||||
await expect(authedAdminApi.get(`hooks/${created.id}`)).rejects.toHaveProperty(
|
||||
'response.statusCode',
|
||||
|
@ -54,6 +87,47 @@ describe('hooks', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('should throw error when creating a hook with an empty hook name', async () => {
|
||||
const payload = {
|
||||
name: '',
|
||||
events: [HookEvent.PostRegister],
|
||||
config: {
|
||||
url: 'not_work_url',
|
||||
},
|
||||
};
|
||||
await expect(authedAdminApi.post('hooks', { json: payload })).rejects.toMatchObject(
|
||||
createResponseWithCode(400)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error when no event is provided when creating a hook', async () => {
|
||||
const payload = {
|
||||
name: 'hook_name',
|
||||
config: {
|
||||
url: 'not_work_url',
|
||||
},
|
||||
};
|
||||
await expect(authedAdminApi.post('hooks', { json: payload })).rejects.toMatchObject(
|
||||
createResponseWithCode(400)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if update a hook with a invalid hook id', async () => {
|
||||
const payload = {
|
||||
name: 'new_hook_name',
|
||||
};
|
||||
|
||||
await expect(authedAdminApi.patch('hooks/invalid_id', { json: payload })).rejects.toMatchObject(
|
||||
createResponseWithCode(404)
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error if regenerate a hook signing key with a invalid hook id', async () => {
|
||||
await expect(authedAdminApi.patch('hooks/invalid_id/signing-key')).rejects.toMatchObject(
|
||||
createResponseWithCode(404)
|
||||
);
|
||||
});
|
||||
|
||||
it('should trigger sign-in hook and record error when interaction finished', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostSignIn) })
|
||||
|
@ -93,11 +167,20 @@ describe('hooks', () => {
|
|||
});
|
||||
|
||||
it('should trigger multiple register hooks and record properly when interaction finished', async () => {
|
||||
const [hook1, hook2] = await Promise.all([
|
||||
const [hook1, hook2, hook3] = await Promise.all([
|
||||
authedAdminApi.post('hooks', { json: createPayload(HookEvent.PostRegister) }).json<Hook>(),
|
||||
authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
|
||||
.json<Hook>(),
|
||||
// Using the old API to create a hook
|
||||
authedAdminApi
|
||||
.post('hooks', {
|
||||
json: {
|
||||
event: HookEvent.PostRegister,
|
||||
config: { url: 'http://localhost:9999', retries: 2 },
|
||||
},
|
||||
})
|
||||
.json<Hook>(),
|
||||
]);
|
||||
const logKey: LogKey = 'TriggerHook.PostRegister';
|
||||
|
||||
|
@ -128,11 +211,17 @@ describe('hooks', () => {
|
|||
({ payload: { hookId, result } }) => hookId === hook2.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) => hookId === hook3.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
// Clean up
|
||||
await Promise.all([
|
||||
authedAdminApi.delete(`hooks/${hook1.id}`),
|
||||
authedAdminApi.delete(`hooks/${hook2.id}`),
|
||||
authedAdminApi.delete(`hooks/${hook3.id}`),
|
||||
]);
|
||||
await deleteUser(id);
|
||||
});
|
||||
|
|
5
packages/phrases/src/locales/de/errors/hook.ts
Normal file
5
packages/phrases/src/locales/de/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Sie müssen mindestens ein Ereignis angeben.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/en/errors/hook.ts
Normal file
5
packages/phrases/src/locales/en/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'You need to provide at least one event.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/es/errors/hook.ts
Normal file
5
packages/phrases/src/locales/es/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Necesita proporcionar al menos un evento.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/fr/errors/hook.ts
Normal file
5
packages/phrases/src/locales/fr/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Vous devez fournir au moins un événement.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/it/errors/hook.ts
Normal file
5
packages/phrases/src/locales/it/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'È necessario fornire almeno un evento.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ja/errors/hook.ts
Normal file
5
packages/phrases/src/locales/ja/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: '少なくとも1つのイベントを提供する必要があります。',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ko/errors/hook.ts
Normal file
5
packages/phrases/src/locales/ko/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: '최소한 하나의 이벤트를 제공해야 합니다.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pl-pl/errors/hook.ts
Normal file
5
packages/phrases/src/locales/pl-pl/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Musisz podać przynajmniej jedno zdarzenie.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pt-br/errors/hook.ts
Normal file
5
packages/phrases/src/locales/pt-br/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Você precisa fornecer pelo menos um evento.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/pt-pt/errors/hook.ts
Normal file
5
packages/phrases/src/locales/pt-pt/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Você precisa fornecer pelo menos um evento.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/ru/errors/hook.ts
Normal file
5
packages/phrases/src/locales/ru/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'Вы должны предоставить как минимум одно событие.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/tr-tr/errors/hook.ts
Normal file
5
packages/phrases/src/locales/tr-tr/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: 'En az bir etkinlik sağlamanız gerekiyor.',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-cn/errors/hook.ts
Normal file
5
packages/phrases/src/locales/zh-cn/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: '你需要提供至少一个事件。',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-hk/errors/hook.ts
Normal file
5
packages/phrases/src/locales/zh-hk/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: '你需要至少提供一個事件。',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
5
packages/phrases/src/locales/zh-tw/errors/hook.ts
Normal file
5
packages/phrases/src/locales/zh-tw/errors/hook.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
const hook = {
|
||||
missing_events: '你需要至少提供一個事件。',
|
||||
};
|
||||
|
||||
export default hook;
|
|
@ -2,6 +2,7 @@ import auth from './auth.js';
|
|||
import connector from './connector.js';
|
||||
import entity from './entity.js';
|
||||
import guard from './guard.js';
|
||||
import hook from './hook.js';
|
||||
import localization from './localization.js';
|
||||
import log from './log.js';
|
||||
import oidc from './oidc.js';
|
||||
|
@ -36,6 +37,7 @@ const errors = {
|
|||
scope,
|
||||
storage,
|
||||
resource,
|
||||
hook,
|
||||
};
|
||||
|
||||
export default errors;
|
||||
|
|
|
@ -217,7 +217,7 @@ export const hookConfigGuard = z.object({
|
|||
* Now the retry times is fixed to 3.
|
||||
* Keep for backward compatibility.
|
||||
*/
|
||||
retries: z.number().gte(0).lte(3),
|
||||
retries: z.number().gte(0).lte(3).optional(),
|
||||
});
|
||||
|
||||
export type HookConfig = z.infer<typeof hookConfigGuard>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Application, User } from '../db-entries/index.js';
|
||||
import type { HookEvent } from '../foundations/index.js';
|
||||
import { type Application, type User } from '../db-entries/index.js';
|
||||
import { type HookEvent } from '../foundations/index.js';
|
||||
|
||||
import type { userInfoSelectFields } from './user.js';
|
||||
|
||||
|
|
Loading…
Reference in a new issue