diff --git a/.gitignore b/.gitignore index dcdd7bc..8c49e68 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,6 @@ obj node_modules dist +.idea/* + local.settings.json diff --git a/enums/Operation.ts b/enums/Operation.ts new file mode 100644 index 0000000..a65ba93 --- /dev/null +++ b/enums/Operation.ts @@ -0,0 +1,4 @@ +export enum Operation { + Upsert = 'upsert', + ChangeWorkflowStep = 'change_workflow_step', +} diff --git a/kcd-webhook-service/Configuration.ts b/kcd-webhook-service/Configuration.ts new file mode 100644 index 0000000..a8c1bff --- /dev/null +++ b/kcd-webhook-service/Configuration.ts @@ -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']) || ''; + } +} diff --git a/kcd-webhook-service/eventComposer.test.ts b/kcd-webhook-service/eventComposer.test.ts index 4c861a3..2f77a56 100644 --- a/kcd-webhook-service/eventComposer.test.ts +++ b/kcd-webhook-service/eventComposer.test.ts @@ -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(); + }); }); diff --git a/kcd-webhook-service/eventComposer.ts b/kcd-webhook-service/eventComposer.ts index 9c086e7..2f9edce 100644 --- a/kcd-webhook-service/eventComposer.ts +++ b/kcd-webhook-service/eventComposer.ts @@ -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', id: getUuid(), - subject: webhookBody.message.operation - }; -}; + subject: body.message.operation, +}); diff --git a/kcd-webhook-service/helpers.ts b/kcd-webhook-service/helpers.ts new file mode 100644 index 0000000..8501174 --- /dev/null +++ b/kcd-webhook-service/helpers.ts @@ -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; +}; diff --git a/kcd-webhook-service/index.test.ts b/kcd-webhook-service/index.test.ts index 52aaf53..161e5e4 100644 --- a/kcd-webhook-service/index.test.ts +++ b/kcd-webhook-service/index.test.ts @@ -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'); + }); }); diff --git a/kcd-webhook-service/index.ts b/kcd-webhook-service/index.ts index ddb044a..9257f48 100644 --- a/kcd-webhook-service/index.ts +++ b/kcd-webhook-service/index.ts @@ -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 => { - 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 => { + 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; diff --git a/kcd-webhook-service/publishEventsCreator.test.ts b/kcd-webhook-service/publishEventsCreator.test.ts index ff2072c..01b678d 100644 --- a/kcd-webhook-service/publishEventsCreator.test.ts +++ b/kcd-webhook-service/publishEventsCreator.test.ts @@ -1,30 +1,30 @@ import { publishEventsCreator } from './publishEventsCreator'; const eventGridClient = { - publishEvents: jest.fn() + publishEvents: jest.fn(), }; const host = 'fake.url-to-webhook.cloud'; const fakeHost = `http://${host}/api/webhook`; const events = [ - { - data: { xxx: 'xxx' }, - dataVersion: '1.0', - eventTime: new Date(), - eventType: 'test_event', - subject: 'test' - } + { + data: {xxx: 'xxx'}, + dataVersion: '1.0', + eventTime: new Date(), + eventType: 'test_event', + subject: 'test', + }, ]; describe('publishEvents', () => { - test('calls publishEvents with correct host and events', async () => { - const deps = { - eventGridClient, - host: fakeHost - }; + test('calls publishEvents with correct host and events', async () => { + const deps = { + eventGridClient, + host: fakeHost, + }; - await publishEventsCreator(deps as any)(events as any); + await publishEventsCreator(deps as any)(events as any); - expect(eventGridClient.publishEvents.mock.calls[0][0]).toBe(host); - expect(eventGridClient.publishEvents.mock.calls[0][1]).toBe(events); - }); + expect(eventGridClient.publishEvents.mock.calls[0][0]).toBe(host); + expect(eventGridClient.publishEvents.mock.calls[0][1]).toBe(events); + }); }); diff --git a/tsconfig.json b/tsconfig.json index e9fd89b..5accd5e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "strictNullChecks": true, "strictFunctionTypes": true, "noUnusedLocals": true, - "noUnusedParameters": true, + "noUnusedParameters": false, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, "types": ["node", "jest"], @@ -22,4 +22,4 @@ "files": [ "./@types/global.d.ts" ] -} \ No newline at end of file +} diff --git a/tslint.json b/tslint.json index 0402fd4..b3b9838 100644 --- a/tslint.json +++ b/tslint.json @@ -1,8 +1,7 @@ { "defaultSeverity": "error", "extends": [ - "tslint:recommended", - "tslint-config-standard" + "tslint:recommended" ], "rules": { "await-promise": true, @@ -12,6 +11,9 @@ "no-unnecessary-qualifier": true, "no-unnecessary-type-assertion": false, "semicolon": true, - "strict-type-predicates": true + "strict-type-predicates": true, + "quotemark": [ + true, "single" + ] } }