Skip to content
This repository has been archived by the owner on Nov 16, 2023. It is now read-only.

KA-559 Rework publisher to react to webhooks #26

Open
wants to merge 1 commit into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ obj
node_modules
dist

.idea/*

local.settings.json
4 changes: 4 additions & 0 deletions enums/Operation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export enum Operation {
Upsert = 'upsert',
ChangeWorkflowStep = 'change_workflow_step',
}
18 changes: 18 additions & 0 deletions kcd-webhook-service/Configuration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { HttpRequest } from '@azure/functions/Interfaces';
import { Operation } from '../enums/Operation';

export class Configuration {
public static eventGridKey: string;
public static eventGridHost: string;

public static set(request: HttpRequest): void {
const isWorkflowStepChange = request.body.message.operation === Operation.ChangeWorkflowStep;

this.eventGridKey = (isWorkflowStepChange
? process.env['EventGrid.WorkflowChanged.Key']
: process.env['EventGrid.DocsChanged.Key']) || '';
this.eventGridHost = (isWorkflowStepChange
? process.env['EventGrid.WorkflowChanged.Endpoint']
: process.env['EventGrid.DocsChanged.Endpoint']) || '';
}
}
105 changes: 63 additions & 42 deletions kcd-webhook-service/eventComposer.test.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,75 @@
import { eventComposer } from './eventComposer';

const webhookBody = {
data: {
xxx: 'xxx',
yyy: 'yyy'
},
message: {
operation: 'someOperation',
type: ''
}
const body = {
data: {
xxx: 'xxx',
yyy: 'yyy',
},
message: {
operation: 'someOperation',
type: '',
},
};
const eventType = 'kentico-cloud';

describe('eventComposer', () => {
test('composes event with data from webhook', async () => {
const isTest = undefined;
const event = eventComposer(webhookBody, isTest);
test('composes event with data from webhook', async () => {
const isTest = undefined;
const request = {
body,
query: {
source: eventType,
test: isTest,
},
} as any;
const event = eventComposer(request);

expect(event.id).toBeTruthy();
expect(event.subject).toBe(webhookBody.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.dataVersion).toBe('1.0');
expect(event.data.webhook).toBe(webhookBody.data);
expect(event.data.test).toBe('disabled');
expect(event.eventTime).toBeTruthy();
});
expect(event.id).toBeTruthy();
expect(event.subject).toBe(body.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.dataVersion).toBe('1.0');
expect(event.data.webhook).toBe(body.data);
expect(event.data.test).toBe('disabled');
expect(event.eventTime).toBeTruthy();
});

test('composes event with testing configuration', async () => {
const isTest = 'enabled';
const event = eventComposer(webhookBody, isTest);
test('composes event with testing configuration', async () => {
const isTest = 'enabled';
const request = {
body,
query: {
source: eventType,
test: isTest,
},
} as any;
const event = eventComposer(request);

expect(event.id).toBeTruthy();
expect(event.subject).toBe(webhookBody.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.dataVersion).toBe('1.0');
expect(event.data.webhook).toBe(webhookBody.data);
expect(event.data.test).toBe('enabled');
expect(event.eventTime).toBeTruthy();
});
expect(event.id).toBeTruthy();
expect(event.subject).toBe(body.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.dataVersion).toBe('1.0');
expect(event.data.webhook).toBe(body.data);
expect(event.data.test).toBe('enabled');
expect(event.eventTime).toBeTruthy();
});

test('composes event with incorrect testing configuration', async () => {
const isTest = 'something';
const event = eventComposer(webhookBody, isTest);
test('composes event with incorrect testing configuration', async () => {
const isTest = 'something';
const request = {
body,
query: {
source: eventType,
test: isTest,
},
} as any;
const event = eventComposer(request);

expect(event.id).toBeTruthy();
expect(event.subject).toBe(webhookBody.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.dataVersion).toBe('1.0');
expect(event.data.webhook).toBe(webhookBody.data);
expect(event.data.test).toBe('disabled');
expect(event.eventTime).toBeTruthy();
});
expect(event.id).toBeTruthy();
expect(event.subject).toBe(body.message.operation);
expect(event.eventType).toBe(eventType);
expect(event.data.webhook).toBe(body.data);
expect(event.dataVersion).toBe('1.0');
expect(event.data.test).toBe('disabled');
expect(event.eventTime).toBeTruthy();
});
});
19 changes: 6 additions & 13 deletions kcd-webhook-service/eventComposer.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,15 @@
import { HttpRequest } from '@azure/functions/Interfaces';
import { EventGridModels } from 'azure-eventgrid';
import getUuid from 'uuid';
import { RequestBody } from '../@types/global';

export const eventComposer = (
webhookBody: RequestBody,
test?: string
): EventGridModels.EventGridEvent => {
const isTest = test === 'enabled' ? 'enabled' : 'disabled';

return {
export const eventComposer = ({body, query: {test}}: HttpRequest): EventGridModels.EventGridEvent => ({
data: {
test: isTest,
webhook: webhookBody.data
test: test === 'enabled' ? test : 'disabled',
webhook: body.data,
},
dataVersion: '1.0',
eventTime: new Date(),
eventType: 'kentico-cloud',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

EventType should be different for a different data structure.

id: getUuid(),
subject: webhookBody.message.operation
};
};
subject: body.message.operation,
});
31 changes: 31 additions & 0 deletions kcd-webhook-service/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import EventGridClient from 'azure-eventgrid';
import { TopicCredentials } from 'ms-rest-azure';
import {
HttpRequest,
RequestBody,
} from '../@types/global';
import { Operation } from '../enums/Operation';
import { Configuration } from './Configuration';
import { eventComposer } from './eventComposer';
import { publishEventsCreator } from './publishEventsCreator';

export const isRequestBodyValid = (body: any): body is RequestBody =>
body
&& body.message
&& body.message.type
&& body.message.operation
&& body.data;

export const shouldIgnoreRequest = ({ message: { type, operation } }: RequestBody): boolean =>
type === 'content_item' && operation !== Operation.Upsert;

export const publishEventWithWebhookData = async (request: HttpRequest) => {
const topicCredentials = new TopicCredentials(Configuration.eventGridKey);
const eventGridClient = new EventGridClient(topicCredentials);
const publishEvents = publishEventsCreator({eventGridClient, host: Configuration.eventGridHost});

const event = eventComposer(request);
await publishEvents([event]);

return event;
};
48 changes: 24 additions & 24 deletions kcd-webhook-service/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
import azureFunction from './index';

describe('Azure function fails', () => {
let context = {};
let context = {};

beforeEach(() => {
context = {
done: jest.fn(),
res: null
};
});
beforeEach(() => {
context = {
done: jest.fn(),
res: null,
};
});

test('returns 200 but does nothing on kentico-cloud and content_item', async () => {
const request = {
body: {
data: 'anything',
message: {
operation: 'anything',
type: 'content_item'
}
},
query: {
source: 'kentico-cloud'
}
};
test('returns 200 but does nothing on kentico-cloud and content_item', async () => {
const request = {
body: {
data: 'anything',
message: {
operation: 'anything',
type: 'content_item',
},
},
query: {
source: 'kentico-cloud',
},
};

const response = await azureFunction(context as any, request, null);
const response = await azureFunction(context as any, request, null);

expect(response.status).toBe(200);
expect(response.body).toBe('Nothing published');
});
expect(response.status).toBe(200);
expect(response.body).toBe('Nothing published');
});
});
92 changes: 39 additions & 53 deletions kcd-webhook-service/index.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,43 @@
import { AzureFunction, Context } from '@azure/functions';
import EventGridClient from 'azure-eventgrid';
import { TopicCredentials } from 'ms-rest-azure';
import { HttpRequest, HttpResponse, RequestBody } from '../@types/global';
import { eventComposer } from './eventComposer';
import { publishEventsCreator } from './publishEventsCreator';

const isRequestBodyValid = (body: any): body is RequestBody =>
body
&& body.message
&& body.message.type
&& body.message.operation
&& body.data;

const shouldIgnoreRequest = ({ message: { type, operation } }: RequestBody): boolean =>
type === 'content_item' && operation !== 'upsert';

const parseWebhook: AzureFunction = async (
_context: Context,
request: HttpRequest
): Promise<HttpResponse> => {
try {
if (!isRequestBodyValid(request.body)) {
throw new Error('Received invalid message body');
}

if (shouldIgnoreRequest(request.body)) {
return {
body: 'Nothing published',
status: 200
};
import {
AzureFunction,
Context,
} from '@azure/functions';
import {
HttpRequest,
HttpResponse,
} from '../@types/global';
import { Configuration } from './Configuration';
import {
isRequestBodyValid,
publishEventWithWebhookData,
shouldIgnoreRequest,
} from './helpers';

const parseWebhook: AzureFunction = async (context: Context, request: HttpRequest): Promise<HttpResponse> => {
try {
if (!isRequestBodyValid(request.body)) {
throw new Error('Received invalid message body');
}

if (shouldIgnoreRequest(request.body)) {
return {
body: 'Nothing published',
status: 200,
};
}

Configuration.set(request);

const event = await publishEventWithWebhookData(request);

return {
body: event,
status: 200,
};
} catch (error) {
/** This try-catch is required for correct logging of exceptions in Azure */
throw `Message: ${error.message} \nStack Trace: ${error.stack}`;
}

const eventGridKey = process.env['EventGrid.DocsChanged.Key'];
const host = process.env['EventGrid.DocsChanged.Endpoint'];
if (!eventGridKey || !host) {
throw new Error('Undefined env property provided');
}

const topicCredentials = new TopicCredentials(eventGridKey);
const eventGridClient = new EventGridClient(topicCredentials);
const publishEvents = publishEventsCreator({ eventGridClient, host });

const event = eventComposer(request.body, request.query.test);
await publishEvents([event]);

return {
body: event,
status: 200
};
} catch (error) {
/** This try-catch is required for correct logging of exceptions in Azure */
throw `Message: ${error.message} \nStack Trace: ${error.stack}`;
}
};

export default parseWebhook;
Loading