diff --git a/src/lib/addons/__snapshots__/teams.test.ts.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap index 7f25aeaa35..fe2875686a 100644 --- a/src/lib/addons/__snapshots__/teams.test.ts.snap +++ b/src/lib/addons/__snapshots__/teams.test.ts.snap @@ -1,16 +1,16 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should call teams webhook 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-created"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//features/some-toggle"}]}]}"`; +exports[`Teams integration Should call teams webhook 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* created *[some-toggle](http://some-url.com/projects//features/some-toggle)* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-created"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//features/some-toggle"}]}]}"`; -exports[`Should call teams webhook for archived toggle 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//archive"}]}]}"`; +exports[`Teams integration Should call teams webhook for archived toggle 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project **","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects//archive"}]}]}"`; -exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`; +exports[`Teams integration Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* archived *some-toggle* in project *[some-project](http://some-url.com/projects/some-project)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`; -exports[`Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Teams integration Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; -exports[`Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Teams integration Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"*some@user.com* disabled *[some-toggle](http://some-url.com/projects/default/features/some-toggle)* for the *development* environment in project *[default](http://some-url.com/projects/default)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; -exports[`Should include custom headers in call to teams 2`] = ` +exports[`Teams integration Should include custom headers in call to teams 2`] = ` { "Content-Type": "application/json", "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE", diff --git a/src/lib/addons/teams.test.ts b/src/lib/addons/teams.test.ts index e36bd5881c..dcf7a38caa 100644 --- a/src/lib/addons/teams.test.ts +++ b/src/lib/addons/teams.test.ts @@ -13,11 +13,13 @@ import noLogger from '../../test/fixtures/no-logger'; import { type IAddonConfig, type IFlagResolver, + serializeDates, SYSTEM_USER_ID, } from '../types'; import type { IntegrationEventsService } from '../services'; let fetchRetryCalls: any[]; +const registerEventMock = jest.fn(); const INTEGRATION_ID = 1337; const ARGS: IAddonConfig = { @@ -45,142 +47,216 @@ jest.mock( retries, backoff, }); - return Promise.resolve({ status: 200 }); + return Promise.resolve({ ok: true, status: 200 }); } - async registerEvent(_) { - return Promise.resolve(); + async registerEvent(event) { + return registerEventMock(event); } }, ); -test('Should call teams webhook', async () => { - const addon = new TeamsAddon(ARGS); - const event: IEvent = { - id: 1, - createdAt: new Date(), - type: FEATURE_CREATED, - createdByUserId: SYSTEM_USER_ID, - createdBy: 'some@user.com', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - enabled: false, - strategies: [{ name: 'default' }], - }, - }; +describe('Teams integration', () => { + beforeEach(() => { + registerEventMock.mockClear(); + }); - const parameters = { - url: 'http://hooks.office.com', - }; + test('Should call teams webhook', async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + type: FEATURE_CREATED, + createdByUserId: SYSTEM_USER_ID, + createdBy: 'some@user.com', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }; - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); - -test('Should call teams webhook for archived toggle', async () => { - const addon = new TeamsAddon(ARGS); - const event: IEvent = { - id: 1, - createdAt: new Date(), - createdByUserId: SYSTEM_USER_ID, - type: FEATURE_ARCHIVED, - createdBy: 'some@user.com', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.office.com', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); - -test('Should call teams webhook for archived toggle with project info', async () => { - const addon = new TeamsAddon(ARGS); - const event: IEvent = { - id: 1, - createdAt: new Date(), - createdByUserId: SYSTEM_USER_ID, - type: FEATURE_ARCHIVED, - createdBy: 'some@user.com', - featureName: 'some-toggle', - project: 'some-project', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.office.com', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls.length).toBe(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); - -test(`Should call teams webhook for toggled environment`, async () => { - const addon = new TeamsAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - createdByUserId: SYSTEM_USER_ID, - type: FEATURE_ENVIRONMENT_DISABLED, - createdBy: 'some@user.com', - environment: 'development', - project: 'default', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.slack.com', - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls).toHaveLength(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); -}); - -test('Should include custom headers in call to teams', async () => { - const addon = new TeamsAddon(ARGS); - const event: IEvent = { - id: 2, - createdAt: new Date(), - createdByUserId: SYSTEM_USER_ID, - type: FEATURE_ENVIRONMENT_DISABLED, - createdBy: 'some@user.com', - environment: 'development', - project: 'default', - featureName: 'some-toggle', - data: { - name: 'some-toggle', - }, - }; - - const parameters = { - url: 'http://hooks.slack.com', - customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, - }; - - await addon.handleEvent(event, parameters, INTEGRATION_ID); - expect(fetchRetryCalls).toHaveLength(1); - expect(fetchRetryCalls[0].url).toBe(parameters.url); - expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); - expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); - expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); + const parameters = { + url: 'http://hooks.office.com', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test('Should call teams webhook for archived toggle', async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, + type: FEATURE_ARCHIVED, + createdBy: 'some@user.com', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.office.com', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test('Should call teams webhook for archived toggle with project info', async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 1, + createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, + type: FEATURE_ARCHIVED, + createdBy: 'some@user.com', + featureName: 'some-toggle', + project: 'some-project', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.office.com', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls.length).toBe(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test(`Should call teams webhook for toggled environment`, async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + }); + + test('Should include custom headers in call to teams', async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); + }); + + test('Should call registerEvent', async () => { + const addon = new TeamsAddon(ARGS); + const event: IEvent = { + id: 2, + createdAt: new Date(), + createdByUserId: SYSTEM_USER_ID, + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.teams.com', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + + await addon.handleEvent(event, parameters, INTEGRATION_ID); + + expect(registerEventMock).toHaveBeenCalledTimes(1); + expect(registerEventMock).toHaveBeenCalledWith({ + integrationId: INTEGRATION_ID, + state: 'success', + stateDetails: + 'Teams webhook request was successful with status code: 200.', + event: serializeDates(event), + details: { + url: parameters.url, + body: { + themeColor: '0076D7', + summary: 'Message', + sections: [ + { + activityTitle: `*${event.createdBy}* disabled *[${event.featureName}](${ARGS.unleashUrl}/projects/${event.project}/features/${event.featureName})* for the *${event.environment}* environment in project *[${event.project}](${ARGS.unleashUrl}/projects/${event.project})*`, + activitySubtitle: `Unleash notification update`, + facts: [ + { + name: 'User', + value: event.createdBy, + }, + { + name: 'Action', + value: event.type, + }, + ], + }, + ], + potentialAction: [ + { + '@type': 'OpenUri', + name: 'Go to feature', + targets: [ + { + os: 'default', + uri: `${ARGS.unleashUrl}/projects/${event.project}/features/${event.featureName}`, + }, + ], + }, + ], + }, + }, + }); + }); }); diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index 2957186fa0..c1492d4e1f 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -1,12 +1,13 @@ import Addon from './addon'; import teamsDefinition from './teams-definition'; -import type { IAddonConfig } from '../types/model'; +import { type IAddonConfig, serializeDates } from '../types'; import { type FeatureEventFormatter, FeatureEventFormatterMd, } from './feature-event-formatter-md'; import type { IEvent } from '../types/events'; +import type { IntegrationEventState } from '../features/integration-events/integration-events-store'; interface ITeamsParameters { url: string; @@ -26,6 +27,9 @@ export default class TeamsAddon extends Addon { parameters: ITeamsParameters, integrationId: number, ): Promise { + let state: IntegrationEventState = 'success'; + const stateDetails: string[] = []; + const { url, customHeaders } = parameters; const { createdBy } = event; const { text, url: featureLink } = this.msgFormatter.format(event); @@ -68,9 +72,11 @@ export default class TeamsAddon extends Addon { try { extraHeaders = JSON.parse(customHeaders); } catch (e) { - this.logger.warn( - `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, - ); + state = 'successWithErrors'; + const badHeadersMessage = + 'Could not parse the JSON in the customHeaders parameter.'; + stateDetails.push(badHeadersMessage); + this.logger.warn(badHeadersMessage); } } @@ -80,8 +86,29 @@ export default class TeamsAddon extends Addon { body: JSON.stringify(body), }; const res = await this.fetchRetry(url, requestOpts); - this.logger.info( - `Handled event ${event.type}. Status codes=${res.status}`, - ); + + this.logger.info(`Handled event "${event.type}".`); + + if (res.ok) { + const successMessage = `Teams webhook request was successful with status code: ${res.status}.`; + stateDetails.push(successMessage); + this.logger.info(successMessage); + } else { + state = 'failed'; + const failedMessage = `Teams webhook request failed with status code: ${res.status}.`; + stateDetails.push(failedMessage); + this.logger.warn(failedMessage); + } + + this.registerEvent({ + integrationId, + state, + stateDetails: stateDetails.join('\n'), + event: serializeDates(event), + details: { + url, + body, + }, + }); } } diff --git a/src/lib/addons/webhook.ts b/src/lib/addons/webhook.ts index 0a530bca9b..4086bba648 100644 --- a/src/lib/addons/webhook.ts +++ b/src/lib/addons/webhook.ts @@ -26,8 +26,13 @@ export default class Webhook extends Addon { let state: IntegrationEventState = 'success'; const stateDetails: string[] = []; - const { url, bodyTemplate, contentType, authorization, customHeaders } = - parameters; + const { + url, + bodyTemplate, + contentType = 'application/json', + authorization, + customHeaders, + } = parameters; const context = { event, // Stringify twice to avoid escaping in Mustache @@ -35,11 +40,13 @@ export default class Webhook extends Addon { }; let body: string | undefined; + let sendingEvent = false; if (typeof bodyTemplate === 'string' && bodyTemplate.length > 1) { body = Mustache.render(bodyTemplate, context); } else { body = JSON.stringify(event); + sendingEvent = true; } let extraHeaders = {}; @@ -47,17 +54,17 @@ export default class Webhook extends Addon { try { extraHeaders = JSON.parse(customHeaders); } catch (e) { - const detailMessage = - 'Could not parse the JSON in the customHeaders parameter.'; state = 'successWithErrors'; - stateDetails.push(detailMessage); - this.logger.warn(detailMessage); + const badHeadersMessage = + 'Could not parse the JSON in the customHeaders parameter.'; + stateDetails.push(badHeadersMessage); + this.logger.warn(badHeadersMessage); } } const requestOpts = { method: 'POST', headers: { - 'Content-Type': contentType || 'application/json', + 'Content-Type': contentType, Authorization: authorization || undefined, ...extraHeaders, }, @@ -68,14 +75,14 @@ export default class Webhook extends Addon { this.logger.info(`Handled event "${event.type}".`); if (res.ok) { - const detailMessage = `Webhook request was successful with status code: ${res.status}.`; - stateDetails.push(detailMessage); - this.logger.info(detailMessage); + const successMessage = `Webhook request was successful with status code: ${res.status}.`; + stateDetails.push(successMessage); + this.logger.info(successMessage); } else { - const detailMessage = `Webhook request failed with status code: ${res.status}.`; state = 'failed'; - stateDetails.push(detailMessage); - this.logger.warn(detailMessage); + const failedMessage = `Webhook request failed with status code: ${res.status}.`; + stateDetails.push(failedMessage); + this.logger.warn(failedMessage); } this.registerEvent({ @@ -86,7 +93,7 @@ export default class Webhook extends Addon { details: { url, contentType, - body, + body: sendingEvent ? event : body, }, }); }