From 842b51885ba8d9bbe90a06fcfafe40cff8f34e25 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Fri, 16 Feb 2024 10:40:53 +0100 Subject: [PATCH 1/9] fix: test are more stable by using events --- CHANGELOG.md | 4 ++++ config/test.json | 26 +++++++++++++++++++-- package-lock.json | 4 ++-- package.json | 5 ++-- test/lib/Launcher.ts | 6 ++--- test/lib/testUtils.ts | 48 ++++++++++++++++++++++++++++++++------- test/lib/webhookServer.ts | 35 ++++++++++++++++++++++++++++ 7 files changed, 111 insertions(+), 17 deletions(-) create mode 100644 test/lib/webhookServer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f7401e7d..b3b32b15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 3.7.4 + +- Tests are now more stable by waiting for events of message handling + ## 3.7.3 - the webhooksV2 module is now named webhooks diff --git a/config/test.json b/config/test.json index c50e4e34..136a740f 100644 --- a/config/test.json +++ b/config/test.json @@ -11,7 +11,7 @@ }, "console": { "type": "logLevelFilter", - "level": "WARN", + "level": "ERROR", "appender": "consoleAppender" } }, @@ -24,5 +24,27 @@ } }, "infrastructure": { "httpServer": { "enabled": true } }, - "modules": { "coreHttpApi": { "docs": { "enabled": true } } } + "modules": { + "coreHttpApi": { + "docs": { + "enabled": true + } + }, + "webhooks": { + "displayName": "Webhooks", + "enabled": true, + "location": "webhooks/WebhooksModule", + "targets": { + "testTarget": { + "url": "http://localhost:3769/" + } + }, + "webhooks": [ + { + "triggers": ["**"], + "target": "testTarget" + } + ] + } + } } diff --git a/package-lock.json b/package-lock.json index 9872378c..c69f6881 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nmshd/connector", - "version": "3.7.3", + "version": "3.7.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nmshd/connector", - "version": "3.7.3", + "version": "3.7.4", "license": "MIT", "workspaces": [ ".", diff --git a/package.json b/package.json index 5a9d4002..d65b0a16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/connector", - "version": "3.7.3", + "version": "3.7.4", "private": true, "description": "The Enmeshed Connector", "homepage": "https://enmeshed.eu/integrate", @@ -46,7 +46,8 @@ "setupFilesAfterEnv": [ "jest-expect-message", "./test/lib/validation.ts", - "./test/lib/customMatcher.ts" + "./test/lib/customMatcher.ts", + "./test/lib/webhookServer.ts" ], "testEnvironment": "node", "testTimeout": 60000, diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index e1c0bd50..bcf07ae0 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -34,7 +34,7 @@ export class Launcher { public async launchSimple(): Promise { const port = await getPort(); - const accountName = this.randomString(); + const accountName = Launcher.randomString(); this._processes.push(this.spawnConnector(port, accountName)); @@ -52,7 +52,7 @@ export class Launcher { clients.push(ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey })); ports.push(port); - const accountName = this.randomString(); + const accountName = Launcher.randomString(); this._processes.push(this.spawnConnector(port, accountName)); } @@ -60,7 +60,7 @@ export class Launcher { return clients; } - private randomString(): string { + public static randomString(): string { return Math.random().toString(36).substring(7); } diff --git a/test/lib/testUtils.ts b/test/lib/testUtils.ts index f5f07671..0ec64150 100644 --- a/test/lib/testUtils.ts +++ b/test/lib/testUtils.ts @@ -17,7 +17,9 @@ import { } from "@nmshd/connector-sdk"; import fs from "fs"; import { DateTime } from "luxon"; +import { Launcher } from "./Launcher"; import { ValidationSchema } from "./validation"; +import { getEvents, startEventLog, stopEventLog } from "./webhookServer"; export async function syncUntil(client: ConnectorClient, until: (syncResult: ConnectorSyncResult) => boolean): Promise { const syncResponse = await client.account.sync(); @@ -58,31 +60,61 @@ export async function syncUntilHasMessages(client: ConnectorClient, expectedNumb } export async function syncUntilHasMessageWithRequest(client: ConnectorClient, requestId: string): Promise { + const isRequest = (content: any) => content["@type"] === "Request" && content.id === requestId; const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { - return syncResult.messages.filter((m: ConnectorMessage) => (m.content as any)["@type"] === "Request" && (m.content as any).id === requestId); + return syncResult.messages.filter((m: ConnectorMessage) => isRequest(m.content)); }; + + const name = Launcher.randomString(); + startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); + let events = getEvents(name); + while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isRequest(e.data.message?.content))) { + await sleep(500); + events = getEvents(name); + } + stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } export async function syncUntilHasMessageWithNotification(client: ConnectorClient, notificationId: string): Promise { + const isNotification = (content: any) => { + if (!content) { + return false; + } + return content["@type"] === "Notification" && content.id === notificationId; + }; const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { - return syncResult.messages.filter((m: ConnectorMessage) => (m.content as any)["@type"] === "Notification" && (m.content as any).id === notificationId); + return syncResult.messages.filter((m: ConnectorMessage) => isNotification(m.content)); }; + const name = Launcher.randomString(); + startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); + let events = getEvents(name); + while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isNotification(e.data.message?.content))) { + await sleep(500); + events = getEvents(name); + } + stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } export async function syncUntilHasMessageWithResponse(client: ConnectorClient, requestId: string): Promise { + const isResponse = (content: any) => content["@type"] === "ResponseWrapper" && content.requestId === requestId; const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { - return syncResult.messages.filter((m: ConnectorMessage) => { - const matcher = (m.content as any)["@type"] === "ResponseWrapper" && (m.content as any).requestId === requestId; - return matcher; - }); + return syncResult.messages.filter((m: ConnectorMessage) => isResponse(m.content)); }; + + const name = Launcher.randomString(); + startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => { - const length = filterRequestMessagesByRequestId(syncResult).length; - return length !== 0; + return filterRequestMessagesByRequestId(syncResult).length !== 0; }); + let events = getEvents(name); + while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isResponse(e.data.message?.content))) { + await sleep(500); + events = getEvents(name); + } + stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } diff --git a/test/lib/webhookServer.ts b/test/lib/webhookServer.ts new file mode 100644 index 00000000..492874bd --- /dev/null +++ b/test/lib/webhookServer.ts @@ -0,0 +1,35 @@ +import express from "express"; + +const app = express(); + +app.use(express.json()); +app.use((req, res) => { + Object.keys(events).forEach((key) => { + events[key]?.push(req.body); + }); + res.send("OK"); +}); + +interface EventData { + trigger: string; + data: any; +} +const events: Record = {}; + +const server = app.listen(3769); + +export function getEvents(name: string): EventData[] { + return events[name] ?? []; +} + +export function startEventLog(name: string): void { + events[name] = []; +} + +export function stopEventLog(name: string): void { + delete events[name]; +} + +afterAll(() => { + server.close(); +}); From a3502e90b128bd9328ed2c8028f17a3cf3a69310 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Fri, 16 Feb 2024 11:11:37 +0100 Subject: [PATCH 2/9] =?UTF-8?q?chore:=20use=20better=20implementation?= =?UTF-8?q?=C2=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/lib/Launcher.ts | 9 +++++---- test/lib/testUtils.ts | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index bcf07ae0..b97fe838 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -1,4 +1,5 @@ import { ConnectorClient } from "@nmshd/connector-sdk"; +import { Random, RandomCharacterRange } from "@nmshd/transport"; import { ChildProcess, spawn } from "child_process"; import path from "path"; import getPort from "./getPort"; @@ -34,7 +35,7 @@ export class Launcher { public async launchSimple(): Promise { const port = await getPort(); - const accountName = Launcher.randomString(); + const accountName = await this.randomString(); this._processes.push(this.spawnConnector(port, accountName)); @@ -52,7 +53,7 @@ export class Launcher { clients.push(ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey })); ports.push(port); - const accountName = Launcher.randomString(); + const accountName = await this.randomString(); this._processes.push(this.spawnConnector(port, accountName)); } @@ -60,8 +61,8 @@ export class Launcher { return clients; } - public static randomString(): string { - return Math.random().toString(36).substring(7); + public async randomString(): Promise { + return await Random.string(7, RandomCharacterRange.Alphabet); } public stop(): void { diff --git a/test/lib/testUtils.ts b/test/lib/testUtils.ts index 0ec64150..0037bd54 100644 --- a/test/lib/testUtils.ts +++ b/test/lib/testUtils.ts @@ -65,7 +65,7 @@ export async function syncUntilHasMessageWithRequest(client: ConnectorClient, re return syncResult.messages.filter((m: ConnectorMessage) => isRequest(m.content)); }; - const name = Launcher.randomString(); + const name = await new Launcher().randomString(); startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); let events = getEvents(name); @@ -86,7 +86,7 @@ export async function syncUntilHasMessageWithNotification(client: ConnectorClien const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { return syncResult.messages.filter((m: ConnectorMessage) => isNotification(m.content)); }; - const name = Launcher.randomString(); + const name = await new Launcher().randomString(); startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); let events = getEvents(name); @@ -104,7 +104,7 @@ export async function syncUntilHasMessageWithResponse(client: ConnectorClient, r return syncResult.messages.filter((m: ConnectorMessage) => isResponse(m.content)); }; - const name = Launcher.randomString(); + const name = await new Launcher().randomString(); startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => { return filterRequestMessagesByRequestId(syncResult).length !== 0; From 2f5d285965fe9b7484f4e40d549d5d59f91f2821 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Fri, 16 Feb 2024 13:48:35 +0100 Subject: [PATCH 3/9] chore: move webhook server creation in launcher --- CHANGELOG.md | 4 -- config/test.json | 11 +---- package-lock.json | 4 +- package.json | 5 +-- src/index.ts | 10 +++++ test/attributes.test.ts | 8 ++-- test/lib/Launcher.ts | 90 +++++++++++++++++++++++++++++++++------ test/lib/testUtils.ts | 41 +++++++++--------- test/lib/webhookServer.ts | 35 --------------- 9 files changed, 117 insertions(+), 91 deletions(-) delete mode 100644 test/lib/webhookServer.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b3b32b15..f7401e7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,5 @@ # Changelog -## 3.7.4 - -- Tests are now more stable by waiting for events of message handling - ## 3.7.3 - the webhooksV2 module is now named webhooks diff --git a/config/test.json b/config/test.json index 136a740f..854b3b40 100644 --- a/config/test.json +++ b/config/test.json @@ -11,7 +11,7 @@ }, "console": { "type": "logLevelFilter", - "level": "ERROR", + "level": "DEBUG", "appender": "consoleAppender" } }, @@ -31,18 +31,11 @@ } }, "webhooks": { - "displayName": "Webhooks", "enabled": true, - "location": "webhooks/WebhooksModule", - "targets": { - "testTarget": { - "url": "http://localhost:3769/" - } - }, "webhooks": [ { "triggers": ["**"], - "target": "testTarget" + "target": { "url": "http://localhost:3769/" } } ] } diff --git a/package-lock.json b/package-lock.json index c69f6881..9872378c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@nmshd/connector", - "version": "3.7.4", + "version": "3.7.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nmshd/connector", - "version": "3.7.4", + "version": "3.7.3", "license": "MIT", "workspaces": [ ".", diff --git a/package.json b/package.json index d65b0a16..5a9d4002 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nmshd/connector", - "version": "3.7.4", + "version": "3.7.3", "private": true, "description": "The Enmeshed Connector", "homepage": "https://enmeshed.eu/integrate", @@ -46,8 +46,7 @@ "setupFilesAfterEnv": [ "jest-expect-message", "./test/lib/validation.ts", - "./test/lib/customMatcher.ts", - "./test/lib/webhookServer.ts" + "./test/lib/customMatcher.ts" ], "testEnvironment": "node", "testTimeout": 60000, diff --git a/src/index.ts b/src/index.ts index 2ffd6a20..67d9cbb7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,6 +21,16 @@ export function createConnectorConfig(overrides?: RuntimeConfig): ConnectorRunti variable.key = variable.key.replace(/__/g, ":"); variable.value = parseString(variable.value); + // REVISIT: This is a workaround for the issue that nconf does not parse JSON objects correctly + try { + const newValue = JSON.parse(variable.value); + if (typeof newValue === "object") { + variable.value = newValue; + } + } catch (e) { + // Do nothing + } + return variable; } }) diff --git a/test/attributes.test.ts b/test/attributes.test.ts index 408fa3c4..b0d3811a 100644 --- a/test/attributes.test.ts +++ b/test/attributes.test.ts @@ -1,5 +1,5 @@ -import { ConnectorClient, ConnectorRelationshipAttribute } from "@nmshd/connector-sdk"; -import { Launcher } from "./lib/Launcher"; +import { ConnectorRelationshipAttribute } from "@nmshd/connector-sdk"; +import { ConnectorClientWithMetadata, Launcher } from "./lib/Launcher"; import { QueryParamConditions } from "./lib/QueryParamConditions"; import { createRepositoryAttribute, @@ -12,8 +12,8 @@ import { import { ValidationSchema } from "./lib/validation"; const launcher = new Launcher(); -let client1: ConnectorClient; -let client2: ConnectorClient; +let client1: ConnectorClientWithMetadata; +let client2: ConnectorClientWithMetadata; let client1Address: string; let client2Address: string; diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index b97fe838..25cac440 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -1,15 +1,48 @@ import { ConnectorClient } from "@nmshd/connector-sdk"; import { Random, RandomCharacterRange } from "@nmshd/transport"; import { ChildProcess, spawn } from "child_process"; +import express from "express"; +import { Server } from "http"; import path from "path"; import getPort from "./getPort"; import waitForConnector from "./waitForConnector"; +interface EventData { + trigger: string; + data: any; +} + +export type ConnectorClientWithMetadata = ConnectorClient & { + /* eslint-disable @typescript-eslint/naming-convention */ + _metadata?: Record; + _events: { + getEvents(name: string): EventData[]; + startEventLog(name: string): void; + stopEventLog(name: string): void; + }; + /* eslint-enable @typescript-eslint/naming-convention */ +}; + export class Launcher { - private readonly _processes: ChildProcess[] = []; + private readonly _processes: { connector: ChildProcess; webhookServer: Server }[] = []; private readonly apiKey = "xxx"; + private readonly events: Record = {}; + + private startWebHookServer(port: number): Server { + const app = express(); + + app.use(express.json()); + app.use((req, res) => { + Object.keys(this.events).forEach((key) => { + this.events[key]?.push(req.body); + }); + res.send("OK"); + }); + + return app.listen(port); + } - private spawnConnector(port: number, accountName: string) { + private async spawnConnector(port: number, accountName: string) { const env = process.env; env["infrastructure:httpServer:port"] = port.toString(); env["infrastructure:httpServer:apiKey"] = this.apiKey; @@ -26,35 +59,63 @@ export class Launcher { env.NODE_CONFIG_ENV = "test"; env.DATABASE_NAME = accountName; - return spawn("node", ["dist/index.js"], { - env: { ...process.env, ...env }, - cwd: path.resolve(`${__dirname}/../..`), - stdio: "inherit" - }); + const webhookServerPort = await getPort(); + env["modules:webhooks:webhooks"] = JSON.stringify([ + { + triggers: ["**"], + target: { url: `http://localhost:${webhookServerPort}` } + } + ]); + // `http://localhost:${webhookServerPort}`; + + const webhookServer = this.startWebHookServer(webhookServerPort); + + return { + connector: spawn("node", ["dist/index.js"], { + env: { ...process.env, ...env }, + cwd: path.resolve(`${__dirname}/../..`), + stdio: "inherit" + }), + webhookServer + }; } public async launchSimple(): Promise { const port = await getPort(); const accountName = await this.randomString(); - this._processes.push(this.spawnConnector(port, accountName)); + this._processes.push(await this.spawnConnector(port, accountName)); await waitForConnector(port); return `http://localhost:${port}`; } - public async launch(count: number): Promise { - const clients: ConnectorClient[] = []; + public async launch(count: number): Promise { + const clients: ConnectorClientWithMetadata[] = []; const ports: number[] = []; for (let i = 0; i < count; i++) { const port = await getPort(); - clients.push(ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey })); + const connectorClient = ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey }) as ConnectorClientWithMetadata; + connectorClient._events = { + getEvents: (name: string): EventData[] => { + return this.events[name] ?? []; + }, + + startEventLog: (name: string): void => { + this.events[name] = []; + }, + + stopEventLog: (name: string): void => { + delete this.events[name]; + } + }; + clients.push(connectorClient); ports.push(port); const accountName = await this.randomString(); - this._processes.push(this.spawnConnector(port, accountName)); + this._processes.push(await this.spawnConnector(port, accountName)); } await Promise.all(ports.map(waitForConnector)); @@ -66,6 +127,9 @@ export class Launcher { } public stop(): void { - this._processes.forEach((p) => p.kill()); + this._processes.forEach((p) => { + p.connector.kill(); + p.webhookServer.close(); + }); } } diff --git a/test/lib/testUtils.ts b/test/lib/testUtils.ts index 0037bd54..96a5f51b 100644 --- a/test/lib/testUtils.ts +++ b/test/lib/testUtils.ts @@ -17,9 +17,8 @@ import { } from "@nmshd/connector-sdk"; import fs from "fs"; import { DateTime } from "luxon"; -import { Launcher } from "./Launcher"; +import { ConnectorClientWithMetadata, Launcher } from "./Launcher"; import { ValidationSchema } from "./validation"; -import { getEvents, startEventLog, stopEventLog } from "./webhookServer"; export async function syncUntil(client: ConnectorClient, until: (syncResult: ConnectorSyncResult) => boolean): Promise { const syncResponse = await client.account.sync(); @@ -59,24 +58,24 @@ export async function syncUntilHasMessages(client: ConnectorClient, expectedNumb return syncResult.messages; } -export async function syncUntilHasMessageWithRequest(client: ConnectorClient, requestId: string): Promise { +export async function syncUntilHasMessageWithRequest(client: ConnectorClientWithMetadata, requestId: string): Promise { const isRequest = (content: any) => content["@type"] === "Request" && content.id === requestId; const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { return syncResult.messages.filter((m: ConnectorMessage) => isRequest(m.content)); }; const name = await new Launcher().randomString(); - startEventLog(name); + client._events.startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); - let events = getEvents(name); + let events = client._events.getEvents(name); while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isRequest(e.data.message?.content))) { await sleep(500); - events = getEvents(name); + events = client._events.getEvents(name); } - stopEventLog(name); + client._events.stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } -export async function syncUntilHasMessageWithNotification(client: ConnectorClient, notificationId: string): Promise { +export async function syncUntilHasMessageWithNotification(client: ConnectorClientWithMetadata, notificationId: string): Promise { const isNotification = (content: any) => { if (!content) { return false; @@ -87,34 +86,34 @@ export async function syncUntilHasMessageWithNotification(client: ConnectorClien return syncResult.messages.filter((m: ConnectorMessage) => isNotification(m.content)); }; const name = await new Launcher().randomString(); - startEventLog(name); + client._events.startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); - let events = getEvents(name); + let events = client._events.getEvents(name); while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isNotification(e.data.message?.content))) { await sleep(500); - events = getEvents(name); + events = client._events.getEvents(name); } - stopEventLog(name); + client._events.stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } -export async function syncUntilHasMessageWithResponse(client: ConnectorClient, requestId: string): Promise { +export async function syncUntilHasMessageWithResponse(client: ConnectorClientWithMetadata, requestId: string): Promise { const isResponse = (content: any) => content["@type"] === "ResponseWrapper" && content.requestId === requestId; const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { return syncResult.messages.filter((m: ConnectorMessage) => isResponse(m.content)); }; const name = await new Launcher().randomString(); - startEventLog(name); + client._events.startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => { return filterRequestMessagesByRequestId(syncResult).length !== 0; }); - let events = getEvents(name); + let events = client._events.getEvents(name); while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isResponse(e.data.message?.content))) { await sleep(500); - events = getEvents(name); + events = client._events.getEvents(name); } - stopEventLog(name); + client._events.stopEventLog(name); return filterRequestMessagesByRequestId(syncResult)[0]; } @@ -289,8 +288,8 @@ export async function createRepositoryAttribute(client: ConnectorClient, request * Returns the sender's own shared relationship attribute. */ export async function executeFullCreateAndShareRelationshipAttributeFlow( - sender: ConnectorClient, - recipient: ConnectorClient, + sender: ConnectorClientWithMetadata, + recipient: ConnectorClientWithMetadata, attributeContent: Omit ): Promise { const senderIdentityInfoResult = await sender.account.getIdentityInfo(); @@ -347,8 +346,8 @@ export async function executeFullCreateAndShareRelationshipAttributeFlow( * Returns the sender's own shared identity attribute. */ export async function executeFullCreateAndShareRepositoryAttributeFlow( - sender: ConnectorClient, - recipient: ConnectorClient, + sender: ConnectorClientWithMetadata, + recipient: ConnectorClientWithMetadata, attributeContent: ConnectorIdentityAttribute ): Promise { const recipientIdentityInfoResult = await recipient.account.getIdentityInfo(); diff --git a/test/lib/webhookServer.ts b/test/lib/webhookServer.ts deleted file mode 100644 index 492874bd..00000000 --- a/test/lib/webhookServer.ts +++ /dev/null @@ -1,35 +0,0 @@ -import express from "express"; - -const app = express(); - -app.use(express.json()); -app.use((req, res) => { - Object.keys(events).forEach((key) => { - events[key]?.push(req.body); - }); - res.send("OK"); -}); - -interface EventData { - trigger: string; - data: any; -} -const events: Record = {}; - -const server = app.listen(3769); - -export function getEvents(name: string): EventData[] { - return events[name] ?? []; -} - -export function startEventLog(name: string): void { - events[name] = []; -} - -export function stopEventLog(name: string): void { - delete events[name]; -} - -afterAll(() => { - server.close(); -}); From 1affa85e62ef43d27283905d50eca25d7500aa47 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Fri, 16 Feb 2024 13:52:48 +0100 Subject: [PATCH 4/9] revert: wrong log level --- config/test.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/test.json b/config/test.json index 854b3b40..01a0c2f1 100644 --- a/config/test.json +++ b/config/test.json @@ -11,7 +11,7 @@ }, "console": { "type": "logLevelFilter", - "level": "DEBUG", + "level": "ERROR", "appender": "consoleAppender" } }, From 54323171a654a68576a10422c1867c6e92be4587 Mon Sep 17 00:00:00 2001 From: Sebastian Mahr Date: Fri, 16 Feb 2024 14:14:56 +0100 Subject: [PATCH 5/9] chore: better parsing of config from env variable --- src/index.ts | 28 +++++----------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/index.ts b/src/index.ts index 67d9cbb7..c80c948a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -21,16 +21,6 @@ export function createConnectorConfig(overrides?: RuntimeConfig): ConnectorRunti variable.key = variable.key.replace(/__/g, ":"); variable.value = parseString(variable.value); - // REVISIT: This is a workaround for the issue that nconf does not parse JSON objects correctly - try { - const newValue = JSON.parse(variable.value); - if (typeof newValue === "object") { - variable.value = newValue; - } - } catch (e) { - // Do nothing - } - return variable; } }) @@ -70,21 +60,13 @@ function applyAlias(variable: { key: string; value: any }) { } } -function isNumeric(value: string) { - if (typeof value !== "string") return false; - - return !isNaN(value as any) && !isNaN(parseFloat(value)); -} - function parseString(value: string) { - if (value === "true") { - return true; - } else if (value === "false") { - return false; - } else if (isNumeric(value)) { - return parseFloat(value); + // REVISIT: This is a workaround for the issue that nconf does not parse JSON objects correctly + try { + value = JSON.parse(value); + } catch (e) { + // Do nothing } - return value; } From ca95ecc118179a734bbecd1489d1abe2fb8039e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 19 Feb 2024 13:05:49 +0100 Subject: [PATCH 6/9] refactor: use eventbus, codestyle --- test/attributes.test.ts | 5 ++ test/lib/Launcher.ts | 127 ++++++++++++++++++--------------------- test/lib/MockEventBus.ts | 42 +++++++++++++ test/lib/testUtils.ts | 77 ++++++++++++++---------- 4 files changed, 149 insertions(+), 102 deletions(-) create mode 100644 test/lib/MockEventBus.ts diff --git a/test/attributes.test.ts b/test/attributes.test.ts index b0d3811a..31fb1a0f 100644 --- a/test/attributes.test.ts +++ b/test/attributes.test.ts @@ -25,6 +25,11 @@ beforeAll(async () => { }, 30000); afterAll(() => launcher.stop()); +beforeEach(() => { + client1._eventBus?.reset(); + client2._eventBus?.reset(); +}); + describe("Attributes", () => { test("should create a repository attribute", async () => { const createAttributeResponse = await client1.attributes.createRepositoryAttribute({ diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index 25cac440..72192736 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -1,9 +1,11 @@ +import { DataEvent } from "@js-soft/ts-utils"; import { ConnectorClient } from "@nmshd/connector-sdk"; import { Random, RandomCharacterRange } from "@nmshd/transport"; import { ChildProcess, spawn } from "child_process"; import express from "express"; import { Server } from "http"; import path from "path"; +import { MockEventBus } from "./MockEventBus"; import getPort from "./getPort"; import waitForConnector from "./waitForConnector"; @@ -15,34 +17,52 @@ interface EventData { export type ConnectorClientWithMetadata = ConnectorClient & { /* eslint-disable @typescript-eslint/naming-convention */ _metadata?: Record; - _events: { - getEvents(name: string): EventData[]; - startEventLog(name: string): void; - stopEventLog(name: string): void; - }; + _eventBus?: MockEventBus; /* eslint-enable @typescript-eslint/naming-convention */ }; export class Launcher { - private readonly _processes: { connector: ChildProcess; webhookServer: Server }[] = []; + private readonly _processes: { connector: ChildProcess; webhookServer: Server | undefined }[] = []; private readonly apiKey = "xxx"; private readonly events: Record = {}; - private startWebHookServer(port: number): Server { - const app = express(); + public async launchSimple(): Promise { + const port = await getPort(); + const accountName = await this.randomString(); - app.use(express.json()); - app.use((req, res) => { - Object.keys(this.events).forEach((key) => { - this.events[key]?.push(req.body); - }); - res.send("OK"); - }); + this._processes.push(await this.spawnConnector(port, accountName)); + + await waitForConnector(port); + + return `http://localhost:${port}`; + } + + public async launch(count: number): Promise { + const clients: ConnectorClientWithMetadata[] = []; + const ports: number[] = []; + + for (let i = 0; i < count; i++) { + const port = await getPort(); + const connectorClient = ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey }) as ConnectorClientWithMetadata; + + connectorClient._eventBus = new MockEventBus(); + + clients.push(connectorClient); + ports.push(port); + + const accountName = await this.randomString(); + this._processes.push(await this.spawnConnector(port, accountName, connectorClient._eventBus)); + } + + await Promise.all(ports.map(waitForConnector)); + return clients; + } - return app.listen(port); + private async randomString(): Promise { + return await Random.string(7, RandomCharacterRange.Alphabet); } - private async spawnConnector(port: number, accountName: string) { + private async spawnConnector(port: number, accountName: string, eventBus?: MockEventBus) { const env = process.env; env["infrastructure:httpServer:port"] = port.toString(); env["infrastructure:httpServer:apiKey"] = this.apiKey; @@ -59,16 +79,18 @@ export class Launcher { env.NODE_CONFIG_ENV = "test"; env.DATABASE_NAME = accountName; - const webhookServerPort = await getPort(); - env["modules:webhooks:webhooks"] = JSON.stringify([ - { - triggers: ["**"], - target: { url: `http://localhost:${webhookServerPort}` } - } - ]); - // `http://localhost:${webhookServerPort}`; + let webhookServer: Server | undefined; + if (eventBus) { + const webhookServerPort = await getPort(); + env["modules:webhooks:webhooks"] = JSON.stringify([ + { + triggers: ["**"], + target: { url: `http://localhost:${webhookServerPort}` } + } + ]); - const webhookServer = this.startWebHookServer(webhookServerPort); + webhookServer = this.startWebHookServer(webhookServerPort, eventBus); + } return { connector: spawn("node", ["dist/index.js"], { @@ -80,56 +102,21 @@ export class Launcher { }; } - public async launchSimple(): Promise { - const port = await getPort(); - const accountName = await this.randomString(); - - this._processes.push(await this.spawnConnector(port, accountName)); - - await waitForConnector(port); - - return `http://localhost:${port}`; - } - - public async launch(count: number): Promise { - const clients: ConnectorClientWithMetadata[] = []; - const ports: number[] = []; + private startWebHookServer(port: number, eventBus: MockEventBus): Server { + return express() + .use(express.json()) + .use((req, res) => { + res.status(200).send("OK"); - for (let i = 0; i < count; i++) { - const port = await getPort(); - const connectorClient = ConnectorClient.create({ baseUrl: `http://localhost:${port}`, apiKey: this.apiKey }) as ConnectorClientWithMetadata; - connectorClient._events = { - getEvents: (name: string): EventData[] => { - return this.events[name] ?? []; - }, - - startEventLog: (name: string): void => { - this.events[name] = []; - }, - - stopEventLog: (name: string): void => { - delete this.events[name]; - } - }; - clients.push(connectorClient); - ports.push(port); - - const accountName = await this.randomString(); - this._processes.push(await this.spawnConnector(port, accountName)); - } - - await Promise.all(ports.map(waitForConnector)); - return clients; - } - - public async randomString(): Promise { - return await Random.string(7, RandomCharacterRange.Alphabet); + eventBus.publish(new DataEvent(req.body.trigger, req.body.data)); + }) + .listen(port); } public stop(): void { this._processes.forEach((p) => { p.connector.kill(); - p.webhookServer.close(); + p.webhookServer?.close(); }); } } diff --git a/test/lib/MockEventBus.ts b/test/lib/MockEventBus.ts new file mode 100644 index 00000000..5870ca1f --- /dev/null +++ b/test/lib/MockEventBus.ts @@ -0,0 +1,42 @@ +import { Event, EventEmitter2EventBus, getEventNamespaceFromObject } from "@js-soft/ts-utils"; +import { waitForEvent } from "./testUtils"; + +export class MockEventBus extends EventEmitter2EventBus { + public publishedEvents: Event[] = []; + + private publishPromises: Promise[] = []; + + public constructor() { + super((e) => console.log(e)); // eslint-disable-line no-console + } + + public override publish(event: Event): void { + this.publishedEvents.push(event); + + const namespace = getEventNamespaceFromObject(event); + + if (!namespace) { + throw Error("The event needs a namespace. Use the EventNamespace-decorator in order to define a namespace for a event."); + } + this.publishPromises.push(this.emitter.emitAsync(namespace, event)); + } + + public async waitForEvent(subscriptionTarget: string, predicate?: (event: TEvent) => boolean): Promise { + const alreadyTriggeredEvents = this.publishedEvents.find((e) => e.namespace === subscriptionTarget && (!predicate || predicate(e as TEvent))) as TEvent | undefined; + if (alreadyTriggeredEvents) { + return alreadyTriggeredEvents; + } + + const event = await waitForEvent(this, subscriptionTarget, predicate); + return event; + } + + public async waitForRunningEventHandlers(): Promise { + await Promise.all(this.publishPromises); + } + + public reset(): void { + this.publishedEvents = []; + this.publishPromises = []; + } +} diff --git a/test/lib/testUtils.ts b/test/lib/testUtils.ts index 96a5f51b..132b4b1f 100644 --- a/test/lib/testUtils.ts +++ b/test/lib/testUtils.ts @@ -1,4 +1,4 @@ -import { sleep } from "@js-soft/ts-utils"; +import { DataEvent, EventBus, SubscriptionTarget, sleep } from "@js-soft/ts-utils"; import { ConnectorAttribute, ConnectorClient, @@ -17,7 +17,7 @@ import { } from "@nmshd/connector-sdk"; import fs from "fs"; import { DateTime } from "luxon"; -import { ConnectorClientWithMetadata, Launcher } from "./Launcher"; +import { ConnectorClientWithMetadata } from "./Launcher"; import { ValidationSchema } from "./validation"; export async function syncUntil(client: ConnectorClient, until: (syncResult: ConnectorSyncResult) => boolean): Promise { @@ -64,15 +64,9 @@ export async function syncUntilHasMessageWithRequest(client: ConnectorClientWith return syncResult.messages.filter((m: ConnectorMessage) => isRequest(m.content)); }; - const name = await new Launcher().randomString(); - client._events.startEventLog(name); const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); - let events = client._events.getEvents(name); - while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isRequest(e.data.message?.content))) { - await sleep(500); - events = client._events.getEvents(name); - } - client._events.stopEventLog(name); + await client._eventBus!.waitForEvent>("consumption.messageProcessed", (e) => isRequest(e.data.message?.content)); + return filterRequestMessagesByRequestId(syncResult)[0]; } export async function syncUntilHasMessageWithNotification(client: ConnectorClientWithMetadata, notificationId: string): Promise { @@ -82,18 +76,13 @@ export async function syncUntilHasMessageWithNotification(client: ConnectorClien } return content["@type"] === "Notification" && content.id === notificationId; }; - const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => { - return syncResult.messages.filter((m: ConnectorMessage) => isNotification(m.content)); - }; - const name = await new Launcher().randomString(); - client._events.startEventLog(name); + + const filterRequestMessagesByRequestId = (syncResult: ConnectorSyncResult) => syncResult.messages.filter((m: ConnectorMessage) => isNotification(m.content)); + const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); - let events = client._events.getEvents(name); - while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isNotification(e.data.message?.content))) { - await sleep(500); - events = client._events.getEvents(name); - } - client._events.stopEventLog(name); + + await client._eventBus!.waitForEvent>("consumption.messageProcessed", (e) => isNotification(e.data.message?.content)); + return filterRequestMessagesByRequestId(syncResult)[0]; } @@ -103,17 +92,10 @@ export async function syncUntilHasMessageWithResponse(client: ConnectorClientWit return syncResult.messages.filter((m: ConnectorMessage) => isResponse(m.content)); }; - const name = await new Launcher().randomString(); - client._events.startEventLog(name); - const syncResult = await syncUntil(client, (syncResult) => { - return filterRequestMessagesByRequestId(syncResult).length !== 0; - }); - let events = client._events.getEvents(name); - while (!events.some((e) => e.trigger === "consumption.messageProcessed" && isResponse(e.data.message?.content))) { - await sleep(500); - events = client._events.getEvents(name); - } - client._events.stopEventLog(name); + const syncResult = await syncUntil(client, (syncResult) => filterRequestMessagesByRequestId(syncResult).length !== 0); + + await client._eventBus!.waitForEvent>("consumption.messageProcessed", (e) => isResponse(e.data.message?.content)); + return filterRequestMessagesByRequestId(syncResult)[0]; } @@ -433,3 +415,34 @@ export function combinations(...arrays: T[][]): T[][] { } return result; } + +export async function waitForEvent( + eventBus: EventBus, + subscriptionTarget: SubscriptionTarget, + assertionFunction?: (t: TEvent) => boolean, + timeout = 5000 +): Promise { + let subscriptionId: number; + + const eventPromise = new Promise((resolve) => { + subscriptionId = eventBus.subscribe(subscriptionTarget, (event: TEvent) => { + if (assertionFunction && !assertionFunction(event)) return; + + resolve(event); + }); + }); + if (!timeout) return await eventPromise.finally(() => eventBus.unsubscribe(subscriptionId)); + + let timeoutId: NodeJS.Timeout; + const timeoutPromise = new Promise((_resolve, reject) => { + timeoutId = setTimeout( + () => reject(new Error(`timeout exceeded for waiting for event ${typeof subscriptionTarget === "string" ? subscriptionTarget : subscriptionTarget.name}`)), + timeout + ); + }); + + return await Promise.race([eventPromise, timeoutPromise]).finally(() => { + eventBus.unsubscribe(subscriptionId); + clearTimeout(timeoutId); + }); +} From 1bc12afb0621d7cbab3080bcfd2fd0b8852f4bb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 19 Feb 2024 13:11:09 +0100 Subject: [PATCH 7/9] refactor: simplify webhooks test config --- config/test.json | 16 ++++------------ test/lib/Launcher.ts | 9 ++------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/config/test.json b/config/test.json index 01a0c2f1..507a2a87 100644 --- a/config/test.json +++ b/config/test.json @@ -25,19 +25,11 @@ }, "infrastructure": { "httpServer": { "enabled": true } }, "modules": { - "coreHttpApi": { - "docs": { - "enabled": true - } - }, + "coreHttpApi": { "docs": { "enabled": true } }, "webhooks": { - "enabled": true, - "webhooks": [ - { - "triggers": ["**"], - "target": { "url": "http://localhost:3769/" } - } - ] + "enabled": false, + "webhooks": [{ "triggers": ["**"], "target": "test" }], + "targets": { "test": {} } } } } diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index 72192736..88c3954a 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -82,13 +82,8 @@ export class Launcher { let webhookServer: Server | undefined; if (eventBus) { const webhookServerPort = await getPort(); - env["modules:webhooks:webhooks"] = JSON.stringify([ - { - triggers: ["**"], - target: { url: `http://localhost:${webhookServerPort}` } - } - ]); - + env["modules:webhooks:enabled"] = "true"; + env["modules:webhooks:targets:test:url"] = `http://localhost:${webhookServerPort}`; webhookServer = this.startWebHookServer(webhookServerPort, eventBus); } From 06181af5372f0573bcad986332aadb9a0e80cd3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 19 Feb 2024 13:14:28 +0100 Subject: [PATCH 8/9] chore: undo / simplify stuff --- config/test.json | 2 +- src/index.ts | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/test.json b/config/test.json index 507a2a87..2bbd1163 100644 --- a/config/test.json +++ b/config/test.json @@ -11,7 +11,7 @@ }, "console": { "type": "logLevelFilter", - "level": "ERROR", + "level": "WARN", "appender": "consoleAppender" } }, diff --git a/src/index.ts b/src/index.ts index c80c948a..afdd8a6a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -61,13 +61,11 @@ function applyAlias(variable: { key: string; value: any }) { } function parseString(value: string) { - // REVISIT: This is a workaround for the issue that nconf does not parse JSON objects correctly try { - value = JSON.parse(value); - } catch (e) { - // Do nothing + return JSON.parse(value); + } catch (_) { + return value; } - return value; } async function run() { From 5bf429454e708428cc8dc4774b64fcd1e8bb1110 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Julian=20K=C3=B6nig?= Date: Mon, 19 Feb 2024 13:16:03 +0100 Subject: [PATCH 9/9] chore: remove unused stuff --- test/lib/Launcher.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/test/lib/Launcher.ts b/test/lib/Launcher.ts index 88c3954a..926ca191 100644 --- a/test/lib/Launcher.ts +++ b/test/lib/Launcher.ts @@ -9,11 +9,6 @@ import { MockEventBus } from "./MockEventBus"; import getPort from "./getPort"; import waitForConnector from "./waitForConnector"; -interface EventData { - trigger: string; - data: any; -} - export type ConnectorClientWithMetadata = ConnectorClient & { /* eslint-disable @typescript-eslint/naming-convention */ _metadata?: Record; @@ -24,7 +19,6 @@ export type ConnectorClientWithMetadata = ConnectorClient & { export class Launcher { private readonly _processes: { connector: ChildProcess; webhookServer: Server | undefined }[] = []; private readonly apiKey = "xxx"; - private readonly events: Record = {}; public async launchSimple(): Promise { const port = await getPort();