diff --git a/packages/core/src/__mocks__/index.ts b/packages/core/src/__mocks__/index.ts index 4f6865581..3b067fadb 100644 --- a/packages/core/src/__mocks__/index.ts +++ b/packages/core/src/__mocks__/index.ts @@ -1,5 +1,5 @@ -import type { Application, Passcode, Resource, Role, Setting } from '@logto/schemas'; -import { ApplicationType, PasscodeType } from '@logto/schemas'; +import type { Application, Hook, Passcode, Resource, Role, Setting } from '@logto/schemas'; +import { HookEvent, ApplicationType, PasscodeType } from '@logto/schemas'; export * from './connector'; export * from './sign-in-experience'; @@ -36,6 +36,19 @@ export const mockRole: Role = { description: 'admin', }; +export const mockHook: Readonly = Object.freeze({ + id: 'logto_hook', + event: HookEvent.PostSignIn, + config: { + url: 'https://foo.bar', + headers: { + 'User-Agent': 'Logto Core', + }, + retries: 3, + }, + createdAt: 1_645_334_775_356, +}); + export const mockSetting: Setting = { id: 'foo setting', adminConsole: { diff --git a/packages/core/src/routes/hook.test.ts b/packages/core/src/routes/hook.test.ts new file mode 100644 index 000000000..ced1ee6d4 --- /dev/null +++ b/packages/core/src/routes/hook.test.ts @@ -0,0 +1,95 @@ +import type { Hook, CreateHook, HookConfig } from '@logto/schemas'; +import { HookEvent } from '@logto/schemas'; + +import { mockHook } from '@/__mocks__'; +import { createRequester } from '@/utils/test-utils'; + +import hookRoutes from './hook'; + +jest.mock('@/queries/hook', () => ({ + findTotalNumberOfHooks: jest.fn(async () => ({ count: 10 })), + findAllHooks: jest.fn(async (): Promise => [mockHook]), + findHookById: jest.fn(async (): Promise => mockHook), + insertHook: jest.fn( + async (body: CreateHook): Promise => ({ + ...mockHook, + ...body, + }) + ), + updateHookById: jest.fn( + async (_, data: Partial): Promise => ({ + ...mockHook, + ...data, + config: { ...mockHook.config, ...data.config }, + }) + ), + deleteHookById: jest.fn(), +})); + +jest.mock('@logto/shared', () => ({ + generateStandardId: jest.fn(() => mockHook.id), +})); + +describe('hook routes', () => { + const hookRequest = createRequester({ authedRoutes: hookRoutes }); + + it('GET /hooks', async () => { + const response = await hookRequest.get('/hooks'); + expect(response.status).toEqual(200); + expect(response.body).toEqual([mockHook]); + expect(response.header).toHaveProperty('total-number', '10'); + }); + + it('POST /hooks', async () => { + const event = HookEvent.PostChangePassword; + + const response = await hookRequest.post('/hooks').send({ event, config: mockHook.config }); + + console.log(); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + ...mockHook, + event, + }); + }); + + it('POST /hooks should throw with invalid input body', async () => { + const id = 'a_good_hook'; + const event = HookEvent.PostChangePassword; + + await expect(hookRequest.post('/hooks')).resolves.toHaveProperty('status', 400); + await expect(hookRequest.post('/hooks').send({ id })).resolves.toHaveProperty('status', 400); + await expect(hookRequest.post('/hooks').send({ event })).resolves.toHaveProperty('status', 400); + }); + + it('GET /hooks/:id', async () => { + const response = await hookRequest.get('/hooks/foo'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(mockHook); + }); + + it('PATCH /hooks/:id', async () => { + const event = HookEvent.PostChangePassword; + const config: Partial = { url: 'https://bar.baz' }; + + const response = await hookRequest.patch('/hooks/foo').send({ event, config }); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ + ...mockHook, + event, + config: { ...mockHook.config, url: config.url }, + }); + }); + + it('PATCH /hooks/:id should throw with invalid properties', async () => { + const response = await hookRequest.patch('/hooks/foo').send({ event: 12 }); + expect(response.status).toEqual(400); + }); + + it('DELETE /hooks/:id', async () => { + await expect(hookRequest.delete('/hooks/foo')).resolves.toHaveProperty('status', 204); + }); +}); diff --git a/packages/core/src/routes/phrase.test.ts b/packages/core/src/routes/phrase.test.ts index faa884f0b..bc15cf667 100644 --- a/packages/core/src/routes/phrase.test.ts +++ b/packages/core/src/routes/phrase.test.ts @@ -64,7 +64,7 @@ afterEach(() => { jest.clearAllMocks(); }); -describe('when the application is admin-console', () => { +describe('when application is admin-console', () => { beforeEach(() => { interactionDetails.mockResolvedValueOnce({ params: { client_id: adminConsoleApplicationId }, @@ -100,7 +100,7 @@ describe('when the application is admin-console', () => { }); }); -describe('when the application is not admin-console', () => { +describe('when application is not admin-console', () => { it('should call interactionDetails', async () => { await expect(phraseRequest.get('/phrase')).resolves.toHaveProperty('status', 200); expect(interactionDetails).toBeCalledTimes(1); diff --git a/packages/core/src/routes/resource.test.ts b/packages/core/src/routes/resource.test.ts index 304fcdb5b..4c4101b88 100644 --- a/packages/core/src/routes/resource.test.ts +++ b/packages/core/src/routes/resource.test.ts @@ -25,8 +25,7 @@ jest.mock('@/queries/resource', () => ({ })); jest.mock('@logto/shared', () => ({ - // eslint-disable-next-line unicorn/consistent-function-scoping - buildIdGenerator: jest.fn(() => () => 'randomId'), + generateStandardId: jest.fn(() => 'randomId'), })); describe('resource routes', () => { diff --git a/packages/schemas/src/types/hook.ts b/packages/schemas/src/types/hook.ts new file mode 100644 index 000000000..5c03fe91a --- /dev/null +++ b/packages/schemas/src/types/hook.ts @@ -0,0 +1,5 @@ +export enum HookEvent { + PostSignIn = 'PostSignIn', + PostSignOut = 'PostSignOut', + PostChangePassword = 'PostChangePassword', +} diff --git a/packages/schemas/src/types/index.ts b/packages/schemas/src/types/index.ts index 9bb6fd003..8a5dbc1b7 100644 --- a/packages/schemas/src/types/index.ts +++ b/packages/schemas/src/types/index.ts @@ -3,3 +3,4 @@ export * from './log'; export * from './oidc-config'; export * from './user'; export * from './logto-config'; +export * from './hook';