mirror of
https://github.com/logto-io/logto.git
synced 2024-12-16 20:26:19 -05:00
refactor(test): split hook integration tests into several files (#3891)
This commit is contained in:
parent
2d8a226970
commit
37c3f5952d
6 changed files with 393 additions and 382 deletions
17
packages/integration-tests/src/helpers/hook.ts
Normal file
17
packages/integration-tests/src/helpers/hook.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
import { type Hook, type HookConfig, type HookEvent } from '@logto/schemas';
|
||||
|
||||
type HookCreationPayload = Pick<Hook, 'name' | 'events'> & {
|
||||
config: HookConfig;
|
||||
};
|
||||
|
||||
export const getHookCreationPayload = (
|
||||
event: HookEvent,
|
||||
url = 'not_work_url'
|
||||
): HookCreationPayload => ({
|
||||
name: 'hook_name',
|
||||
events: [event],
|
||||
config: {
|
||||
url,
|
||||
headers: { foo: 'bar' },
|
||||
},
|
||||
});
|
|
@ -30,8 +30,9 @@ export const registerNewUser = async (username: string, password: string) => {
|
|||
});
|
||||
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
await processSession(client, redirectTo);
|
||||
const userId = await processSession(client, redirectTo);
|
||||
await logoutClient(client);
|
||||
return userId;
|
||||
};
|
||||
|
||||
export const signInWithPassword = async (
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
import {
|
||||
type Hook,
|
||||
HookEvent,
|
||||
type HookResponse,
|
||||
type Log,
|
||||
LogResult,
|
||||
SignInIdentifier,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { authedAdminApi } from '#src/api/api.js';
|
||||
import { getHookCreationPayload } from '#src/helpers/hook.js';
|
||||
import { createMockServer } from '#src/helpers/index.js';
|
||||
import { registerNewUser } from '#src/helpers/interactions.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUserProfile } from '#src/helpers/user.js';
|
||||
|
||||
describe('hook logs', () => {
|
||||
const { listen, close } = createMockServer(9999);
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
await listen();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await close();
|
||||
});
|
||||
|
||||
it('should get recent hook logs correctly', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', {
|
||||
json: getHookCreationPayload(HookEvent.PostRegister, 'http://localhost:9999'),
|
||||
})
|
||||
.json<Hook>();
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const userId = await registerNewUser(username, password);
|
||||
|
||||
const logs = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}/recent-logs?page_size=100`)
|
||||
.json<Log[]>();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) =>
|
||||
hookId === createdHook.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
|
||||
it('should get hook execution stats correctly', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', {
|
||||
json: getHookCreationPayload(HookEvent.PostRegister, 'http://localhost:9999'),
|
||||
})
|
||||
.json<Hook>();
|
||||
|
||||
const hooksWithExecutionStats = await authedAdminApi
|
||||
.get('hooks?includeExecutionStats=true')
|
||||
.json<HookResponse[]>();
|
||||
|
||||
for (const hook of hooksWithExecutionStats) {
|
||||
expect(hook.executionStats).toBeTruthy();
|
||||
}
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const userId = await registerNewUser(username, password);
|
||||
|
||||
const hookWithExecutionStats = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}?includeExecutionStats=true`)
|
||||
.json<HookResponse>();
|
||||
|
||||
const { executionStats } = hookWithExecutionStats;
|
||||
|
||||
expect(executionStats).toBeTruthy();
|
||||
expect(executionStats.requestCount).toBe(1);
|
||||
expect(executionStats.successCount).toBe(1);
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
});
|
98
packages/integration-tests/src/tests/api/hook/hook.test.ts
Normal file
98
packages/integration-tests/src/tests/api/hook/hook.test.ts
Normal file
|
@ -0,0 +1,98 @@
|
|||
import type { Hook } from '@logto/schemas';
|
||||
import { HookEvent } from '@logto/schemas';
|
||||
|
||||
import { authedAdminApi } from '#src/api/index.js';
|
||||
import { createResponseWithCode } from '#src/helpers/admin-tenant.js';
|
||||
import { getHookCreationPayload } from '#src/helpers/hook.js';
|
||||
|
||||
describe('hooks', () => {
|
||||
it('should be able to create, query, update, and delete a hook', async () => {
|
||||
const payload = getHookCreationPayload(HookEvent.PostRegister);
|
||||
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);
|
||||
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);
|
||||
expect(
|
||||
await authedAdminApi
|
||||
.patch(`hooks/${created.id}`, { json: { event: HookEvent.PostSignIn } })
|
||||
.json<Hook>()
|
||||
).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',
|
||||
404
|
||||
);
|
||||
});
|
||||
|
||||
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)
|
||||
);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,185 @@
|
|||
import { createHmac } from 'node:crypto';
|
||||
import { type RequestListener } from 'node:http';
|
||||
|
||||
import {
|
||||
type Hook,
|
||||
HookEvent,
|
||||
type LogKey,
|
||||
LogResult,
|
||||
SignInIdentifier,
|
||||
type Log,
|
||||
} from '@logto/schemas';
|
||||
|
||||
import { deleteUser } from '#src/api/admin-user.js';
|
||||
import { authedAdminApi } from '#src/api/api.js';
|
||||
import { getLogs } from '#src/api/logs.js';
|
||||
import { getHookCreationPayload } from '#src/helpers/hook.js';
|
||||
import { createMockServer } from '#src/helpers/index.js';
|
||||
import { registerNewUser, signInWithPassword } from '#src/helpers/interactions.js';
|
||||
import { enableAllPasswordSignInMethods } from '#src/helpers/sign-in-experience.js';
|
||||
import { generateNewUser, generateNewUserProfile } from '#src/helpers/user.js';
|
||||
|
||||
type HookSecureData = {
|
||||
signature: string;
|
||||
payload: string;
|
||||
};
|
||||
|
||||
// Note: return hook payload and signature for webhook security testing
|
||||
const hookServerRequestListener: RequestListener = (request, response) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
response.statusCode = 204;
|
||||
|
||||
const data: Buffer[] = [];
|
||||
request.on('data', (chunk: Buffer) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
request.on('end', () => {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
const payload = Buffer.concat(data).toString();
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
signature: request.headers['logto-signature-sha-256'] as string,
|
||||
payload,
|
||||
} satisfies HookSecureData)
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
describe('trigger hooks', () => {
|
||||
const { listen, close } = createMockServer(9999, hookServerRequestListener);
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
await listen();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await close();
|
||||
});
|
||||
|
||||
it('should trigger sign-in hook and record error when interaction finished', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: getHookCreationPayload(HookEvent.PostSignIn) })
|
||||
.json<Hook>();
|
||||
const logKey: LogKey = 'TriggerHook.PostSignIn';
|
||||
|
||||
const {
|
||||
userProfile: { username, password },
|
||||
user,
|
||||
} = await generateNewUser({ username: true, password: true });
|
||||
|
||||
await signInWithPassword({ username, password });
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === createdHook.id &&
|
||||
result === LogResult.Error &&
|
||||
error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
// Clean up
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
await deleteUser(user.id);
|
||||
});
|
||||
|
||||
it('should trigger multiple register hooks and record properly when interaction finished', async () => {
|
||||
const [hook1, hook2, hook3] = await Promise.all([
|
||||
authedAdminApi
|
||||
.post('hooks', { json: getHookCreationPayload(HookEvent.PostRegister) })
|
||||
.json<Hook>(),
|
||||
authedAdminApi
|
||||
.post('hooks', {
|
||||
json: getHookCreationPayload(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';
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const userId = await registerNewUser(username, password);
|
||||
|
||||
// Check hook trigger log
|
||||
const logs = await getLogs(new URLSearchParams({ logKey, page_size: '100' }));
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result, error } }) =>
|
||||
hookId === hook1.id && result === LogResult.Error && error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
logs.some(
|
||||
({ 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(userId);
|
||||
});
|
||||
|
||||
it('should secure webhook payload data successfully', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', {
|
||||
json: getHookCreationPayload(HookEvent.PostRegister, 'http://localhost:9999'),
|
||||
})
|
||||
.json<Hook>();
|
||||
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const userId = await registerNewUser(username, password);
|
||||
|
||||
const logs = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}/recent-logs?page_size=100`)
|
||||
.json<Log[]>();
|
||||
|
||||
const log = logs.find(({ payload: { hookId } }) => hookId === createdHook.id);
|
||||
expect(log).toBeTruthy();
|
||||
|
||||
const response = log?.payload.response;
|
||||
expect(response).toBeTruthy();
|
||||
|
||||
const {
|
||||
body: { signature, payload },
|
||||
} = response as { body: HookSecureData };
|
||||
|
||||
expect(signature).toBeTruthy();
|
||||
expect(payload).toBeTruthy();
|
||||
|
||||
const calculateSignature = createHmac('sha256', createdHook.signingKey)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
|
||||
expect(calculateSignature).toEqual(signature);
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(userId);
|
||||
});
|
||||
});
|
|
@ -1,381 +0,0 @@
|
|||
import { createHmac } from 'node:crypto';
|
||||
import { type RequestListener } from 'node:http';
|
||||
|
||||
import type { Hook, HookConfig, HookResponse, Log, 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';
|
||||
|
||||
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' },
|
||||
},
|
||||
});
|
||||
|
||||
type HookSecureData = {
|
||||
signature: string;
|
||||
payload: string;
|
||||
};
|
||||
|
||||
// Note: return hook payload and signature for webhook security testing
|
||||
const hookServerRequestListener: RequestListener = (request, response) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutation
|
||||
response.statusCode = 204;
|
||||
|
||||
const data: Buffer[] = [];
|
||||
request.on('data', (chunk: Buffer) => {
|
||||
// eslint-disable-next-line @silverhand/fp/no-mutating-methods
|
||||
data.push(chunk);
|
||||
});
|
||||
|
||||
request.on('end', () => {
|
||||
response.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
const payload = Buffer.concat(data).toString();
|
||||
response.end(
|
||||
JSON.stringify({
|
||||
signature: request.headers['logto-signature-sha-256'] as string,
|
||||
payload,
|
||||
} satisfies HookSecureData)
|
||||
);
|
||||
});
|
||||
};
|
||||
describe('hooks', () => {
|
||||
const { listen, close } = createMockServer(9999, hookServerRequestListener);
|
||||
|
||||
beforeAll(async () => {
|
||||
await enableAllPasswordSignInMethods({
|
||||
identifiers: [SignInIdentifier.Username],
|
||||
password: true,
|
||||
verify: false,
|
||||
});
|
||||
await listen();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await close();
|
||||
});
|
||||
|
||||
it('should be able to create, query, update, and delete a hook', async () => {
|
||||
const payload = createPayload(HookEvent.PostRegister);
|
||||
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);
|
||||
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);
|
||||
expect(
|
||||
await authedAdminApi
|
||||
.patch(`hooks/${created.id}`, { json: { event: HookEvent.PostSignIn } })
|
||||
.json<Hook>()
|
||||
).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',
|
||||
404
|
||||
);
|
||||
});
|
||||
|
||||
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) })
|
||||
.json<Hook>();
|
||||
const logKey: LogKey = 'TriggerHook.PostSignIn';
|
||||
|
||||
// Init session and submit
|
||||
const {
|
||||
userProfile: { username, password },
|
||||
user,
|
||||
} = await generateNewUser({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.successSend(putInteraction, {
|
||||
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, error } }) =>
|
||||
hookId === createdHook.id &&
|
||||
result === LogResult.Error &&
|
||||
error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
// Clean up
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
await deleteUser(user.id);
|
||||
});
|
||||
|
||||
it('should trigger multiple register hooks and record properly when interaction finished', async () => {
|
||||
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';
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
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, error } }) =>
|
||||
hookId === hook1.id && result === LogResult.Error && error === 'RequestError: Invalid URL'
|
||||
)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
logs.some(
|
||||
({ 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);
|
||||
});
|
||||
|
||||
it('should get recent hook logs correctly', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
|
||||
.json<Hook>();
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
const logs = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}/recent-logs?page_size=100`)
|
||||
.json<Log[]>();
|
||||
expect(
|
||||
logs.some(
|
||||
({ payload: { hookId, result } }) =>
|
||||
hookId === createdHook.id && result === LogResult.Success
|
||||
)
|
||||
).toBeTruthy();
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(id);
|
||||
});
|
||||
|
||||
it('should secure webhook payload data successfully', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
|
||||
.json<Hook>();
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
const logs = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}/recent-logs?page_size=100`)
|
||||
.json<Log[]>();
|
||||
|
||||
const log = logs.find(({ payload: { hookId } }) => hookId === createdHook.id);
|
||||
expect(log).toBeTruthy();
|
||||
|
||||
const response = log?.payload.response;
|
||||
expect(response).toBeTruthy();
|
||||
|
||||
const {
|
||||
body: { signature, payload },
|
||||
} = response as { body: HookSecureData };
|
||||
|
||||
expect(signature).toBeTruthy();
|
||||
expect(payload).toBeTruthy();
|
||||
|
||||
const calculateSignature = createHmac('sha256', createdHook.signingKey)
|
||||
.update(payload)
|
||||
.digest('hex');
|
||||
|
||||
expect(calculateSignature).toEqual(signature);
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(id);
|
||||
});
|
||||
|
||||
it('should get hook execution stats correctly', async () => {
|
||||
const createdHook = await authedAdminApi
|
||||
.post('hooks', { json: createPayload(HookEvent.PostRegister, 'http://localhost:9999') })
|
||||
.json<Hook>();
|
||||
|
||||
const hooksWithExecutionStats = await authedAdminApi
|
||||
.get('hooks?includeExecutionStats=true')
|
||||
.json<HookResponse[]>();
|
||||
|
||||
for (const hook of hooksWithExecutionStats) {
|
||||
expect(hook.executionStats).toBeTruthy();
|
||||
}
|
||||
|
||||
// Init session and submit
|
||||
const { username, password } = generateNewUserProfile({ username: true, password: true });
|
||||
const client = await initClient();
|
||||
await client.send(putInteraction, {
|
||||
event: InteractionEvent.Register,
|
||||
profile: {
|
||||
username,
|
||||
password,
|
||||
},
|
||||
});
|
||||
const { redirectTo } = await client.submitInteraction();
|
||||
const id = await processSession(client, redirectTo);
|
||||
await waitFor(500); // Wait for hooks execution
|
||||
|
||||
const hookWithExecutionStats = await authedAdminApi
|
||||
.get(`hooks/${createdHook.id}?includeExecutionStats=true`)
|
||||
.json<HookResponse>();
|
||||
|
||||
const { executionStats } = hookWithExecutionStats;
|
||||
|
||||
expect(executionStats).toBeTruthy();
|
||||
expect(executionStats.requestCount).toBe(1);
|
||||
expect(executionStats.successCount).toBe(1);
|
||||
|
||||
await authedAdminApi.delete(`hooks/${createdHook.id}`);
|
||||
|
||||
await deleteUser(id);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue