Skip to content

Commit

Permalink
test(core): sign webhook payload data
Browse files Browse the repository at this point in the history
  • Loading branch information
xiaoyijun committed May 20, 2023
1 parent 07d58f8 commit 18c378f
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 49 deletions.
103 changes: 54 additions & 49 deletions packages/core/src/libraries/hook/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
import {
type Application,
ApplicationType,
type Hook,
type HookEventPayload,
HookEvent,
InteractionEvent,
LogResult,
type User,
type Application,
ApplicationType,
type userInfoSelectFields,
type User,
type Hook,
} from '@logto/schemas';
import { createMockUtils } from '@logto/shared/esm';
import { got } from 'got';

import type { Interaction } from './index.js';
import { createHookRequestOptions } from './utils.js';
import { parseResponse } from './utils.js';

const { jest } = import.meta;
const { mockEsmWithActual, mockEsm } = createMockUtils(jest);
Expand All @@ -25,24 +25,7 @@ await mockEsmWithActual('@logto/shared', () => ({
generateStandardId: () => nanoIdMock,
}));

mockEsm('#src/utils/signature.js', () => {
return { generateSignature: jest.fn(() => 'mock-signature') };
});

const { MockQueries } = await import('#src/test-utils/tenant.js');

const url = 'https://logto.gg';
const hook: Hook = {
tenantId: 'bar',
id: 'foo',
name: 'hook_name',
event: HookEvent.PostSignIn,
events: [HookEvent.PostSignIn],
signingKey: 'signing_key',
enabled: true,
config: { headers: { bar: 'baz' }, url, retries: 3 },
createdAt: Date.now() / 1000,
};
const mockUserAgent = 'Mock User Agent';

const mockApplication: Pick<Application, 'id' | 'type' | 'name' | 'description'> = {
id: 'app_id',
Expand All @@ -68,7 +51,48 @@ const mockUser: {
isSuspended: false,
};

const mockUserAgent = 'Mock User Agent';
const url = 'https://logto.gg';
const mockEvent = HookEvent.PostSignIn;
const hook: Hook = {
tenantId: 'bar',
id: 'foo',
name: 'hook_name',
event: mockEvent,
events: [mockEvent],
signingKey: 'signing_key',
enabled: true,
config: { headers: { bar: 'baz' }, url, retries: 3 },
createdAt: Date.now() / 1000,
};

const mockHookEventPayload: HookEventPayload = {
hookId: hook.id,
event: mockEvent,
interactionEvent: 'SignIn',
createdAt: new Date(100_000).toISOString(),
sessionId: 'some_jti',
userAgent: 'Mock User Agent',
userId: '123',
user: mockUser,
application: mockApplication,
};

const mockHookRequestOptions = {
headers: {
'user-agent': 'Logto (https://logto.io)',
'x-logto-signature-256': 'mock_signature',
},
json: mockHookEventPayload,
retry: { limit: 3 },
timeout: { request: 10_000 },
};

mockEsm('./utils.js', () => ({
parseResponse,
createHookRequestOptions: jest.fn(() => mockHookRequestOptions),
}));

const { MockQueries } = await import('#src/test-utils/tenant.js');

const post = jest
.spyOn(got, 'post')
Expand Down Expand Up @@ -117,36 +141,17 @@ describe('triggerInteractionHooksIfNeeded()', () => {
mockUserAgent
);

const expectedPayload: HookEventPayload = {
hookId: 'foo',
event: HookEvent.PostSignIn,
interactionEvent: 'SignIn',
createdAt: new Date(100_000).toISOString(),
sessionId: 'some_jti',
userAgent: mockUserAgent,
userId: '123',
user: mockUser,
application: mockApplication,
};

const expectedWebhookRequestOptions = createHookRequestOptions({
signingKey: hook.signingKey,
payload: expectedPayload,
customHeaders: hook.config.headers,
retries: hook.config.retries,
});

expect(findAllHooks).toHaveBeenCalled();
expect(post).toHaveBeenCalledWith(url, expectedWebhookRequestOptions);
expect(post).toHaveBeenCalledWith(url, mockHookRequestOptions);
const calledPayload: unknown = insertLog.mock.calls[0][0];
expect(calledPayload).toHaveProperty('id', nanoIdMock);
expect(calledPayload).toHaveProperty('key', 'TriggerHook.' + HookEvent.PostSignIn);
expect(calledPayload).toHaveProperty('key', 'TriggerHook.' + mockEvent);
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.hookId', hook.id);
expect(calledPayload).toHaveProperty('payload.json.event', mockHookEventPayload.event);
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.json.hookId', mockHookEventPayload.hookId);
expect(calledPayload).toHaveProperty('payload.json.userId', mockHookEventPayload.userId);
expect(calledPayload).toHaveProperty('payload.response.statusCode', 200);
expect(calledPayload).toHaveProperty('payload.response.body.message', 'ok');
jest.useRealTimers();
Expand Down
90 changes: 90 additions & 0 deletions packages/core/src/libraries/hook/utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { HookEvent, type HookEventPayload } from '@logto/schemas';
import { createMockUtils } from '@logto/shared/esm';

const { jest } = import.meta;
const { mockEsm } = createMockUtils(jest);

const mockSignature = 'mock-signature';
const { generateSignature } = mockEsm('#src/utils/signature.js', () => ({
generateSignature: jest.fn().mockReturnValue(mockSignature),
}));

const { createHookRequestOptions } = await import('#src/libraries/hook/utils.js');

describe('createHookRequestOptions', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should call generateSignature with correct values', () => {
const signingKey = 'mock-signing-key';
const payload: HookEventPayload = {
hookId: 'hookId',
event: HookEvent.PostSignIn,
createdAt: '123456',
};

createHookRequestOptions({ signingKey, payload });

expect(generateSignature).toBeCalledWith(signingKey, payload);
});

it('should create correct hook request options', () => {
const signingKey = 'mock-signing-key';
const payload: HookEventPayload = {
hookId: 'hookId',
event: HookEvent.PostSignIn,
createdAt: '123456',
};

const customHeaders = {
'x-custom-header': 'custom-header',
'x-logto-signature-256': 'custom-signature',
};

const options = createHookRequestOptions({ signingKey, payload, customHeaders });

expect(options).toEqual({
headers: {
'user-agent': 'Logto (https://logto.io)',
'x-custom-header': 'custom-header',
'x-logto-signature-256': mockSignature,
},
json: payload,
retry: { limit: 3 },
timeout: { request: 10_000 },
});
});

it('ensure the x-logto-signature-256 header is not set when signingKey is not provided', () => {
const signingKey = '';
const payload: HookEventPayload = {
hookId: 'hookId',
event: HookEvent.PostSignIn,
createdAt: '123456',
};

const options = createHookRequestOptions({ signingKey, payload });

expect(options).toBeTruthy();
expect(options.headers).not.toHaveProperty('x-logto-signature-256');
});

it('ensure the reserved x-logto-signature-256 header will not be overridden', () => {
const signingKey = 'mock-signing-key';
const payload: HookEventPayload = {
hookId: 'hookId',
event: HookEvent.PostSignIn,
createdAt: '123456',
};

const customHeaders = {
'x-logto-signature-256': 'custom-signature',
};

const options = createHookRequestOptions({ signingKey, payload, customHeaders });

expect(options).toBeTruthy();
expect(options.headers).toHaveProperty('x-logto-signature-256', mockSignature);
});
});

0 comments on commit 18c378f

Please sign in to comment.