Skip to content

Commit 7ca7d96

Browse files
feat(core-webhooks): dispatch webhook events (#3771)
1 parent b87a7a8 commit 7ca7d96

File tree

3 files changed

+94
-12
lines changed

3 files changed

+94
-12
lines changed

__tests__/unit/core-webhooks/listener.test.ts

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
import "jest-extended";
22

3-
import { Application } from "@packages/core-kernel/src/application";
43
import { Container, Utils } from "@packages/core-kernel";
4+
import { HttpOptions, HttpResponse } from "@packages/core-kernel/src/utils";
5+
import { Sandbox } from "@packages/core-test-framework";
6+
import * as coditions from "@packages/core-webhooks/src/conditions";
57
import { Database } from "@packages/core-webhooks/src/database";
8+
import { WebhookEvent } from "@packages/core-webhooks/src/events";
69
import { Identifiers } from "@packages/core-webhooks/src/identifiers";
7-
import { Listener } from "@packages/core-webhooks/src/listener";
810
import { Webhook } from "@packages/core-webhooks/src/interfaces";
11+
import { Listener } from "@packages/core-webhooks/src/listener";
912
import { dirSync, setGracefulCleanup } from "tmp";
10-
import { HttpOptions, HttpResponse } from "@packages/core-kernel/src/utils";
13+
1114
import { dummyWebhook } from "./__fixtures__/assets";
12-
import * as coditions from "@packages/core-webhooks/src/conditions";
1315

14-
let app: Application;
16+
let sandbox: Sandbox;
1517
let database: Database;
1618
let listener: Listener;
1719
let webhook: Webhook;
@@ -21,20 +23,42 @@ const logger = {
2123
error: jest.fn(),
2224
};
2325

26+
const mockEventDispatcher = {
27+
dispatch: jest.fn(),
28+
};
29+
2430
let spyOnPost: jest.SpyInstance;
2531

32+
const expectFinishedEventData = () => {
33+
return expect.objectContaining({
34+
executionTime: expect.toBeNumber(),
35+
webhook: expect.toBeObject(),
36+
payload: expect.anything(),
37+
});
38+
};
39+
40+
const expectFailedEventData = () => {
41+
return expect.objectContaining({
42+
executionTime: expect.toBeNumber(),
43+
webhook: expect.toBeObject(),
44+
payload: expect.anything(),
45+
error: expect.toBeObject(),
46+
});
47+
};
48+
2649
beforeEach(() => {
27-
app = new Application(new Container.Container());
28-
app.bind("path.cache").toConstantValue(dirSync().name);
50+
sandbox = new Sandbox();
51+
sandbox.app.bind("path.cache").toConstantValue(dirSync().name);
2952

30-
app.bind<Database>(Identifiers.Database).to(Database).inSingletonScope();
53+
sandbox.app.bind(Container.Identifiers.EventDispatcherService).toConstantValue(mockEventDispatcher);
54+
sandbox.app.bind<Database>(Identifiers.Database).to(Database).inSingletonScope();
3155

32-
app.bind(Container.Identifiers.LogService).toConstantValue(logger);
56+
sandbox.app.bind(Container.Identifiers.LogService).toConstantValue(logger);
3357

34-
database = app.get<Database>(Identifiers.Database);
58+
database = sandbox.app.get<Database>(Identifiers.Database);
3559
database.boot();
3660

37-
listener = app.resolve<Listener>(Listener);
61+
listener = sandbox.app.resolve<Listener>(Listener);
3862

3963
webhook = Object.assign({}, dummyWebhook);
4064

@@ -48,6 +72,7 @@ beforeEach(() => {
4872
});
4973

5074
afterEach(() => {
75+
jest.clearAllMocks();
5176
jest.resetAllMocks();
5277
});
5378

@@ -62,6 +87,12 @@ describe("Listener", () => {
6287

6388
expect(spyOnPost).toHaveBeenCalled();
6489
expect(logger.debug).toHaveBeenCalled();
90+
91+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledTimes(1);
92+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledWith(
93+
WebhookEvent.Broadcasted,
94+
expectFinishedEventData(),
95+
);
6596
});
6697

6798
it("should log error if broadcast is not successful", async () => {
@@ -77,6 +108,9 @@ describe("Listener", () => {
77108

78109
expect(spyOnPost).toHaveBeenCalled();
79110
expect(logger.error).toHaveBeenCalled();
111+
112+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledTimes(1);
113+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledWith(WebhookEvent.Failed, expectFailedEventData());
80114
});
81115
});
82116

@@ -90,6 +124,14 @@ describe("Listener", () => {
90124
expect(spyOnPost).toHaveBeenCalledTimes(0);
91125
});
92126

127+
it("should not broadcast if event is webhook event", async () => {
128+
database.create(webhook);
129+
130+
await listener.handle({ name: WebhookEvent.Broadcasted, data: "dummy_data" });
131+
132+
expect(spyOnPost).toHaveBeenCalledTimes(0);
133+
});
134+
93135
it("should broadcast if webhook condition is satisfied", async () => {
94136
webhook.conditions = [
95137
{
@@ -103,6 +145,12 @@ describe("Listener", () => {
103145
await listener.handle({ name: "event", data: { test: 1 } });
104146

105147
expect(spyOnPost).toHaveBeenCalledTimes(1);
148+
149+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledTimes(1);
150+
expect(mockEventDispatcher.dispatch).toHaveBeenCalledWith(
151+
WebhookEvent.Broadcasted,
152+
expectFinishedEventData(),
153+
);
106154
});
107155

108156
it("should not broadcast if webhook condition is not satisfied", async () => {
@@ -121,7 +169,7 @@ describe("Listener", () => {
121169
});
122170

123171
it("should not broadcast if webhook condition throws error", async () => {
124-
let spyOnEq = jest.spyOn(coditions, "eq").mockImplementation((actual, expected) => {
172+
const spyOnEq = jest.spyOn(coditions, "eq").mockImplementation((actual, expected) => {
125173
throw new Error();
126174
});
127175

packages/core-webhooks/src/events.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export enum WebhookEvent {
2+
Broadcasted = "webhooks.broadcasted",
3+
Failed = "webhooks.failed",
4+
}

packages/core-webhooks/src/listener.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Container, Contracts, Utils } from "@arkecosystem/core-kernel";
2+
import { performance } from "perf_hooks";
23

34
import * as conditions from "./conditions";
45
import { Database } from "./database";
6+
import { WebhookEvent } from "./events";
57
import { Identifiers } from "./identifiers";
68
import { Webhook } from "./interfaces";
79

@@ -31,6 +33,11 @@ export class Listener {
3133
* @memberof Listener
3234
*/
3335
public async handle({ name, data }): Promise<void> {
36+
// Skip own events to prevent cycling
37+
if (name.toString().includes("webhooks")) {
38+
return;
39+
}
40+
3441
const webhooks: Webhook[] = this.getWebhooks(name, data);
3542

3643
for (const webhook of webhooks) {
@@ -45,6 +52,8 @@ export class Listener {
4552
* @memberof Broadcaster
4653
*/
4754
public async broadcast(webhook: Webhook, payload: object, timeout: number = 1500): Promise<void> {
55+
const start = performance.now();
56+
4857
try {
4958
const { statusCode } = await Utils.http.post(webhook.target, {
5059
body: {
@@ -61,8 +70,29 @@ export class Listener {
6170
this.logger.debug(
6271
`Webhooks Job ${webhook.id} completed! Event [${webhook.event}] has been transmitted to [${webhook.target}] with a status of [${statusCode}].`,
6372
);
73+
74+
await this.dispatchWebhookEvent(start, webhook, payload);
6475
} catch (error) {
6576
this.logger.error(`Webhooks Job ${webhook.id} failed: ${error.message}`);
77+
78+
await this.dispatchWebhookEvent(start, webhook, payload, error);
79+
}
80+
}
81+
82+
private async dispatchWebhookEvent(start: number, webhook: Webhook, payload: object, err?: Error) {
83+
if (err) {
84+
this.app.events.dispatch(WebhookEvent.Failed, {
85+
executionTime: performance.now() - start,
86+
webhook: webhook,
87+
payload: payload,
88+
error: err,
89+
});
90+
} else {
91+
this.app.events.dispatch(WebhookEvent.Broadcasted, {
92+
executionTime: performance.now() - start,
93+
webhook: webhook,
94+
payload: payload,
95+
});
6696
}
6797
}
6898

0 commit comments

Comments
 (0)