jackson/npm/test/dsync/webhooks.test.ts

304 lines
9.2 KiB
TypeScript

import { IDirectorySyncController, Directory, DirectorySyncEvent } from '../../src/typings';
import tap from 'tap';
import groups from './data/groups';
import users from './data/users';
import { default as usersRequest } from './data/user-requests';
import { default as groupRequest } from './data/group-requests';
import { getFakeDirectory } from './data/directories';
import { jacksonOptions } from '../utils';
import sinon from 'sinon';
import axios from '../../src/event/axios';
import { createSignatureString } from '../../src/event/webhook';
let directorySync: IDirectorySyncController;
let directory: Directory;
const fakeDirectory = getFakeDirectory();
const webhook: Directory['webhook'] = {
endpoint: 'http://localhost',
secret: 'secret',
};
tap.before(async () => {
const jackson = await (await import('../../src/index')).default(jacksonOptions);
directorySync = jackson.directorySyncController;
// Create a directory before starting the test
const { data, error } = await directorySync.directories.create({
...fakeDirectory,
webhook_url: webhook.endpoint,
webhook_secret: webhook.secret,
});
if (error || !data) {
tap.fail("Couldn't create a directory");
return;
}
directory = data;
// Turn on webhook event logging for the directory
await directorySync.directories.update(directory.id, {
log_webhook_events: true,
});
directorySync.webhookLogs.setTenantAndProduct(directory.tenant, directory.product);
directorySync.users.setTenantAndProduct(directory.tenant, directory.product);
});
tap.teardown(async () => {
process.exit(0);
});
tap.test('Webhook Events /', async (t) => {
t.teardown(async () => {
await directorySync.directories.delete(directory.id);
});
t.test('Webhook Events / ', async (t) => {
t.afterEach(async () => {
await directorySync.webhookLogs.deleteAll(directory.id);
await directorySync.users.deleteAll(directory.id);
});
t.test("Should be able to get the directory's webhook", async (t) => {
t.match(directory.webhook.endpoint, webhook.endpoint);
t.match(directory.webhook.secret, webhook.secret);
});
t.test('Should not log events if the directory has no webhook', async (t) => {
await directorySync.directories.update(directory.id, {
webhook: {
endpoint: '',
secret: '',
},
});
// Create a user
await directorySync.requests.handle(usersRequest.create(directory, users[0]));
const { data: events } = await directorySync.webhookLogs.getAll();
t.equal(events.length, 0);
// Restore the directory's webhook
await directorySync.directories.update(directory.id, {
webhook: {
endpoint: webhook.endpoint,
secret: webhook.secret,
},
});
});
t.test('Should not log webhook events if the logging is turned off', async (t) => {
// Turn off webhook event logging for the directory
await directorySync.directories.update(directory.id, {
log_webhook_events: false,
});
// Create a user
await directorySync.requests.handle(usersRequest.create(directory, users[0]));
const { data: events } = await directorySync.webhookLogs.getAll();
t.equal(events.length, 0);
// Turn on webhook event logging for the directory
await directorySync.directories.update(directory.id, {
log_webhook_events: true,
});
});
t.test('Should be able to get an event by id', async (t) => {
// Create a user
await directorySync.requests.handle(usersRequest.create(directory, users[0]));
const { data: logs } = await directorySync.webhookLogs.getAll();
const log = await directorySync.webhookLogs.get(logs[0].id);
t.equal(log.id, logs[0].id);
});
t.test('Should send user related events', async (t) => {
const mock = sinon.mock(axios);
mock.expects('post').thrice().withArgs(webhook.endpoint).throws();
// Create the user
const { data: createdUser } = await directorySync.requests.handle(
usersRequest.create(directory, users[0])
);
// Update the user
const { data: updatedUser } = await directorySync.requests.handle(
usersRequest.updateById(directory, createdUser.id, users[0])
);
// Delete the user
const { data: deletedUser } = await directorySync.requests.handle(
usersRequest.deleteById(directory, createdUser.id)
);
mock.verify();
mock.restore();
const { data: logs } = await directorySync.webhookLogs.getAll();
t.ok(logs);
t.equal(logs.length, 3);
if (!Array.isArray(logs[0].payload)) {
t.match(logs[0].payload.event, 'user.deleted');
t.match(logs[0].payload.directory_id, directory.id);
t.hasStrict(logs[0].payload.data.raw, deletedUser);
}
if (!Array.isArray(logs[1].payload)) {
t.match(logs[1].payload.event, 'user.updated');
t.match(logs[1].payload.directory_id, directory.id);
t.hasStrict(logs[1].payload.data.raw, updatedUser);
}
if (!Array.isArray(logs[2].payload)) {
t.match(logs[2].payload.event, 'user.created');
t.match(logs[2].payload.directory_id, directory.id);
t.hasStrict(logs[2].payload.data.raw, createdUser);
}
await directorySync.users.deleteAll(directory.id);
});
t.test('Should send group related events', async (t) => {
const mock = sinon.mock(axios);
mock.expects('post').thrice().withArgs(webhook.endpoint).throws();
// Create the group
const { data: createdGroup } = await directorySync.requests.handle(
groupRequest.create(directory, groups[0])
);
// Update the group
const { data: updatedGroup } = await directorySync.requests.handle(
groupRequest.updateById(directory, createdGroup.id, groups[0])
);
// Delete the group
const { data: deletedGroup } = await directorySync.requests.handle(
groupRequest.deleteById(directory, createdGroup.id)
);
mock.verify();
mock.restore();
const { data: logs } = await directorySync.webhookLogs.getAll();
t.ok(logs);
t.equal(logs.length, 3);
if (!Array.isArray(logs[0].payload)) {
t.match(logs[0].payload.event, 'group.deleted');
t.match(logs[0].payload.directory_id, directory.id);
t.hasStrict(logs[0].payload.data.raw, deletedGroup);
}
if (!Array.isArray(logs[1].payload)) {
t.match(logs[1].payload.event, 'group.updated');
t.match(logs[1].payload.directory_id, directory.id);
t.hasStrict(logs[1].payload.data.raw, updatedGroup);
}
if (!Array.isArray(logs[2].payload)) {
t.match(logs[2].payload.event, 'group.created');
t.match(logs[2].payload.directory_id, directory.id);
t.hasStrict(logs[2].payload.data.raw, createdGroup);
}
});
});
t.test('Should send group membership related events', async (t) => {
const mock = sinon.mock(axios);
mock.expects('post').exactly(4).withArgs(webhook.endpoint).throws();
// Create the user
const { data: createdUser } = await directorySync.requests.handle(
usersRequest.create(directory, users[0])
);
// Create the group
const { data: createdGroup } = await directorySync.requests.handle(
groupRequest.create(directory, groups[0])
);
// Add the user to the group
await directorySync.requests.handle(
groupRequest.addMembers(directory, createdGroup.id, [{ value: createdUser.id }])
);
// Remove the user from the group
await directorySync.requests.handle(
groupRequest.removeMembers(
directory,
createdGroup.id,
[{ value: createdUser.id }],
`members[value eq "${createdUser.id}"]`
)
);
mock.verify();
mock.restore();
const { data: logs } = await directorySync.webhookLogs.getAll();
t.ok(logs);
t.equal(logs.length, 4);
if (!Array.isArray(logs[0].payload)) {
t.match(logs[0].payload.event, 'group.user_removed');
t.match(logs[0].payload.directory_id, directory.id);
t.hasStrict(logs[0].payload.data.raw, createdUser);
}
if (!Array.isArray(logs[1].payload)) {
t.match(logs[1].payload.event, 'group.user_added');
t.match(logs[1].payload.directory_id, directory.id);
t.hasStrict(logs[1].payload.data.raw, createdUser);
}
await directorySync.users.delete(createdUser.id);
await directorySync.groups.delete(createdGroup.id);
});
t.test('createSignatureString()', async (t) => {
const event: DirectorySyncEvent = {
event: 'user.created',
directory_id: directory.id,
tenant: directory.tenant,
product: directory.product,
data: {
raw: [],
id: 'user-id',
first_name: 'Kiran',
last_name: 'Krishnan',
email: 'kiran@boxyhq.com',
active: true,
},
};
const signatureString = createSignatureString(directory.webhook.secret, event);
const parts = signatureString.split(',');
t.ok(signatureString);
t.ok(parts[0].match(/^t=[0-9a-f]/));
t.ok(parts[1].match(/^s=[0-9a-f]/));
// Empty secret should create an empty signature
const emptySignatureString = createSignatureString('', event);
t.match(emptySignatureString, '');
});
});