diff --git a/package.json b/package.json
index 39fa782..cff307d 100644
--- a/package.json
+++ b/package.json
@@ -12,6 +12,7 @@
"lint": "tslint --project ./tsconfig.json -t stylish",
"start:dev": "yarn build && node lib/index.js",
"test": "ts-mocha --project ./tsconfig.json test/**/*.ts",
+ "test-integration": "NODE_ENV=harness ts-mocha --require test/integration/fixtures.ts --project ./tsconfig.json test/integration/**/*Test.ts",
"harness": "NODE_ENV=harness ts-node test/harness/launchScript.ts"
},
"devDependencies": {
diff --git a/src/Mjolnir.ts b/src/Mjolnir.ts
index 8ce085d..c5db346 100644
--- a/src/Mjolnir.ts
+++ b/src/Mjolnir.ts
@@ -209,6 +209,10 @@ export class Mjolnir {
});
}
+ public stop() {
+ this.client.stop();
+ }
+
public async addProtectedRoom(roomId: string) {
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
diff --git a/src/index.ts b/src/index.ts
index 7b92a41..ab8c379 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -20,17 +20,13 @@ import {
LogService,
MatrixClient,
PantalaimonClient,
- Permalinks,
RichConsoleLogger,
SimpleFsStorageProvider
} from "matrix-bot-sdk";
import config from "./config";
-import BanList from "./models/BanList";
-import { Mjolnir } from "./Mjolnir";
import { logMessage } from "./LogProxy";
-import { MembershipEvent } from "matrix-bot-sdk/lib/models/events/MembershipEvent";
-import * as htmlEscape from "escape-html";
import { Healthz } from "./health/healthz";
+import { setupMjolnir } from "./setup";
config.RUNTIME = {};
@@ -58,63 +54,7 @@ if (config.health.healthz.enabled) {
config.RUNTIME.client = client;
- client.on("room.invite", async (roomId: string, inviteEvent: any) => {
- const membershipEvent = new MembershipEvent(inviteEvent);
-
- const reportInvite = async () => {
- if (!config.recordIgnoredInvites) return; // Nothing to do
-
- await client.sendMessage(config.managementRoom, {
- msgtype: "m.text",
- body: `${membershipEvent.sender} has invited me to ${roomId} but the config prevents me from accepting the invitation. `
- + `If you would like this room protected, use "!mjolnir rooms add ${roomId}" so I can accept the invite.`,
- format: "org.matrix.custom.html",
- formatted_body: `${htmlEscape(membershipEvent.sender)} has invited me to ${htmlEscape(roomId)} but the config prevents me from `
- + `accepting the invitation. If you would like this room protected, use !mjolnir rooms add ${htmlEscape(roomId)}
`
- + `so I can accept the invite.`,
- });
- };
-
- if (config.autojoinOnlyIfManager) {
- const managers = await client.getJoinedRoomMembers(config.managementRoom);
- if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
- } else {
- const groupMembers = await client.unstableApis.getGroupUsers(config.acceptInvitesFromGroup);
- const userIds = groupMembers.map(m => m.user_id);
- if (!userIds.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
- }
-
- return client.joinRoom(roomId);
- });
-
- const banLists: BanList[] = [];
- const protectedRooms: { [roomId: string]: string } = {};
- const joinedRooms = await client.getJoinedRooms();
- // Ensure we're also joined to the rooms we're protecting
- LogService.info("index", "Resolving protected rooms...");
- for (const roomRef of config.protectedRooms) {
- const permalink = Permalinks.parseUrl(roomRef);
- if (!permalink.roomIdOrAlias) continue;
-
- let roomId = await client.resolveRoom(permalink.roomIdOrAlias);
- if (!joinedRooms.includes(roomId)) {
- roomId = await client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
- }
-
- protectedRooms[roomId] = roomRef;
- }
-
- // Ensure we're also in the management room
- LogService.info("index", "Resolving management room...");
- const managementRoomId = await client.resolveRoom(config.managementRoom);
- if (!joinedRooms.includes(managementRoomId)) {
- config.managementRoom = await client.joinRoom(config.managementRoom);
- } else {
- config.managementRoom = managementRoomId;
- }
- await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
-
- const bot = new Mjolnir(client, protectedRooms, banLists);
+ let bot = await setupMjolnir(client, config);
await bot.start();
})().catch(err => {
logMessage(LogLevel.ERROR, "index", err);
diff --git a/src/setup.ts b/src/setup.ts
new file mode 100644
index 0000000..8b29c7d
--- /dev/null
+++ b/src/setup.ts
@@ -0,0 +1,95 @@
+/*
+Copyright 2019-2021 The Matrix.org Foundation C.I.C.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+import { LogLevel, LogService, MatrixClient, Permalinks } from "matrix-bot-sdk";
+import { MembershipEvent } from "matrix-bot-sdk/lib/models/events/MembershipEvent";
+import * as htmlEscape from "escape-html";
+import BanList from "./models/BanList";
+import { logMessage } from "./LogProxy";
+import { Mjolnir } from "./Mjolnir";
+
+/**
+ * Adds a listener to the client that will automatically accept invitations.
+ * @param {MatrixClient} client
+ * @param options By default accepts invites from anyone.
+ * @param {string} options.managementRoom The room to report ignored invitations to if `recordIgnoredInvites` is true.
+ * @param {boolean} options.recordIgnoredInvites Whether to report invites that will be ignored to the `managementRoom`.
+ * @param {boolean} options.autojoinOnlyIfManager Whether to only accept an invitation by a user present in the `managementRoom`.
+ * @param {string} options.acceptInvitesFromGroup A group of users to accept invites from, ignores invites form users not in this group.
+ */
+export function addJoinOnInviteListener(client: MatrixClient, options) {
+ client.on("room.invite", async (roomId: string, inviteEvent: any) => {
+ const membershipEvent = new MembershipEvent(inviteEvent);
+
+ const reportInvite = async () => {
+ if (!options.recordIgnoredInvites) return; // Nothing to do
+
+ await client.sendMessage(options.managementRoom, {
+ msgtype: "m.text",
+ body: `${membershipEvent.sender} has invited me to ${roomId} but the config prevents me from accepting the invitation. `
+ + `If you would like this room protected, use "!mjolnir rooms add ${roomId}" so I can accept the invite.`,
+ format: "org.matrix.custom.html",
+ formatted_body: `${htmlEscape(membershipEvent.sender)} has invited me to ${htmlEscape(roomId)} but the config prevents me from `
+ + `accepting the invitation. If you would like this room protected, use !mjolnir rooms add ${htmlEscape(roomId)}
`
+ + `so I can accept the invite.`,
+ });
+ };
+
+ if (options.autojoinOnlyIfManager) {
+ const managers = await client.getJoinedRoomMembers(options.managementRoom);
+ if (!managers.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
+ } else {
+ const groupMembers = await client.unstableApis.getGroupUsers(options.acceptInvitesFromGroup);
+ const userIds = groupMembers.map(m => m.user_id);
+ if (!userIds.includes(membershipEvent.sender)) return reportInvite(); // ignore invite
+ }
+
+ return client.joinRoom(roomId);
+ });
+}
+
+export async function setupMjolnir(client, config): Promise {
+ addJoinOnInviteListener(client, config);
+
+ const banLists: BanList[] = [];
+ const protectedRooms: { [roomId: string]: string } = {};
+ const joinedRooms = await client.getJoinedRooms();
+ // Ensure we're also joined to the rooms we're protecting
+ LogService.info("index", "Resolving protected rooms...");
+ for (const roomRef of config.protectedRooms) {
+ const permalink = Permalinks.parseUrl(roomRef);
+ if (!permalink.roomIdOrAlias) continue;
+
+ let roomId = await client.resolveRoom(permalink.roomIdOrAlias);
+ if (!joinedRooms.includes(roomId)) {
+ roomId = await client.joinRoom(permalink.roomIdOrAlias, permalink.viaServers);
+ }
+
+ protectedRooms[roomId] = roomRef;
+ }
+
+ // Ensure we're also in the management room
+ LogService.info("index", "Resolving management room...");
+ const managementRoomId = await client.resolveRoom(config.managementRoom);
+ if (!joinedRooms.includes(managementRoomId)) {
+ config.managementRoom = await client.joinRoom(config.managementRoom);
+ } else {
+ config.managementRoom = managementRoomId;
+ }
+ await logMessage(LogLevel.INFO, "index", "Mjolnir is starting up. Use !mjolnir to query status.");
+
+ return new Mjolnir(client, protectedRooms, banLists);
+}
\ No newline at end of file
diff --git a/test/integration/clientHelper.ts b/test/integration/clientHelper.ts
new file mode 100644
index 0000000..ed36fe5
--- /dev/null
+++ b/test/integration/clientHelper.ts
@@ -0,0 +1,57 @@
+import axios from "axios";
+import { HmacSHA1 } from "crypto-js";
+import { MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
+import config from "../../src/config";
+
+export async function registerUser(username: string, displayname: string, password: string, admin: boolean) {
+ let registerUrl = `${config.homeserverUrl}/_synapse/admin/v1/register`
+ let { data } = await axios.get(registerUrl);
+ let nonce = data.nonce!;
+ let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
+ return await axios.post(registerUrl, {
+ nonce,
+ username,
+ displayname,
+ password,
+ admin,
+ mac: mac.toString()
+ })
+}
+
+/**
+ * Register a new test user with a unique username.
+ * @param isAdmin Whether to make the new user an admin.
+ * @returns A string that is the username and password of a new user.
+ */
+export async function registerNewTestUser(isAdmin: boolean) {
+ let isUserValid = false;
+ let username;
+ do {
+ username = `test-user-${Math.floor(Math.random() * 100000)}`
+ await registerUser(username, username, username, isAdmin).then(_ => isUserValid = true).catch(e => {
+ if (e.isAxiosError && e.response.data.errcode === 'M_USER_IN_USE') {
+ // FIXME: Replace with the real logging service.
+ console.log(`${username} already registered, trying another`);
+ false // continue and try again
+ } else {
+ console.error(`failed to register user ${e}`);
+ throw e;
+ }
+ })
+ } while (!isUserValid);
+ return username;
+}
+
+export async function newTestUser(isAdmin?: boolean): Promise {
+ const username = await registerNewTestUser(isAdmin);
+ const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
+ return await pantalaimon.createClientWithCredentials(username, username);
+}
+
+export function noticeListener(targetRoomdId: string, cb) {
+ return (roomId, event) => {
+ if (roomId !== targetRoomdId) return;
+ if (event?.content?.msgtype !== "m.notice") return;
+ cb(event);
+ }
+}
\ No newline at end of file
diff --git a/test/harness/config/mjolnir/harness.yaml b/test/integration/config/harness.yaml
similarity index 99%
rename from test/harness/config/mjolnir/harness.yaml
rename to test/integration/config/harness.yaml
index e65664e..c70936c 100644
--- a/test/harness/config/mjolnir/harness.yaml
+++ b/test/integration/config/harness.yaml
@@ -73,8 +73,7 @@ automaticallyRedactForReasons:
- "advertising"
# A list of rooms to protect (matrix.to URLs)
-protectedRooms:
- - "https://matrix.to/#/#lobby:localhost:9999"
+protectedRooms: []
# Set this option to true to protect every room the bot is joined to. Note that
# this effectively makes the protectedRooms and associated commands useless because
diff --git a/test/integration/fixtures.ts b/test/integration/fixtures.ts
new file mode 100644
index 0000000..9443ee8
--- /dev/null
+++ b/test/integration/fixtures.ts
@@ -0,0 +1,19 @@
+import { Mjolnir } from "../../src/Mjolnir";
+import { makeMjolnir } from "./mjolnirSetupUtils";
+
+export async function mochaGlobalSetup() {
+ console.log("Starting mjolnir.");
+ try {
+ this.bot = await makeMjolnir()
+ // do not block on this!
+ this.bot.start();
+ } catch (e) {
+ console.trace(e);
+ throw e;
+ }
+}
+
+export async function mochaGlobalTeardown() {
+ this.bot.stop();
+ console.log('stopping mjolnir');
+ }
\ No newline at end of file
diff --git a/test/integration/helloTest.ts b/test/integration/helloTest.ts
new file mode 100644
index 0000000..85f8c40
--- /dev/null
+++ b/test/integration/helloTest.ts
@@ -0,0 +1,32 @@
+import { doesNotMatch } from "assert";
+import { assert } from "console";
+import config from "../../src/config";
+import { newTestUser, noticeListener } from "./clientHelper"
+
+// need the start and newTestUser and then the stop call to be in setup and tear down.
+describe("help command", () => {
+ let client;
+ before(async function () {
+ client = await newTestUser(true);
+ await client.start();
+ })
+ it('Mjolnir responded to !mjolnir help', async function() {
+ this.timeout(30000);
+ // send a messgage
+ await client.joinRoom(config.managementRoom);
+ // listener for getting the event reply
+ let reply = new Promise((resolve, reject) => {
+ client.on('room.message', noticeListener(config.managementRoom, (event) => {
+ console.log(event.event_id)
+ if (event.content.body.includes("Print status information")) {
+ resolve(event);
+ }
+ }))});
+ // check we get one back
+ await client.sendMessage(config.managementRoom, {msgtype: "m.text", body: "!mjolnir help"})
+ await reply
+ })
+ after(async function () {
+ await client.stop();
+ })
+})
diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts
index 9e3d88e..a562ede 100644
--- a/test/integration/mjolnirSetupUtils.ts
+++ b/test/integration/mjolnirSetupUtils.ts
@@ -24,6 +24,8 @@ import * as HmacSHA1 from 'crypto-js/hmac-sha1';
import axios from 'axios';
import * as path from 'path';
import * as fs from 'fs/promises';
+import { setupMjolnir } from '../../src/setup';
+import { registerUser } from "./clientHelper";
export async function createManagementRoom(client: MatrixClient) {
let roomId = await client.createRoom();
@@ -52,39 +54,25 @@ export async function ensureLobbyRoomExists(client: MatrixClient): Promise {
+ await fs.copyFile(path.join(__dirname, 'config', 'harness.yaml'), path.join(__dirname, '../../config/harness.yaml'));
+ await registerUser('mjolnir', 'mjolnir', 'mjolnir', true).catch(e => {
if (e.isAxiosError && e.response.data.errcode === 'M_USER_IN_USE') {
- console.log(`${username} already registered, skipping`)
+ console.log('mjolnir already registered, skipping');
} else {
throw e;
}
});
}
+// it actually might make sense to give mjolnir a clean plate each time we setup and teardown a test.
+// the only issues with this might be e.g. if we need to delete a community or something
+// that mjolnir sets up each time, but tbh we should probably just avoid setting things like that and tearing it down.
+// One thing that probably should not be persisted between tests is the management room, subscribed lists and protected rooms.
+export async function makeMjolnir() {
+ await configureMjolnir();
+ console.info('starting mjolnir');
+ const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
+ const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
+ await ensureManagementRoomExists(client);
+ return await setupMjolnir(client, config);
+}
\ No newline at end of file