Add End to End testing (#868)

* Ensure connection state always explicitly states all keys, even if some are undefined.

* changelog

* Fix type

* fix test types

* Add support for E2E testing

* Add CI job for e2e test

* Ensure integration test only runs when regular tests complete

* Add homerunner image

* Disallow concurrent runs

* Add concurrency to other expensive steps

* changelog

* Fix mq test

* Cache rust deps

* Drop only

* Use a shared key
This commit is contained in:
Will Hunt 2023-12-28 15:04:03 +00:00 committed by GitHub
parent 46d198c2c0
commit 8e115b40ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1927 additions and 78 deletions

View File

@ -12,6 +12,11 @@ on:
- changelog.d/**'
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
DOCKER_NAMESPACE: halfshot
PLATFORMS: linux/amd64

View File

@ -10,6 +10,10 @@ env:
DOCKER_NAMESPACE: halfshot
PLATFORMS: linux/amd64,linux/arm64
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
docker-release:
runs-on: ubuntu-latest

View File

@ -4,6 +4,11 @@ on:
push:
paths-ignore:
- changelog.d/**'
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-20.04

View File

@ -4,6 +4,10 @@ on:
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
deploy:
runs-on: ubuntu-20.04

View File

@ -13,6 +13,11 @@ on:
workflow_dispatch:
merge_group:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
lint-node:
runs-on: ubuntu-latest
@ -75,5 +80,83 @@ jobs:
with:
toolchain: stable
profile: minimal
- uses: Swatinem/rust-cache@v2
with:
shared-key: rust-cache
- run: yarn
- run: yarn test:cover
build-homerunner:
runs-on: ubuntu-latest
outputs:
homerunnersha: ${{ steps.gitsha.outputs.sha }}
steps:
- name: Checkout matrix-org/complement
uses: actions/checkout@v3
with:
repository: matrix-org/complement
- name: Get complement git sha
id: gitsha
run: echo sha=`git rev-parse --short HEAD` >> "$GITHUB_OUTPUT"
- name: Cache homerunner
id: cached
uses: actions/cache@v3
with:
path: homerunner
key: ${{ runner.os }}-homerunner-${{ steps.gitsha.outputs.sha }}
- name: "Set Go Version"
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
run: |
echo "$GOROOT_1_18_X64/bin" >> $GITHUB_PATH
echo "~/go/bin" >> $GITHUB_PATH
# Build and install homerunner
- name: Install Complement Dependencies
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
run: |
sudo apt-get update && sudo apt-get install -y libolm3 libolm-dev
- name: Build homerunner
if: ${{ steps.cached.outputs.cache-hit != 'true' }}
run: |
go build ./cmd/homerunner
integration-test:
runs-on: ubuntu-latest
timeout-minutes: 30
needs:
- test
- build-homerunner
steps:
- name: Install Complement Dependencies
run: |
sudo apt-get update && sudo apt-get install -y libolm3
- name: Load cached homerunner bin
uses: actions/cache@v3
with:
path: homerunner
key: ${{ runner.os }}-homerunner-${{ needs.build-synapse.outputs.homerunnersha }}
fail-on-cache-miss: true # Shouldn't happen, we build this in the needs step.
- name: Checkout matrix-hookshot
uses: actions/checkout@v3
with:
path: matrix-hookshot
# Setup node & run tests
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version-file: matrix-hookshot/.node-version
- uses: Swatinem/rust-cache@v2
with:
workspaces: matrix-hookshot
shared-key: rust-cache
- name: Run Homerunner tests
timeout-minutes: 10
env:
HOMERUNNER_SPAWN_HS_TIMEOUT_SECS: 100
HOMERUNNER_IMAGE: ghcr.io/element-hq/synapse/complement-synapse:latest
NODE_OPTIONS: --dns-result-order ipv4first
run: |
docker pull $HOMERUNNER_IMAGE
cd matrix-hookshot
yarn --strict-semver --frozen-lockfile
../homerunner &
bash -ic 'yarn test:e2e'

1
changelog.d/869.misc Normal file
View File

@ -0,0 +1 @@
Integrate end to end testing.

21
jest.config.ts Normal file
View File

@ -0,0 +1,21 @@
/**
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
import type {Config} from 'jest';
const config: Config = {
// The root directory that Jest should scan for tests and modules within
rootDir: "spec",
testTimeout: 60000,
transform: {
'^.+\\.tsx?$': [
'ts-jest',
{
},
],
},
};
export default config;

View File

@ -32,7 +32,7 @@
"start:matrixsender": "node --require source-map-support/register lib/App/MatrixSenderApp.js",
"start:resetcrypto": "node --require source-map-support/register lib/App/ResetCryptoStore.js",
"test": "mocha -r ts-node/register tests/init.ts tests/*.ts tests/**/*.ts",
"test:e2e": "mocha -r ts-node/register tests/init.ts tests/*.ts tests/**/*.ts",
"test:e2e": "yarn node --experimental-vm-modules $(yarn bin jest)",
"test:cover": "nyc --reporter=lcov --reporter=text yarn test",
"lint": "yarn run lint:js && yarn run lint:rs",
"lint:js": "eslint -c .eslintrc.js 'src/**/*.ts' 'tests/**/*.ts' 'web/**/*.ts' 'web/**/*.tsx'",
@ -60,7 +60,7 @@
"jira-client": "^8.2.2",
"markdown-it": "^14.0.0",
"matrix-appservice-bridge": "^9.0.1",
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.6.7-element.1",
"matrix-bot-sdk": "npm:@vector-im/matrix-bot-sdk@^0.7.0-specific-device-2",
"matrix-widget-api": "^1.6.0",
"micromatch": "^4.0.5",
"mime": "^4.0.1",
@ -88,6 +88,7 @@
"@types/chai": "^4.2.22",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.5.11",
"@types/jira-client": "^7.1.0",
"@types/markdown-it": "^13.0.7",
"@types/micromatch": "^4.0.1",
@ -102,14 +103,17 @@
"eslint": "^8.49.0",
"eslint-config-preact": "^1.3.0",
"eslint-plugin-mocha": "^10.1.0",
"homerunner-client": "^1.0.0",
"jest": "^29.7.0",
"mini.css": "^3.0.1",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"preact": "^10.5.15",
"rimraf": "^5.0.5",
"sass": "^1.51.0",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.3",
"typescript": "^5.3.3",
"vite": "^5.0.10"
}
}

62
spec/basic.spec.ts Normal file
View File

@ -0,0 +1,62 @@
import { MessageEventContent } from "matrix-bot-sdk";
import { E2ESetupTestTimeout, E2ETestEnv } from "./util/e2e-test";
import { describe, it, beforeEach, afterEach } from "@jest/globals";
import { expect } from "chai";
describe('Basic test setup', () => {
let testEnv: E2ETestEnv;
beforeEach(async () => {
testEnv = await E2ETestEnv.createTestEnv({matrixLocalparts: ['user']});
await testEnv.setUp();
}, E2ESetupTestTimeout);
afterEach(() => {
return testEnv?.tearDown();
});
it('should be able to invite the bot to a room', async () => {
const user = testEnv.getUser('user');
const roomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId });
await user.sendText(roomId, "!hookshot help");
const msg = await user.waitForRoomEvent<MessageEventContent>({
eventType: 'm.room.message', sender: testEnv.botMxid, roomId
});
// Expect help text.
expect(msg.data.content.body).to.include('!hookshot help` - This help text\n');
});
// TODO: Move test to it's own generic connections file.
it('should be able to setup a webhook', async () => {
const user = testEnv.getUser('user');
const testRoomId = await user.createRoom({ name: 'Test room', invite:[testEnv.botMxid] });
await user.waitForRoomJoin({sender: testEnv.botMxid, roomId: testRoomId });
await user.setUserPowerLevel(testEnv.botMxid, testRoomId, 50);
await user.sendText(testRoomId, "!hookshot webhook test-webhook");
const inviteResponse = await user.waitForRoomInvite({sender: testEnv.botMxid});
await user.waitForRoomEvent<MessageEventContent>({
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId,
body: 'Room configured to bridge webhooks. See admin room for secret url.'
});
const webhookUrlMessage = user.waitForRoomEvent<MessageEventContent>({
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: inviteResponse.roomId
});
await user.joinRoom(inviteResponse.roomId);
const msgData = (await webhookUrlMessage).data.content.body;
const webhookUrl = msgData.split('\n')[2];
const webhookNotice = user.waitForRoomEvent<MessageEventContent>({
eventType: 'm.room.message', sender: testEnv.botMxid, roomId: testRoomId, body: 'Hello world!'
});
// Send a webhook
await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({text: 'Hello world!'})
});
// And await the notice.
await webhookNotice;
});
});

257
spec/util/e2e-test.ts Normal file
View File

@ -0,0 +1,257 @@
import { ComplementHomeServer, createHS, destroyHS } from "./homerunner";
import { IAppserviceRegistration, MatrixClient, MembershipEventContent, PowerLevelsEventContent } from "matrix-bot-sdk";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { BridgeConfig, BridgeConfigRoot } from "../../src/config/Config";
import { start } from "../../src/App/BridgeApp";
import { RSAKeyPairOptions, generateKeyPair } from "node:crypto";
import path from "node:path";
const WAIT_EVENT_TIMEOUT = 10000;
export const E2ESetupTestTimeout = 60000;
interface Opts {
matrixLocalparts?: string[];
config?: Partial<BridgeConfigRoot>,
}
export class E2ETestMatrixClient extends MatrixClient {
public async waitForPowerLevel(
roomId: string, expected: Partial<PowerLevelsEventContent>,
): Promise<{roomId: string, data: {
sender: string, type: string, state_key?: string, content: PowerLevelsEventContent, event_id: string,
}}> {
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
sender: string, type: string, content: Record<string, unknown>, event_id: string, state_key: string,
}) => {
if (eventRoomId !== roomId) {
return undefined;
}
if (eventData.type !== "m.room.power_levels") {
return undefined;
}
if (eventData.state_key !== "") {
return undefined;
}
// Check only the keys we care about
for (const [key, value] of Object.entries(expected)) {
const evValue = eventData.content[key] ?? undefined;
const sortOrder = value !== null && typeof value === "object" ? Object.keys(value).sort() : undefined;
const jsonLeft = JSON.stringify(evValue, sortOrder);
const jsonRight = JSON.stringify(value, sortOrder);
if (jsonLeft !== jsonRight) {
return undefined;
}
}
console.info(
// eslint-disable-next-line max-len
`${eventRoomId} ${eventData.event_id} ${eventData.sender}`
);
return {roomId: eventRoomId, data: eventData};
}, `Timed out waiting for powerlevel from in ${roomId}`)
}
public async waitForRoomEvent<T extends object = Record<string, unknown>>(
opts: {eventType: string, sender: string, roomId?: string, stateKey?: string, body?: string}
): Promise<{roomId: string, data: {
sender: string, type: string, state_key?: string, content: T, event_id: string,
}}> {
const {eventType, sender, roomId, stateKey} = opts;
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
sender: string, type: string, state_key?: string, content: T, event_id: string,
}) => {
if (eventData.sender !== sender) {
return undefined;
}
if (eventData.type !== eventType) {
return undefined;
}
if (roomId && eventRoomId !== roomId) {
return undefined;
}
if (stateKey !== undefined && eventData.state_key !== stateKey) {
return undefined;
}
const body = 'body' in eventData.content && eventData.content.body;
if (opts.body && body !== opts.body) {
return undefined;
}
console.info(
// eslint-disable-next-line max-len
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? body ?? ''}`
);
return {roomId: eventRoomId, data: eventData};
}, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`)
}
public async waitForRoomJoin(
opts: {sender: string, roomId?: string}
): Promise<{roomId: string, data: unknown}> {
const {sender, roomId} = opts;
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
sender: string,
state_key: string,
content: MembershipEventContent,
}) => {
if (eventData.state_key !== sender) {
return;
}
if (roomId && eventRoomId !== roomId) {
return;
}
if (eventData.content.membership !== "join") {
return;
}
return {roomId: eventRoomId, data: eventData};
}, `Timed out waiting for join to ${roomId || "any room"} from ${sender}`)
}
public async waitForRoomInvite(
opts: {sender: string, roomId?: string}
): Promise<{roomId: string, data: unknown}> {
const {sender, roomId} = opts;
return this.waitForEvent('room.invite', (eventRoomId: string, eventData: {
sender: string
}) => {
if (eventData.sender !== sender) {
return undefined;
}
if (roomId && eventRoomId !== roomId) {
return undefined;
}
return {roomId: eventRoomId, data: eventData};
}, `Timed out waiting for invite to ${roomId || "any room"} from ${sender}`)
}
public async waitForEvent<T>(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
emitterType: string, filterFn: (...args: any[]) => T|undefined, timeoutMsg: string)
: Promise<T> {
return new Promise((resolve, reject) => {
// eslint-disable-next-line prefer-const
let timer: NodeJS.Timeout;
const fn = (...args: unknown[]) => {
const data = filterFn(...args);
if (data) {
clearTimeout(timer);
resolve(data);
}
};
timer = setTimeout(() => {
this.removeListener(emitterType, fn);
reject(new Error(timeoutMsg));
}, WAIT_EVENT_TIMEOUT);
this.on(emitterType, fn)
});
}
}
export class E2ETestEnv {
static async createTestEnv(opts: Opts): Promise<E2ETestEnv> {
const workerID = parseInt(process.env.JEST_WORKER_ID ?? '0');
const { matrixLocalparts, config: providedConfig } = opts;
const keyPromise = new Promise<string>((resolve, reject) => generateKeyPair("rsa", {
modulusLength: 4096,
privateKeyEncoding: {
type: "pkcs8",
format: "pem",
},
publicKeyEncoding: {
format: "pem",
type: "pkcs1",
}
} satisfies RSAKeyPairOptions<"pem", "pem">, (err, _, privateKey) => {
if (err) { reject(err) } else { resolve(privateKey) }
}));
// Configure homeserver and bots
const [homeserver, dir, privateKey] = await Promise.all([
createHS([...matrixLocalparts || []], workerID),
mkdtemp('hookshot-int-test'),
keyPromise,
]);
const keyPath = path.join(dir, 'key.pem');
await writeFile(keyPath, privateKey, 'utf-8');
const webhooksPort = 9500 + workerID;
const config = new BridgeConfig({
bridge: {
domain: homeserver.domain,
url: homeserver.url,
port: homeserver.appPort,
bindAddress: '0.0.0.0',
},
logging: {
level: 'info',
},
queue: {
monolithic: true,
},
// Always enable webhooks so that hookshot starts.
generic: {
enabled: true,
urlPrefix: `http://localhost:${webhooksPort}/webhook`,
},
listeners: [{
port: webhooksPort,
bindAddress: '0.0.0.0',
resources: ['webhooks'],
}],
passFile: keyPath,
...providedConfig,
});
const registration: IAppserviceRegistration = {
as_token: homeserver.asToken,
hs_token: homeserver.hsToken,
sender_localpart: 'hookshot',
namespaces: {
users: [{
regex: `@hookshot:${homeserver.domain}`,
exclusive: true,
}],
rooms: [],
aliases: [],
}
};
const app = await start(config, registration);
return new E2ETestEnv(homeserver, app, opts, config, dir);
}
private constructor(
public readonly homeserver: ComplementHomeServer,
public app: Awaited<ReturnType<typeof start>>,
public readonly opts: Opts,
private readonly config: BridgeConfig,
private readonly dir: string,
) { }
public get botMxid() {
return `@hookshot:${this.homeserver.domain}`;
}
public async setUp(): Promise<void> {
await this.app.bridgeApp.start();
}
public async tearDown(): Promise<void> {
await this.app.bridgeApp.stop();
await this.app.listener.stop();
await this.app.storage.disconnect?.();
this.homeserver.users.forEach(u => u.client.stop());
await destroyHS(this.homeserver.id);
await rm(this.dir, { recursive: true });
}
public getUser(localpart: string) {
const u = this.homeserver.users.find(u => u.userId === `@${localpart}:${this.homeserver.domain}`);
if (!u) {
throw Error("User missing from test");
}
return u.client;
}
}

134
spec/util/homerunner.ts Normal file
View File

@ -0,0 +1,134 @@
import { MatrixClient } from "matrix-bot-sdk";
import { createHash, createHmac, randomUUID } from "crypto";
import { Homerunner } from "homerunner-client";
import { E2ETestMatrixClient } from "./e2e-test";
const HOMERUNNER_IMAGE = process.env.HOMERUNNER_IMAGE || 'ghcr.io/element-hq/synapse/complement-synapse:latest';
export const DEFAULT_REGISTRATION_SHARED_SECRET = (
process.env.REGISTRATION_SHARED_SECRET || 'complement'
);
const COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT = (
process.env.COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT || "host.docker.internal"
);
const homerunner = new Homerunner.Client();
export interface ComplementHomeServer {
id: string,
url: string,
domain: string,
users: {userId: string, accessToken: string, deviceId: string, client: E2ETestMatrixClient}[]
asToken: string,
hsToken: string,
appPort: number,
}
async function waitForHomerunner() {
let attempts = 0;
do {
attempts++;
console.log(`Waiting for homerunner to be ready (${attempts}/100)`);
try {
await homerunner.health();
break;
}
catch (ex) {
await new Promise(r => setTimeout(r, 1000));
}
} while (attempts < 100)
if (attempts === 100) {
throw Error('Homerunner was not ready after 100 attempts');
}
}
export async function createHS(localparts: string[] = [], workerId: number): Promise<ComplementHomeServer> {
await waitForHomerunner();
const appPort = 49600 + workerId;
const blueprint = `hookshot_integration_test_${Date.now()}`;
const asToken = randomUUID();
const hsToken = randomUUID();
const blueprintResponse = await homerunner.create({
base_image_uri: HOMERUNNER_IMAGE,
blueprint: {
Name: blueprint,
Homeservers: [{
Name: 'hookshot',
Users: localparts.map(localpart => ({Localpart: localpart, DisplayName: localpart})),
ApplicationServices: [{
ID: 'hookshot',
URL: `http://${COMPLEMENT_HOSTNAME_RUNNING_COMPLEMENT}:${appPort}`,
SenderLocalpart: 'hookshot',
RateLimited: false,
...{ASToken: asToken,
HSToken: hsToken},
}]
}],
}
});
const [homeserverName, homeserver] = Object.entries(blueprintResponse.homeservers)[0];
// Skip AS user.
const users = Object.entries(homeserver.AccessTokens)
.filter(([_uId, accessToken]) => accessToken !== asToken)
.map(([userId, accessToken]) => ({
userId: userId,
accessToken,
deviceId: homeserver.DeviceIDs[userId],
client: new E2ETestMatrixClient(homeserver.BaseURL, accessToken),
})
);
// Start syncing proactively.
await Promise.all(users.map(u => u.client.start()));
return {
users,
id: blueprint,
url: homeserver.BaseURL,
domain: homeserverName,
asToken,
appPort,
hsToken,
};
}
export function destroyHS(
id: string
): Promise<void> {
return homerunner.destroy(id);
}
export async function registerUser(
homeserverUrl: string,
user: { username: string, admin: boolean },
sharedSecret = DEFAULT_REGISTRATION_SHARED_SECRET,
): Promise<{mxid: string, client: MatrixClient}> {
const registerUrl: string = (() => {
const url = new URL(homeserverUrl);
url.pathname = '/_synapse/admin/v1/register';
return url.toString();
})();
const nonce = await fetch(registerUrl, { method: 'GET' }).then(res => res.json()).then((res) => (res as any).nonce);
const password = createHash('sha256')
.update(user.username)
.update(sharedSecret)
.digest('hex');
const hmac = createHmac('sha1', sharedSecret)
.update(nonce).update("\x00")
.update(user.username).update("\x00")
.update(password).update("\x00")
.update(user.admin ? 'admin' : 'notadmin')
.digest('hex');
return await fetch(registerUrl, { method: "POST", body: JSON.stringify(
{
nonce,
username: user.username,
password,
admin: user.admin,
mac: hmac,
}
)}).then(res => res.json()).then(res => ({
mxid: (res as {user_id: string}).user_id,
client: new MatrixClient(homeserverUrl, (res as {access_token: string}).access_token),
})).catch(err => { console.log(err.response.body); throw new Error(`Failed to register user: ${err}`); });
}

View File

@ -1,12 +1,11 @@
import { Bridge } from "../Bridge";
import { BridgeConfig, parseRegistrationFile } from "../config/Config";
import { Webhooks } from "../Webhooks";
import { MatrixSender } from "../MatrixSender";
import { UserNotificationWatcher } from "../Notifications/UserNotificationWatcher";
import { ListenerService } from "../ListenerService";
import { Logger, getBridgeVersion } from "matrix-appservice-bridge";
import { LogService } from "matrix-bot-sdk";
import { IAppserviceRegistration, LogService } from "matrix-bot-sdk";
import { getAppservice } from "../appservice";
import BotUsersManager from "../Managers/BotUsersManager";
import * as Sentry from '@sentry/node';
@ -15,11 +14,7 @@ import { GenericHookConnection } from "../Connections";
Logger.configure({console: "info"});
const log = new Logger("App");
async function start() {
const configFile = process.argv[2] || "./config.yml";
const registrationFile = process.argv[3] || "./registration.yml";
const config = await BridgeConfig.parseConfig(configFile, process.env);
const registration = await parseRegistrationFile(registrationFile);
export async function start(config: BridgeConfig, registration: IAppserviceRegistration) {
const listener = new ListenerService(config.listeners);
listener.start();
Logger.configure({
@ -65,7 +60,6 @@ async function start() {
// Don't care to await this, as the process is about to end
storage.disconnect?.();
});
await bridgeApp.start();
// XXX: Since the webhook listener listens on /, it must listen AFTER other resources
// have bound themselves.
@ -73,14 +67,30 @@ async function start() {
const webhookHandler = new Webhooks(config);
listener.bindResource('webhooks', webhookHandler.expressRouter);
}
return {
bridgeApp,
storage,
listener,
};
}
start().catch((ex) => {
if (Logger.root.configured) {
log.error("BridgeApp encountered an error and has stopped:", ex);
} else {
// eslint-disable-next-line no-console
console.error("BridgeApp encountered an error and has stopped", ex);
}
process.exit(1);
});
async function startFromFile() {
const configFile = process.argv[2] || "./config.yml";
const registrationFile = process.argv[3] || "./registration.yml";
const config = await BridgeConfig.parseConfig(configFile, process.env);
const registration = await parseRegistrationFile(registrationFile);
const { bridgeApp } = await start(config, registration);
await bridgeApp.start();
}
if (require.main === module) {
startFromFile().catch((ex) => {
if (Logger.root.configured) {
log.error("BridgeApp encountered an error and has stopped:", ex);
} else {
// eslint-disable-next-line no-console
console.error("BridgeApp encountered an error and has stopped", ex);
}
process.exit(1);
});
}

View File

@ -7,7 +7,7 @@ const mq = createMessageQueue({
describe("MessageQueueTest", () => {
describe("LocalMq", () => {
it("should be able to push an event, and listen for it", async (done) => {
it("should be able to push an event, and listen for it", (done) => {
mq.subscribe("fakeevent");
mq.on("fakeevent", (msg) => {
expect(msg).to.deep.equal({
@ -18,7 +18,7 @@ describe("MessageQueueTest", () => {
});
done();
});
await mq.push<number>({
mq.push<number>({
sender: "foo",
eventName: "fakeevent",
messageId: "foooo",

1371
yarn.lock

File diff suppressed because it is too large Load Diff