mirror of
https://github.com/Unleash/unleash.git
synced 2025-04-15 01:16:22 +02:00
chore: register integration events in webhooks (#7621)
https://linear.app/unleash/issue/2-2450/register-integration-events-webhook Registers integration events in the **Webhook** integration. Even though this touches a lot of files, most of it is preparation for the next steps. The only actual implementation of registering integration events is in the **Webhook** integration. The rest will follow on separate PRs. Here's an example of how this looks like in the database table: ```json { "id": 7, "integration_id": 2, "created_at": "2024-07-18T18:11:11.376348+01:00", "state": "failed", "state_details": "Webhook request failed with status code: ECONNREFUSED", "event": { "id": 130, "data": null, "tags": [], "type": "feature-environment-enabled", "preData": null, "project": "default", "createdAt": "2024-07-18T17:11:10.821Z", "createdBy": "admin", "environment": "development", "featureName": "test", "createdByUserId": 1 }, "details": { "url": "http://localhost:1337", "body": "{ \"id\": 130, \"type\": \"feature-environment-enabled\", \"createdBy\": \"admin\", \"createdAt\": \"2024-07-18T17: 11: 10.821Z\", \"createdByUserId\": 1, \"data\": null, \"preData\": null, \"tags\": [], \"featureName\": \"test\", \"project\": \"default\", \"environment\": \"development\" }" } } ```
This commit is contained in:
parent
3db1159304
commit
0869e39603
@ -2,28 +2,32 @@ import nock from 'nock';
|
|||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
|
|
||||||
import SlackAddon from './slack';
|
import SlackAddon from './slack';
|
||||||
|
import type { IAddonConfig, IFlagResolver } from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
nock.disableNetConnect();
|
nock.disableNetConnect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const url = 'https://test.some.com';
|
||||||
|
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: url,
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
test('Does not retry if request succeeds', async () => {
|
test('Does not retry if request succeeds', async () => {
|
||||||
const url = 'https://test.some.com';
|
const url = 'https://test.some.com';
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: url,
|
|
||||||
});
|
|
||||||
nock(url).get('/').reply(201);
|
nock(url).get('/').reply(201);
|
||||||
const res = await addon.fetchRetry(url);
|
const res = await addon.fetchRetry(url);
|
||||||
expect(res.ok).toBe(true);
|
expect(res.ok).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Retries once, and succeeds', async () => {
|
test('Retries once, and succeeds', async () => {
|
||||||
const url = 'https://test.some.com';
|
const addon = new SlackAddon(ARGS);
|
||||||
const addon = new SlackAddon({
|
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: url,
|
|
||||||
});
|
|
||||||
nock(url).get('/').replyWithError('testing retry');
|
nock(url).get('/').replyWithError('testing retry');
|
||||||
nock(url).get('/').reply(200);
|
nock(url).get('/').reply(200);
|
||||||
const res = await addon.fetchRetry(url);
|
const res = await addon.fetchRetry(url);
|
||||||
@ -32,22 +36,14 @@ test('Retries once, and succeeds', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Does not throw if response is error', async () => {
|
test('Does not throw if response is error', async () => {
|
||||||
const url = 'https://test.some.com';
|
const addon = new SlackAddon(ARGS);
|
||||||
const addon = new SlackAddon({
|
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: url,
|
|
||||||
});
|
|
||||||
nock(url).get('/').twice().replyWithError('testing retry');
|
nock(url).get('/').twice().replyWithError('testing retry');
|
||||||
const res = await addon.fetchRetry(url);
|
const res = await addon.fetchRetry(url);
|
||||||
expect(res.ok).toBe(false);
|
expect(res.ok).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Supports custom number of retries', async () => {
|
test('Supports custom number of retries', async () => {
|
||||||
const url = 'https://test.some.com';
|
const addon = new SlackAddon(ARGS);
|
||||||
const addon = new SlackAddon({
|
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: url,
|
|
||||||
});
|
|
||||||
let retries = 0;
|
let retries = 0;
|
||||||
nock(url).get('/').twice().replyWithError('testing retry');
|
nock(url).get('/').twice().replyWithError('testing retry');
|
||||||
nock(url).get('/').reply(201);
|
nock(url).get('/').reply(201);
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
import fetch from 'make-fetch-happen';
|
import fetch from 'make-fetch-happen';
|
||||||
import { addonDefinitionSchema } from './addon-schema';
|
import { addonDefinitionSchema } from './addon-schema';
|
||||||
import type { IUnleashConfig } from '../types/option';
|
|
||||||
import type { Logger } from '../logger';
|
import type { Logger } from '../logger';
|
||||||
import type { IAddonDefinition } from '../types/model';
|
import type { IAddonConfig, IAddonDefinition } from '../types/model';
|
||||||
import type { IEvent } from '../types/events';
|
import type { IEvent } from '../types/events';
|
||||||
|
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||||
|
import type { IntegrationEventWriteModel } from '../features/integration-events/integration-events-store';
|
||||||
|
import type { IFlagResolver } from '../types';
|
||||||
|
|
||||||
export default abstract class Addon {
|
export default abstract class Addon {
|
||||||
logger: Logger;
|
logger: Logger;
|
||||||
@ -12,9 +14,13 @@ export default abstract class Addon {
|
|||||||
|
|
||||||
_definition: IAddonDefinition;
|
_definition: IAddonDefinition;
|
||||||
|
|
||||||
|
integrationEventsService: IntegrationEventsService;
|
||||||
|
|
||||||
|
flagResolver: IFlagResolver;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
definition: IAddonDefinition,
|
definition: IAddonDefinition,
|
||||||
{ getLogger }: Pick<IUnleashConfig, 'getLogger'>,
|
{ getLogger, integrationEventsService, flagResolver }: IAddonConfig,
|
||||||
) {
|
) {
|
||||||
this.logger = getLogger(`addon/${definition.name}`);
|
this.logger = getLogger(`addon/${definition.name}`);
|
||||||
const { error } = addonDefinitionSchema.validate(definition);
|
const { error } = addonDefinitionSchema.validate(definition);
|
||||||
@ -27,6 +33,8 @@ export default abstract class Addon {
|
|||||||
}
|
}
|
||||||
this._name = definition.name;
|
this._name = definition.name;
|
||||||
this._definition = definition;
|
this._definition = definition;
|
||||||
|
this.integrationEventsService = integrationEventsService;
|
||||||
|
this.flagResolver = flagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
@ -60,13 +68,25 @@ export default abstract class Addon {
|
|||||||
} status code ${e.code}`,
|
} status code ${e.code}`,
|
||||||
e,
|
e,
|
||||||
);
|
);
|
||||||
res = { statusCode: e.code, ok: false };
|
res = { status: e.code, ok: false };
|
||||||
}
|
}
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||||||
abstract handleEvent(event: IEvent, parameters: any): Promise<void>;
|
abstract handleEvent(
|
||||||
|
event: IEvent,
|
||||||
|
parameters: any,
|
||||||
|
integrationId: number,
|
||||||
|
): Promise<void>;
|
||||||
|
|
||||||
|
async registerEvent(
|
||||||
|
integrationEvent: IntegrationEventWriteModel,
|
||||||
|
): Promise<void> {
|
||||||
|
if (this.flagResolver.isEnabled('integrationEvents')) {
|
||||||
|
await this.integrationEventsService.registerEvent(integrationEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
destroy?(): void;
|
destroy?(): void;
|
||||||
}
|
}
|
||||||
|
@ -9,9 +9,19 @@ import type { Logger } from '../logger';
|
|||||||
import DatadogAddon from './datadog';
|
import DatadogAddon from './datadog';
|
||||||
|
|
||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
|
import type { IAddonConfig, IFlagResolver } from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
let fetchRetryCalls: any[] = [];
|
let fetchRetryCalls: any[] = [];
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./addon',
|
'./addon',
|
||||||
() =>
|
() =>
|
||||||
@ -32,14 +42,15 @@ jest.mock(
|
|||||||
});
|
});
|
||||||
return Promise.resolve({ status: 200 });
|
return Promise.resolve({ status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerEvent(_) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Should call datadog webhook', async () => {
|
test('Should call datadog webhook', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -59,17 +70,14 @@ test('Should call datadog webhook', async () => {
|
|||||||
apiKey: 'fakeKey',
|
apiKey: 'fakeKey',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call datadog webhook for archived toggle', async () => {
|
test('Should call datadog webhook for archived toggle', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -87,17 +95,14 @@ test('Should call datadog webhook for archived toggle', async () => {
|
|||||||
apiKey: 'fakeKey',
|
apiKey: 'fakeKey',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call datadog webhook for archived toggle with project info', async () => {
|
test('Should call datadog webhook for archived toggle with project info', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -116,17 +121,14 @@ test('Should call datadog webhook for archived toggle with project info', async
|
|||||||
apiKey: 'fakeKey',
|
apiKey: 'fakeKey',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call datadog webhook for toggled environment', async () => {
|
test('Should call datadog webhook for toggled environment', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -146,7 +148,7 @@ test('Should call datadog webhook for toggled environment', async () => {
|
|||||||
apiKey: 'fakeKey',
|
apiKey: 'fakeKey',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
@ -154,10 +156,7 @@ test('Should call datadog webhook for toggled environment', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should include customHeaders in headers when calling service', async () => {
|
test('Should include customHeaders in headers when calling service', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -177,7 +176,7 @@ test('Should include customHeaders in headers when calling service', async () =>
|
|||||||
apiKey: 'fakeKey',
|
apiKey: 'fakeKey',
|
||||||
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
||||||
};
|
};
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
@ -186,10 +185,7 @@ test('Should include customHeaders in headers when calling service', async () =>
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should not include source_type_name when included in the config', async () => {
|
test('Should not include source_type_name when included in the config', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -210,7 +206,7 @@ test('Should not include source_type_name when included in the config', async ()
|
|||||||
sourceTypeName: 'my-custom-source-type',
|
sourceTypeName: 'my-custom-source-type',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(
|
expect(fetchRetryCalls[0].options.body).toMatch(
|
||||||
@ -221,10 +217,7 @@ test('Should not include source_type_name when included in the config', async ()
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should call datadog webhook with JSON when template set', async () => {
|
test('Should call datadog webhook with JSON when template set', async () => {
|
||||||
const addon = new DatadogAddon({
|
const addon = new DatadogAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -246,7 +239,7 @@ test('Should call datadog webhook with JSON when template set', async () => {
|
|||||||
'{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}',
|
'{\n "event": "{{event.type}}",\n "createdBy": "{{event.createdBy}}"\n}',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
|
@ -39,6 +39,7 @@ export default class DatadogAddon extends Addon {
|
|||||||
async handleEvent(
|
async handleEvent(
|
||||||
event: IEvent,
|
event: IEvent,
|
||||||
parameters: IDatadogParameters,
|
parameters: IDatadogParameters,
|
||||||
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const {
|
const {
|
||||||
url = 'https://api.datadoghq.com/api/v1/events',
|
url = 'https://api.datadoghq.com/api/v1/events',
|
||||||
|
@ -4,26 +4,21 @@ import TeamsAddon from './teams';
|
|||||||
import DatadogAddon from './datadog';
|
import DatadogAddon from './datadog';
|
||||||
import NewRelicAddon from './new-relic';
|
import NewRelicAddon from './new-relic';
|
||||||
import type Addon from './addon';
|
import type Addon from './addon';
|
||||||
import type { LogProvider } from '../logger';
|
|
||||||
import SlackAppAddon from './slack-app';
|
import SlackAppAddon from './slack-app';
|
||||||
import type { IFlagResolver } from '../types';
|
import type { IAddonConfig } from '../types';
|
||||||
|
|
||||||
export interface IAddonProviders {
|
export interface IAddonProviders {
|
||||||
[key: string]: Addon;
|
[key: string]: Addon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getAddons: (args: {
|
export const getAddons: (args: IAddonConfig) => IAddonProviders = (args) => {
|
||||||
getLogger: LogProvider;
|
|
||||||
unleashUrl: string;
|
|
||||||
flagResolver: IFlagResolver;
|
|
||||||
}) => IAddonProviders = ({ getLogger, unleashUrl, flagResolver }) => {
|
|
||||||
const addons: Addon[] = [
|
const addons: Addon[] = [
|
||||||
new Webhook({ getLogger }),
|
new Webhook(args),
|
||||||
new SlackAddon({ getLogger, unleashUrl }),
|
new SlackAddon(args),
|
||||||
new SlackAppAddon({ getLogger, unleashUrl }),
|
new SlackAppAddon(args),
|
||||||
new TeamsAddon({ getLogger, unleashUrl }),
|
new TeamsAddon(args),
|
||||||
new DatadogAddon({ getLogger, unleashUrl }),
|
new DatadogAddon(args),
|
||||||
new NewRelicAddon({ getLogger, unleashUrl }),
|
new NewRelicAddon(args),
|
||||||
];
|
];
|
||||||
|
|
||||||
return addons.reduce((map, addon) => {
|
return addons.reduce((map, addon) => {
|
||||||
|
@ -2,6 +2,8 @@ import {
|
|||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
FEATURE_ENVIRONMENT_DISABLED,
|
FEATURE_ENVIRONMENT_DISABLED,
|
||||||
|
type IFlagResolver,
|
||||||
|
type IAddonConfig,
|
||||||
type IEvent,
|
type IEvent,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import type { Logger } from '../logger';
|
import type { Logger } from '../logger';
|
||||||
@ -11,11 +13,20 @@ import NewRelicAddon, { type INewRelicParameters } from './new-relic';
|
|||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
import { gunzip } from 'node:zlib';
|
import { gunzip } from 'node:zlib';
|
||||||
import { promisify } from 'util';
|
import { promisify } from 'util';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
const asyncGunzip = promisify(gunzip);
|
const asyncGunzip = promisify(gunzip);
|
||||||
|
|
||||||
let fetchRetryCalls: any[] = [];
|
let fetchRetryCalls: any[] = [];
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./addon',
|
'./addon',
|
||||||
() =>
|
() =>
|
||||||
@ -36,6 +47,10 @@ jest.mock(
|
|||||||
});
|
});
|
||||||
return Promise.resolve({ status: 200 });
|
return Promise.resolve({ status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerEvent(_) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -59,12 +74,9 @@ const defaultEvent = {
|
|||||||
} as IEvent;
|
} as IEvent;
|
||||||
|
|
||||||
const makeAddHandleEvent = (event: IEvent, parameters: INewRelicParameters) => {
|
const makeAddHandleEvent = (event: IEvent, parameters: INewRelicParameters) => {
|
||||||
const addon = new NewRelicAddon({
|
const addon = new NewRelicAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => addon.handleEvent(event, parameters);
|
return () => addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
};
|
};
|
||||||
|
|
||||||
test.each([
|
test.each([
|
||||||
|
@ -44,6 +44,7 @@ export default class NewRelicAddon extends Addon {
|
|||||||
async handleEvent(
|
async handleEvent(
|
||||||
event: IEvent,
|
event: IEvent,
|
||||||
parameters: INewRelicParameters,
|
parameters: INewRelicParameters,
|
||||||
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { url, licenseKey, customHeaders, bodyTemplate } = parameters;
|
const { url, licenseKey, customHeaders, bodyTemplate } = parameters;
|
||||||
const context = {
|
const context = {
|
||||||
|
@ -1,10 +1,32 @@
|
|||||||
import { type IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events';
|
import { type IEvent, FEATURE_ENVIRONMENT_ENABLED } from '../types/events';
|
||||||
import SlackAppAddon from './slack-app';
|
import SlackAppAddon from './slack-app';
|
||||||
import { type ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
|
import { type ChatPostMessageArguments, ErrorCode } from '@slack/web-api';
|
||||||
import { SYSTEM_USER_ID } from '../types';
|
import {
|
||||||
|
type IAddonConfig,
|
||||||
|
type IFlagResolver,
|
||||||
|
SYSTEM_USER_ID,
|
||||||
|
} from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
const slackApiCalls: ChatPostMessageArguments[] = [];
|
const slackApiCalls: ChatPostMessageArguments[] = [];
|
||||||
|
|
||||||
|
const loggerMock = {
|
||||||
|
debug: jest.fn(),
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
fatal: jest.fn(),
|
||||||
|
};
|
||||||
|
const getLogger = jest.fn(() => loggerMock);
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
let postMessage = jest.fn().mockImplementation((options) => {
|
let postMessage = jest.fn().mockImplementation((options) => {
|
||||||
slackApiCalls.push(options);
|
slackApiCalls.push(options);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
@ -28,14 +50,6 @@ jest.mock('@slack/web-api', () => ({
|
|||||||
describe('SlackAppAddon', () => {
|
describe('SlackAppAddon', () => {
|
||||||
let addon: SlackAppAddon;
|
let addon: SlackAppAddon;
|
||||||
const accessToken = 'test-access-token';
|
const accessToken = 'test-access-token';
|
||||||
const loggerMock = {
|
|
||||||
debug: jest.fn(),
|
|
||||||
info: jest.fn(),
|
|
||||||
warn: jest.fn(),
|
|
||||||
error: jest.fn(),
|
|
||||||
fatal: jest.fn(),
|
|
||||||
};
|
|
||||||
const getLogger = jest.fn(() => loggerMock);
|
|
||||||
const mockError = {
|
const mockError = {
|
||||||
code: ErrorCode.PlatformError,
|
code: ErrorCode.PlatformError,
|
||||||
data: {
|
data: {
|
||||||
@ -64,10 +78,7 @@ describe('SlackAppAddon', () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
slackApiCalls.length = 0;
|
slackApiCalls.length = 0;
|
||||||
postMessage.mockClear();
|
postMessage.mockClear();
|
||||||
addon = new SlackAppAddon({
|
addon = new SlackAppAddon(ARGS);
|
||||||
getLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@ -75,20 +86,28 @@ describe('SlackAppAddon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should post message when feature is toggled', async () => {
|
it('should post message when feature is toggled', async () => {
|
||||||
await addon.handleEvent(event, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
event,
|
||||||
defaultChannels: 'general',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: 'general',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(slackApiCalls.length).toBe(1);
|
expect(slackApiCalls.length).toBe(1);
|
||||||
expect(slackApiCalls[0].channel).toBe('general');
|
expect(slackApiCalls[0].channel).toBe('general');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should post to all channels in defaultChannels', async () => {
|
it('should post to all channels in defaultChannels', async () => {
|
||||||
await addon.handleEvent(event, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
event,
|
||||||
defaultChannels: 'general, another-channel-1',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: 'general, another-channel-1',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(slackApiCalls.length).toBe(2);
|
expect(slackApiCalls.length).toBe(2);
|
||||||
expect(slackApiCalls[0].channel).toBe('general');
|
expect(slackApiCalls[0].channel).toBe('general');
|
||||||
@ -104,10 +123,14 @@ describe('SlackAppAddon', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(eventWith2Tags, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
eventWith2Tags,
|
||||||
defaultChannels: '',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: '',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(slackApiCalls.length).toBe(2);
|
expect(slackApiCalls.length).toBe(2);
|
||||||
expect(slackApiCalls[0].channel).toBe('general');
|
expect(slackApiCalls[0].channel).toBe('general');
|
||||||
@ -123,10 +146,14 @@ describe('SlackAppAddon', () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(eventWith2Tags, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
eventWith2Tags,
|
||||||
defaultChannels: 'another-channel-1, another-channel-2',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: 'another-channel-1, another-channel-2',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(slackApiCalls.length).toBe(3);
|
expect(slackApiCalls.length).toBe(3);
|
||||||
expect(slackApiCalls[0].channel).toBe('general');
|
expect(slackApiCalls[0].channel).toBe('general');
|
||||||
@ -135,10 +162,14 @@ describe('SlackAppAddon', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not post a message if there are no tagged channels and no defaultChannels', async () => {
|
it('should not post a message if there are no tagged channels and no defaultChannels', async () => {
|
||||||
await addon.handleEvent(event, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
event,
|
||||||
defaultChannels: '',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: '',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(slackApiCalls.length).toBe(0);
|
expect(slackApiCalls.length).toBe(0);
|
||||||
});
|
});
|
||||||
@ -146,10 +177,14 @@ describe('SlackAppAddon', () => {
|
|||||||
it('should log error when an API call fails', async () => {
|
it('should log error when an API call fails', async () => {
|
||||||
postMessage = jest.fn().mockRejectedValue(mockError);
|
postMessage = jest.fn().mockRejectedValue(mockError);
|
||||||
|
|
||||||
await addon.handleEvent(event, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
event,
|
||||||
defaultChannels: 'general',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: 'general',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||||
`Error handling event ${event.type}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
`Error handling event ${event.type}. A platform error occurred: ${JSON.stringify(mockError.data)}`,
|
||||||
@ -173,10 +208,14 @@ describe('SlackAppAddon', () => {
|
|||||||
.mockResolvedValueOnce({ ok: true })
|
.mockResolvedValueOnce({ ok: true })
|
||||||
.mockRejectedValueOnce(mockError);
|
.mockRejectedValueOnce(mockError);
|
||||||
|
|
||||||
await addon.handleEvent(eventWith3Tags, {
|
await addon.handleEvent(
|
||||||
accessToken,
|
eventWith3Tags,
|
||||||
defaultChannels: '',
|
{
|
||||||
});
|
accessToken,
|
||||||
|
defaultChannels: '',
|
||||||
|
},
|
||||||
|
INTEGRATION_ID,
|
||||||
|
);
|
||||||
|
|
||||||
expect(postMessage).toHaveBeenCalledTimes(3);
|
expect(postMessage).toHaveBeenCalledTimes(3);
|
||||||
expect(loggerMock.warn).toHaveBeenCalledWith(
|
expect(loggerMock.warn).toHaveBeenCalledWith(
|
||||||
|
@ -44,6 +44,7 @@ export default class SlackAppAddon extends Addon {
|
|||||||
async handleEvent(
|
async handleEvent(
|
||||||
event: IEvent,
|
event: IEvent,
|
||||||
parameters: ISlackAppAddonParameters,
|
parameters: ISlackAppAddonParameters,
|
||||||
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { accessToken, defaultChannels } = parameters;
|
const { accessToken, defaultChannels } = parameters;
|
||||||
|
@ -9,10 +9,23 @@ import type { Logger } from '../logger';
|
|||||||
import SlackAddon from './slack';
|
import SlackAddon from './slack';
|
||||||
|
|
||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
import { SYSTEM_USER_ID } from '../types';
|
import {
|
||||||
|
type IAddonConfig,
|
||||||
|
type IFlagResolver,
|
||||||
|
SYSTEM_USER_ID,
|
||||||
|
} from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
let fetchRetryCalls: any[] = [];
|
let fetchRetryCalls: any[] = [];
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./addon',
|
'./addon',
|
||||||
() =>
|
() =>
|
||||||
@ -33,14 +46,15 @@ jest.mock(
|
|||||||
});
|
});
|
||||||
return Promise.resolve({ status: 200 });
|
return Promise.resolve({ status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerEvent(_) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Should call slack webhook', async () => {
|
test('Should call slack webhook', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -62,17 +76,14 @@ test('Should call slack webhook', async () => {
|
|||||||
defaultChannel: 'general',
|
defaultChannel: 'general',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call slack webhook for archived toggle', async () => {
|
test('Should call slack webhook for archived toggle', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -90,17 +101,14 @@ test('Should call slack webhook for archived toggle', async () => {
|
|||||||
defaultChannel: 'general',
|
defaultChannel: 'general',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call slack webhook for archived toggle with project info', async () => {
|
test('Should call slack webhook for archived toggle with project info', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -119,17 +127,14 @@ test('Should call slack webhook for archived toggle with project info', async ()
|
|||||||
defaultChannel: 'general',
|
defaultChannel: 'general',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`Should call webhook for toggled environment`, async () => {
|
test(`Should call webhook for toggled environment`, async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -149,7 +154,7 @@ test(`Should call webhook for toggled environment`, async () => {
|
|||||||
defaultChannel: 'general',
|
defaultChannel: 'general',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
@ -157,10 +162,7 @@ test(`Should call webhook for toggled environment`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should use default channel', async () => {
|
test('Should use default channel', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 3,
|
id: 3,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -180,7 +182,7 @@ test('Should use default channel', async () => {
|
|||||||
defaultChannel: 'some-channel',
|
defaultChannel: 'some-channel',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
|
|
||||||
const req = JSON.parse(fetchRetryCalls[0].options.body);
|
const req = JSON.parse(fetchRetryCalls[0].options.body);
|
||||||
|
|
||||||
@ -188,10 +190,7 @@ test('Should use default channel', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should override default channel with data from tag', async () => {
|
test('Should override default channel with data from tag', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 4,
|
id: 4,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -217,7 +216,7 @@ test('Should override default channel with data from tag', async () => {
|
|||||||
defaultChannel: 'some-channel',
|
defaultChannel: 'some-channel',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
|
|
||||||
const req = JSON.parse(fetchRetryCalls[0].options.body);
|
const req = JSON.parse(fetchRetryCalls[0].options.body);
|
||||||
|
|
||||||
@ -225,10 +224,7 @@ test('Should override default channel with data from tag', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should post to all channels in tags', async () => {
|
test('Should post to all channels in tags', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 5,
|
id: 5,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -258,7 +254,7 @@ test('Should post to all channels in tags', async () => {
|
|||||||
defaultChannel: 'some-channel',
|
defaultChannel: 'some-channel',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
|
|
||||||
const req1 = JSON.parse(fetchRetryCalls[0].options.body);
|
const req1 = JSON.parse(fetchRetryCalls[0].options.body);
|
||||||
const req2 = JSON.parse(fetchRetryCalls[1].options.body);
|
const req2 = JSON.parse(fetchRetryCalls[1].options.body);
|
||||||
@ -269,10 +265,7 @@ test('Should post to all channels in tags', async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should include custom headers from parameters in call to service', async () => {
|
test('Should include custom headers from parameters in call to service', async () => {
|
||||||
const addon = new SlackAddon({
|
const addon = new SlackAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -293,7 +286,7 @@ test('Should include custom headers from parameters in call to service', async (
|
|||||||
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
|
@ -32,6 +32,7 @@ export default class SlackAddon extends Addon {
|
|||||||
async handleEvent(
|
async handleEvent(
|
||||||
event: IEvent,
|
event: IEvent,
|
||||||
parameters: ISlackAddonParameters,
|
parameters: ISlackAddonParameters,
|
||||||
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const {
|
const {
|
||||||
url,
|
url,
|
||||||
|
@ -10,10 +10,23 @@ import {
|
|||||||
import TeamsAddon from './teams';
|
import TeamsAddon from './teams';
|
||||||
|
|
||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
import { SYSTEM_USER_ID } from '../types';
|
import {
|
||||||
|
type IAddonConfig,
|
||||||
|
type IFlagResolver,
|
||||||
|
SYSTEM_USER_ID,
|
||||||
|
} from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
let fetchRetryCalls: any[];
|
let fetchRetryCalls: any[];
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./addon',
|
'./addon',
|
||||||
() =>
|
() =>
|
||||||
@ -34,14 +47,15 @@ jest.mock(
|
|||||||
});
|
});
|
||||||
return Promise.resolve({ status: 200 });
|
return Promise.resolve({ status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async registerEvent(_) {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Should call teams webhook', async () => {
|
test('Should call teams webhook', async () => {
|
||||||
const addon = new TeamsAddon({
|
const addon = new TeamsAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -60,17 +74,14 @@ test('Should call teams webhook', async () => {
|
|||||||
url: 'http://hooks.office.com',
|
url: 'http://hooks.office.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call teams webhook for archived toggle', async () => {
|
test('Should call teams webhook for archived toggle', async () => {
|
||||||
const addon = new TeamsAddon({
|
const addon = new TeamsAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -87,17 +98,14 @@ test('Should call teams webhook for archived toggle', async () => {
|
|||||||
url: 'http://hooks.office.com',
|
url: 'http://hooks.office.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Should call teams webhook for archived toggle with project info', async () => {
|
test('Should call teams webhook for archived toggle with project info', async () => {
|
||||||
const addon = new TeamsAddon({
|
const addon = new TeamsAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 1,
|
id: 1,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -115,17 +123,14 @@ test('Should call teams webhook for archived toggle with project info', async ()
|
|||||||
url: 'http://hooks.office.com',
|
url: 'http://hooks.office.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
expect(fetchRetryCalls[0].options.body).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
test(`Should call teams webhook for toggled environment`, async () => {
|
test(`Should call teams webhook for toggled environment`, async () => {
|
||||||
const addon = new TeamsAddon({
|
const addon = new TeamsAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -144,7 +149,7 @@ test(`Should call teams webhook for toggled environment`, async () => {
|
|||||||
url: 'http://hooks.slack.com',
|
url: 'http://hooks.slack.com',
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
@ -152,10 +157,7 @@ test(`Should call teams webhook for toggled environment`, async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('Should include custom headers in call to teams', async () => {
|
test('Should include custom headers in call to teams', async () => {
|
||||||
const addon = new TeamsAddon({
|
const addon = new TeamsAddon(ARGS);
|
||||||
getLogger: noLogger,
|
|
||||||
unleashUrl: 'http://some-url.com',
|
|
||||||
});
|
|
||||||
const event: IEvent = {
|
const event: IEvent = {
|
||||||
id: 2,
|
id: 2,
|
||||||
createdAt: new Date(),
|
createdAt: new Date(),
|
||||||
@ -175,7 +177,7 @@ test('Should include custom headers in call to teams', async () => {
|
|||||||
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
||||||
};
|
};
|
||||||
|
|
||||||
await addon.handleEvent(event, parameters);
|
await addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(fetchRetryCalls).toHaveLength(1);
|
expect(fetchRetryCalls).toHaveLength(1);
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
expect(fetchRetryCalls[0].options.body).toMatch(/disabled/);
|
||||||
|
@ -24,6 +24,7 @@ export default class TeamsAddon extends Addon {
|
|||||||
async handleEvent(
|
async handleEvent(
|
||||||
event: IEvent,
|
event: IEvent,
|
||||||
parameters: ITeamsParameters,
|
parameters: ITeamsParameters,
|
||||||
|
integrationId: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { url, customHeaders } = parameters;
|
const { url, customHeaders } = parameters;
|
||||||
const { createdBy } = event;
|
const { createdBy } = event;
|
||||||
|
@ -5,9 +5,24 @@ import { FEATURE_CREATED, type IEvent } from '../types/events';
|
|||||||
import WebhookAddon from './webhook';
|
import WebhookAddon from './webhook';
|
||||||
|
|
||||||
import noLogger from '../../test/fixtures/no-logger';
|
import noLogger from '../../test/fixtures/no-logger';
|
||||||
import { SYSTEM_USER_ID } from '../types';
|
import {
|
||||||
|
type IAddonConfig,
|
||||||
|
type IFlagResolver,
|
||||||
|
serializeDates,
|
||||||
|
SYSTEM_USER_ID,
|
||||||
|
} from '../types';
|
||||||
|
import type { IntegrationEventsService } from '../services';
|
||||||
|
|
||||||
let fetchRetryCalls: any[] = [];
|
let fetchRetryCalls: any[] = [];
|
||||||
|
const registerEventMock = jest.fn();
|
||||||
|
|
||||||
|
const INTEGRATION_ID = 1337;
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger: noLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
jest.mock(
|
jest.mock(
|
||||||
'./addon',
|
'./addon',
|
||||||
@ -27,127 +42,182 @@ jest.mock(
|
|||||||
retries,
|
retries,
|
||||||
backoff,
|
backoff,
|
||||||
});
|
});
|
||||||
return Promise.resolve({ status: 200 });
|
return Promise.resolve({ ok: true, status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerEvent(event) {
|
||||||
|
return registerEventMock(event);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
test('Should handle event without "bodyTemplate"', () => {
|
describe('Webhook integration', () => {
|
||||||
const addon = new WebhookAddon({ getLogger: noLogger });
|
beforeEach(() => {
|
||||||
const event: IEvent = {
|
registerEventMock.mockClear();
|
||||||
id: 1,
|
});
|
||||||
createdAt: new Date(),
|
|
||||||
createdByUserId: SYSTEM_USER_ID,
|
|
||||||
type: FEATURE_CREATED,
|
|
||||||
createdBy: 'some@user.com',
|
|
||||||
featureName: 'some-toggle',
|
|
||||||
data: {
|
|
||||||
name: 'some-toggle',
|
|
||||||
enabled: false,
|
|
||||||
strategies: [{ name: 'default' }],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const parameters = {
|
test('Should handle event without "bodyTemplate"', () => {
|
||||||
url: 'http://test.webhook.com',
|
const addon = new WebhookAddon(ARGS);
|
||||||
};
|
const event: IEvent = {
|
||||||
|
id: 1,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUserId: SYSTEM_USER_ID,
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
createdBy: 'some@user.com',
|
||||||
|
featureName: 'some-toggle',
|
||||||
|
data: {
|
||||||
|
name: 'some-toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
addon.handleEvent(event, parameters);
|
const parameters = {
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
url: 'http://test.webhook.com',
|
||||||
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
};
|
||||||
expect(fetchRetryCalls[0].options.body).toBe(JSON.stringify(event));
|
|
||||||
});
|
addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
test('Should format event with "bodyTemplate"', () => {
|
expect(fetchRetryCalls[0].url).toBe(parameters.url);
|
||||||
const addon = new WebhookAddon({ getLogger: noLogger });
|
expect(fetchRetryCalls[0].options.body).toBe(JSON.stringify(event));
|
||||||
const event: IEvent = {
|
});
|
||||||
id: 1,
|
|
||||||
createdAt: new Date(),
|
test('Should format event with "bodyTemplate"', () => {
|
||||||
createdByUserId: SYSTEM_USER_ID,
|
const addon = new WebhookAddon(ARGS);
|
||||||
type: FEATURE_CREATED,
|
const event: IEvent = {
|
||||||
createdBy: 'some@user.com',
|
id: 1,
|
||||||
featureName: 'some-toggle',
|
createdAt: new Date(),
|
||||||
data: {
|
createdByUserId: SYSTEM_USER_ID,
|
||||||
name: 'some-toggle',
|
type: FEATURE_CREATED,
|
||||||
enabled: false,
|
createdBy: 'some@user.com',
|
||||||
strategies: [{ name: 'default' }],
|
featureName: 'some-toggle',
|
||||||
},
|
data: {
|
||||||
};
|
name: 'some-toggle',
|
||||||
|
enabled: false,
|
||||||
const parameters = {
|
strategies: [{ name: 'default' }],
|
||||||
url: 'http://test.webhook.com/plain',
|
},
|
||||||
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
};
|
||||||
contentType: 'text/plain',
|
|
||||||
};
|
const parameters = {
|
||||||
|
url: 'http://test.webhook.com/plain',
|
||||||
addon.handleEvent(event, parameters);
|
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
||||||
const call = fetchRetryCalls[0];
|
contentType: 'text/plain',
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
};
|
||||||
expect(call.url).toBe(parameters.url);
|
|
||||||
expect(call.options.headers['Content-Type']).toBe('text/plain');
|
addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
const call = fetchRetryCalls[0];
|
||||||
});
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
|
expect(call.url).toBe(parameters.url);
|
||||||
test('Should format event with "authorization"', () => {
|
expect(call.options.headers['Content-Type']).toBe('text/plain');
|
||||||
const addon = new WebhookAddon({ getLogger: noLogger });
|
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
||||||
const event: IEvent = {
|
});
|
||||||
id: 1,
|
|
||||||
createdAt: new Date(),
|
test('Should format event with "authorization"', () => {
|
||||||
createdByUserId: SYSTEM_USER_ID,
|
const addon = new WebhookAddon(ARGS);
|
||||||
type: FEATURE_CREATED,
|
const event: IEvent = {
|
||||||
createdBy: 'some@user.com',
|
id: 1,
|
||||||
featureName: 'some-toggle',
|
createdAt: new Date(),
|
||||||
data: {
|
createdByUserId: SYSTEM_USER_ID,
|
||||||
name: 'some-toggle',
|
type: FEATURE_CREATED,
|
||||||
enabled: false,
|
createdBy: 'some@user.com',
|
||||||
strategies: [{ name: 'default' }],
|
featureName: 'some-toggle',
|
||||||
},
|
data: {
|
||||||
};
|
name: 'some-toggle',
|
||||||
|
enabled: false,
|
||||||
const parameters = {
|
strategies: [{ name: 'default' }],
|
||||||
url: 'http://test.webhook.com/plain',
|
},
|
||||||
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
};
|
||||||
contentType: 'text/plain',
|
|
||||||
authorization: 'API KEY 123abc',
|
const parameters = {
|
||||||
};
|
url: 'http://test.webhook.com/plain',
|
||||||
|
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
||||||
addon.handleEvent(event, parameters);
|
contentType: 'text/plain',
|
||||||
const call = fetchRetryCalls[0];
|
authorization: 'API KEY 123abc',
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
};
|
||||||
expect(call.url).toBe(parameters.url);
|
|
||||||
expect(call.options.headers.Authorization).toBe(parameters.authorization);
|
addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
const call = fetchRetryCalls[0];
|
||||||
});
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
|
expect(call.url).toBe(parameters.url);
|
||||||
test('Should handle custom headers', async () => {
|
expect(call.options.headers.Authorization).toBe(
|
||||||
const addon = new WebhookAddon({ getLogger: noLogger });
|
parameters.authorization,
|
||||||
const event: IEvent = {
|
);
|
||||||
id: 1,
|
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
||||||
createdAt: new Date(),
|
});
|
||||||
createdByUserId: SYSTEM_USER_ID,
|
|
||||||
type: FEATURE_CREATED,
|
test('Should handle custom headers', async () => {
|
||||||
createdBy: 'some@user.com',
|
const addon = new WebhookAddon(ARGS);
|
||||||
featureName: 'some-toggle',
|
const event: IEvent = {
|
||||||
data: {
|
id: 1,
|
||||||
name: 'some-toggle',
|
createdAt: new Date(),
|
||||||
enabled: false,
|
createdByUserId: SYSTEM_USER_ID,
|
||||||
strategies: [{ name: 'default' }],
|
type: FEATURE_CREATED,
|
||||||
},
|
createdBy: 'some@user.com',
|
||||||
};
|
featureName: 'some-toggle',
|
||||||
|
data: {
|
||||||
const parameters = {
|
name: 'some-toggle',
|
||||||
url: 'http://test.webhook.com/plain',
|
enabled: false,
|
||||||
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
strategies: [{ name: 'default' }],
|
||||||
contentType: 'text/plain',
|
},
|
||||||
authorization: 'API KEY 123abc',
|
};
|
||||||
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
|
||||||
};
|
const parameters = {
|
||||||
|
url: 'http://test.webhook.com/plain',
|
||||||
addon.handleEvent(event, parameters);
|
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
||||||
const call = fetchRetryCalls[0];
|
contentType: 'text/plain',
|
||||||
expect(fetchRetryCalls.length).toBe(1);
|
authorization: 'API KEY 123abc',
|
||||||
expect(call.url).toBe(parameters.url);
|
customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`,
|
||||||
expect(call.options.headers.Authorization).toBe(parameters.authorization);
|
};
|
||||||
expect(call.options.headers.MY_CUSTOM_HEADER).toBe('MY_CUSTOM_VALUE');
|
|
||||||
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
addon.handleEvent(event, parameters, INTEGRATION_ID);
|
||||||
|
const call = fetchRetryCalls[0];
|
||||||
|
expect(fetchRetryCalls.length).toBe(1);
|
||||||
|
expect(call.url).toBe(parameters.url);
|
||||||
|
expect(call.options.headers.Authorization).toBe(
|
||||||
|
parameters.authorization,
|
||||||
|
);
|
||||||
|
expect(call.options.headers.MY_CUSTOM_HEADER).toBe('MY_CUSTOM_VALUE');
|
||||||
|
expect(call.options.body).toBe('feature-created on toggle some-toggle');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Should call registerEvent', async () => {
|
||||||
|
const addon = new WebhookAddon(ARGS);
|
||||||
|
const event: IEvent = {
|
||||||
|
id: 1,
|
||||||
|
createdAt: new Date(),
|
||||||
|
createdByUserId: SYSTEM_USER_ID,
|
||||||
|
type: FEATURE_CREATED,
|
||||||
|
createdBy: 'some@user.com',
|
||||||
|
featureName: 'some-toggle',
|
||||||
|
data: {
|
||||||
|
name: 'some-toggle',
|
||||||
|
enabled: false,
|
||||||
|
strategies: [{ name: 'default' }],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const parameters = {
|
||||||
|
url: 'http://test.webhook.com/plain',
|
||||||
|
bodyTemplate: '{{event.type}} on toggle {{event.data.name}}',
|
||||||
|
contentType: 'text/plain',
|
||||||
|
authorization: 'API KEY 123abc',
|
||||||
|
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:
|
||||||
|
'Webhook request was successful with status code: 200',
|
||||||
|
event: serializeDates(event),
|
||||||
|
details: {
|
||||||
|
url: parameters.url,
|
||||||
|
contentType: parameters.contentType,
|
||||||
|
body: 'feature-created on toggle some-toggle',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,8 +1,9 @@
|
|||||||
import Mustache from 'mustache';
|
import Mustache from 'mustache';
|
||||||
import Addon from './addon';
|
import Addon from './addon';
|
||||||
import definition from './webhook-definition';
|
import definition from './webhook-definition';
|
||||||
import type { LogProvider } from '../logger';
|
|
||||||
import type { IEvent } from '../types/events';
|
import type { IEvent } from '../types/events';
|
||||||
|
import { type IAddonConfig, serializeDates } from '../types';
|
||||||
|
import type { IntegrationEventState } from '../features/integration-events/integration-events-store';
|
||||||
|
|
||||||
interface IParameters {
|
interface IParameters {
|
||||||
url: string;
|
url: string;
|
||||||
@ -13,11 +14,18 @@ interface IParameters {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class Webhook extends Addon {
|
export default class Webhook extends Addon {
|
||||||
constructor(args: { getLogger: LogProvider }) {
|
constructor(args: IAddonConfig) {
|
||||||
super(definition, args);
|
super(definition, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
async handleEvent(event: IEvent, parameters: IParameters): Promise<void> {
|
async handleEvent(
|
||||||
|
event: IEvent,
|
||||||
|
parameters: IParameters,
|
||||||
|
integrationId: number,
|
||||||
|
): Promise<void> {
|
||||||
|
let state: IntegrationEventState = 'success';
|
||||||
|
let stateDetails = '';
|
||||||
|
|
||||||
const { url, bodyTemplate, contentType, authorization, customHeaders } =
|
const { url, bodyTemplate, contentType, authorization, customHeaders } =
|
||||||
parameters;
|
parameters;
|
||||||
const context = {
|
const context = {
|
||||||
@ -39,9 +47,10 @@ export default class Webhook extends Addon {
|
|||||||
try {
|
try {
|
||||||
extraHeaders = JSON.parse(customHeaders);
|
extraHeaders = JSON.parse(customHeaders);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.logger.warn(
|
state = 'successWithErrors';
|
||||||
`Could not parse the json in the customHeaders parameter. [${customHeaders}]`,
|
stateDetails =
|
||||||
);
|
'Could not parse the JSON in the customHeaders parameter.';
|
||||||
|
this.logger.warn(stateDetails);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const requestOpts = {
|
const requestOpts = {
|
||||||
@ -58,5 +67,24 @@ export default class Webhook extends Addon {
|
|||||||
this.logger.info(
|
this.logger.info(
|
||||||
`Handled event "${event.type}". Status code: ${res.status}`,
|
`Handled event "${event.type}". Status code: ${res.status}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
stateDetails = `Webhook request was successful with status code: ${res.status}`;
|
||||||
|
} else {
|
||||||
|
state = 'failed';
|
||||||
|
stateDetails = `Webhook request failed with status code: ${res.status}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.registerEvent({
|
||||||
|
integrationId,
|
||||||
|
state,
|
||||||
|
stateDetails,
|
||||||
|
event: serializeDates(event),
|
||||||
|
details: {
|
||||||
|
url,
|
||||||
|
contentType,
|
||||||
|
body,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,6 +7,8 @@ export type IntegrationEventWriteModel = Omit<
|
|||||||
'id' | 'createdAt'
|
'id' | 'createdAt'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type IntegrationEventState = IntegrationEventWriteModel['state'];
|
||||||
|
|
||||||
export class IntegrationEventsStore extends CRUDStore<
|
export class IntegrationEventsStore extends CRUDStore<
|
||||||
IntegrationEventSchema,
|
IntegrationEventSchema,
|
||||||
IntegrationEventWriteModel
|
IntegrationEventWriteModel
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import Addon from '../addons/addon';
|
import Addon from '../addons/addon';
|
||||||
import getLogger from '../../test/fixtures/no-logger';
|
import getLogger from '../../test/fixtures/no-logger';
|
||||||
import type { IAddonDefinition } from '../types/model';
|
import type { IAddonConfig, IAddonDefinition } from '../types/model';
|
||||||
import {
|
import {
|
||||||
FEATURE_ARCHIVED,
|
FEATURE_ARCHIVED,
|
||||||
FEATURE_CREATED,
|
FEATURE_CREATED,
|
||||||
@ -8,6 +8,14 @@ import {
|
|||||||
FEATURE_UPDATED,
|
FEATURE_UPDATED,
|
||||||
type IEvent,
|
type IEvent,
|
||||||
} from '../types/events';
|
} from '../types/events';
|
||||||
|
import type { IFlagResolver, IntegrationEventsService } from '../internals';
|
||||||
|
|
||||||
|
const ARGS: IAddonConfig = {
|
||||||
|
getLogger,
|
||||||
|
unleashUrl: 'http://some-url.com',
|
||||||
|
integrationEventsService: {} as IntegrationEventsService,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
};
|
||||||
|
|
||||||
const definition: IAddonDefinition = {
|
const definition: IAddonDefinition = {
|
||||||
name: 'simple',
|
name: 'simple',
|
||||||
@ -57,7 +65,7 @@ export default class SimpleAddon extends Addon {
|
|||||||
events: any[];
|
events: any[];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(definition, { getLogger });
|
super(definition, ARGS);
|
||||||
this.events = [];
|
this.events = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -15,8 +15,9 @@ import type { IAddonDto } from '../types/stores/addon-store';
|
|||||||
import SimpleAddon from './addon-service-test-simple-addon';
|
import SimpleAddon from './addon-service-test-simple-addon';
|
||||||
import type { IAddonProviders } from '../addons';
|
import type { IAddonProviders } from '../addons';
|
||||||
import EventService from '../features/events/event-service';
|
import EventService from '../features/events/event-service';
|
||||||
import { SYSTEM_USER, TEST_AUDIT_USER } from '../types';
|
import { type IFlagResolver, SYSTEM_USER, TEST_AUDIT_USER } from '../types';
|
||||||
import EventEmitter from 'node:events';
|
import EventEmitter from 'node:events';
|
||||||
|
import { IntegrationEventsService } from '../internals';
|
||||||
|
|
||||||
const MASKED_VALUE = '*****';
|
const MASKED_VALUE = '*****';
|
||||||
|
|
||||||
@ -35,6 +36,10 @@ function getSetup() {
|
|||||||
{ getLogger },
|
{ getLogger },
|
||||||
eventService,
|
eventService,
|
||||||
);
|
);
|
||||||
|
const integrationEventsService = new IntegrationEventsService(stores, {
|
||||||
|
getLogger,
|
||||||
|
flagResolver: {} as IFlagResolver,
|
||||||
|
});
|
||||||
|
|
||||||
addonProvider = { simple: new SimpleAddon() };
|
addonProvider = { simple: new SimpleAddon() };
|
||||||
return {
|
return {
|
||||||
@ -47,6 +52,7 @@ function getSetup() {
|
|||||||
},
|
},
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
eventService,
|
eventService,
|
||||||
|
integrationEventsService,
|
||||||
addonProvider,
|
addonProvider,
|
||||||
),
|
),
|
||||||
eventService,
|
eventService,
|
||||||
|
@ -67,6 +67,7 @@ export default class AddonService {
|
|||||||
}: Pick<IUnleashConfig, 'getLogger' | 'server' | 'flagResolver'>,
|
}: Pick<IUnleashConfig, 'getLogger' | 'server' | 'flagResolver'>,
|
||||||
tagTypeService: TagTypeService,
|
tagTypeService: TagTypeService,
|
||||||
eventService: EventService,
|
eventService: EventService,
|
||||||
|
integrationEventsService,
|
||||||
addons?: IAddonProviders,
|
addons?: IAddonProviders,
|
||||||
) {
|
) {
|
||||||
this.addonStore = addonStore;
|
this.addonStore = addonStore;
|
||||||
@ -80,6 +81,7 @@ export default class AddonService {
|
|||||||
getAddons({
|
getAddons({
|
||||||
getLogger,
|
getLogger,
|
||||||
unleashUrl: server.unleashUrl,
|
unleashUrl: server.unleashUrl,
|
||||||
|
integrationEventsService,
|
||||||
flagResolver,
|
flagResolver,
|
||||||
});
|
});
|
||||||
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
|
this.sensitiveParams = this.loadSensitiveParams(this.addonProviders);
|
||||||
@ -145,6 +147,7 @@ export default class AddonService {
|
|||||||
addonProviders[addon.provider].handleEvent(
|
addonProviders[addon.provider].handleEvent(
|
||||||
event,
|
event,
|
||||||
addon.parameters,
|
addon.parameters,
|
||||||
|
addon.id,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -201,6 +201,7 @@ export const createServices = (
|
|||||||
config,
|
config,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
eventService,
|
eventService,
|
||||||
|
integrationEventsService,
|
||||||
);
|
);
|
||||||
const sessionService = new SessionService(stores, config);
|
const sessionService = new SessionService(stores, config);
|
||||||
const settingService = new SettingService(stores, config, eventService);
|
const settingService = new SettingService(stores, config, eventService);
|
||||||
|
@ -7,6 +7,8 @@ import type { IProjectStats } from '../features/project/project-service';
|
|||||||
import type { CreateFeatureStrategySchema } from '../openapi';
|
import type { CreateFeatureStrategySchema } from '../openapi';
|
||||||
import type { ProjectEnvironment } from '../features/project/project-store-type';
|
import type { ProjectEnvironment } from '../features/project/project-store-type';
|
||||||
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
import type { FeatureSearchEnvironmentSchema } from '../openapi/spec/feature-search-environment-schema';
|
||||||
|
import type { IntegrationEventsService } from '../features/integration-events/integration-events-service';
|
||||||
|
import type { IFlagResolver } from './experimental';
|
||||||
|
|
||||||
export type Operator = (typeof ALL_OPERATORS)[number];
|
export type Operator = (typeof ALL_OPERATORS)[number];
|
||||||
|
|
||||||
@ -376,6 +378,8 @@ export interface IAddonAlert {
|
|||||||
export interface IAddonConfig {
|
export interface IAddonConfig {
|
||||||
getLogger: LogProvider;
|
getLogger: LogProvider;
|
||||||
unleashUrl: string;
|
unleashUrl: string;
|
||||||
|
integrationEventsService: IntegrationEventsService;
|
||||||
|
flagResolver: IFlagResolver;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IUserWithRole {
|
export interface IUserWithRole {
|
||||||
|
@ -7,7 +7,7 @@ import { type IUnleashStores, TEST_AUDIT_USER } from '../../../lib/types';
|
|||||||
import SimpleAddon from '../../../lib/services/addon-service-test-simple-addon';
|
import SimpleAddon from '../../../lib/services/addon-service-test-simple-addon';
|
||||||
import TagTypeService from '../../../lib/features/tag-type/tag-type-service';
|
import TagTypeService from '../../../lib/features/tag-type/tag-type-service';
|
||||||
import { FEATURE_CREATED } from '../../../lib/types/events';
|
import { FEATURE_CREATED } from '../../../lib/types/events';
|
||||||
import { EventService } from '../../../lib/services';
|
import { EventService, IntegrationEventsService } from '../../../lib/services';
|
||||||
|
|
||||||
const addonProvider = { simple: new SimpleAddon() };
|
const addonProvider = { simple: new SimpleAddon() };
|
||||||
|
|
||||||
@ -24,11 +24,16 @@ beforeAll(async () => {
|
|||||||
stores = db.stores;
|
stores = db.stores;
|
||||||
const eventService = new EventService(stores, config);
|
const eventService = new EventService(stores, config);
|
||||||
const tagTypeService = new TagTypeService(stores, config, eventService);
|
const tagTypeService = new TagTypeService(stores, config, eventService);
|
||||||
|
const integrationEventsService = new IntegrationEventsService(
|
||||||
|
stores,
|
||||||
|
config,
|
||||||
|
);
|
||||||
addonService = new AddonService(
|
addonService = new AddonService(
|
||||||
stores,
|
stores,
|
||||||
config,
|
config,
|
||||||
tagTypeService,
|
tagTypeService,
|
||||||
eventService,
|
eventService,
|
||||||
|
integrationEventsService,
|
||||||
addonProvider,
|
addonProvider,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user