diff --git a/src/index.ts b/src/index.ts index ab8c379..987df08 100644 --- a/src/index.ts +++ b/src/index.ts @@ -41,7 +41,7 @@ if (config.health.healthz.enabled) { } (async function () { - const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath : path.join(__dirname, '../', config.dataPath) + const storagePath = path.isAbsolute(config.dataPath) ? config.dataPath : path.join(__dirname, '../', config.dataPath); const storage = new SimpleFsStorageProvider(path.join(storagePath, "bot.json")); let client: MatrixClient; diff --git a/src/setup.ts b/src/setup.ts index 8b29c7d..3485592 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -23,14 +23,14 @@ import { Mjolnir } from "./Mjolnir"; /** * Adds a listener to the client that will automatically accept invitations. - * @param {MatrixClient} client + * @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) { +function addJoinOnInviteListener(client: MatrixClient, options) { client.on("room.invite", async (roomId: string, inviteEvent: any) => { const membershipEvent = new MembershipEvent(inviteEvent); @@ -92,4 +92,4 @@ export async function setupMjolnir(client, config): Promise { 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 index ed36fe5..1a1d116 100644 --- a/test/integration/clientHelper.ts +++ b/test/integration/clientHelper.ts @@ -3,6 +3,16 @@ import { HmacSHA1 } from "crypto-js"; import { MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk"; import config from "../../src/config"; +/** + * Register a user using the synapse admin api that requires the use of a registration secret rather than an admin user. + * This should only be used by test code and should not be included from any file in the source directory + * either by explicit imports or copy pasting. + * @param username The username to give the user. + * @param displayname The displayname to give the user. + * @param password The password to use. + * @param admin True to make the user an admin, false otherwise. + * @returns The response from synapse. + */ 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); @@ -27,9 +37,9 @@ export async function registerNewTestUser(isAdmin: boolean) { let isUserValid = false; let username; do { - username = `test-user-${Math.floor(Math.random() * 100000)}` + username = `mjolnir-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') { + 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 @@ -42,16 +52,27 @@ export async function registerNewTestUser(isAdmin: boolean) { return username; } -export async function newTestUser(isAdmin?: boolean): Promise { +/** + * Registers a unique test user and returns a `MatrixClient` logged in and ready to use. + * @param isAdmin Whether to make the user an admin. + * @returns A new `MatrixClient` session for a unique test user. + */ +export async function newTestUser(isAdmin : boolean = false): Promise { const username = await registerNewTestUser(isAdmin); const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); return await pantalaimon.createClientWithCredentials(username, username); } +/** + * Utility to create an event listener for m.notice msgtype m.room.messages. + * @param targetRoomdId The roomId to listen into. + * @param cb The callback when a m.notice event is found in targetRoomId. + * @returns The callback to pass to `MatrixClient.on('room.message', cb)` + */ 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/integration/fixtures.ts b/test/integration/fixtures.ts index ecc74c0..5a85ebf 100644 --- a/test/integration/fixtures.ts +++ b/test/integration/fixtures.ts @@ -1,9 +1,11 @@ import config from "../../src/config"; -import { Mjolnir } from "../../src/Mjolnir"; import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils"; -// when mjolnir starts it clobbers the config, which is cached between runs, -// by resolving the alias and setting it to a roomid. +// When Mjolnir starts (src/index.ts) it clobbers the config by resolving the management room +// alias specified in the config (config.managementRoom) and overwriting that with the room ID. +// Unfortunately every piece of code importing that config imports the same instance, including +// testing code, which is problematic when we want to create a fresh management room for each test. +// So there is some code in here to "undo" the mutation after we stop Mjolnir syncing. export const mochaHooks = { beforeEach: [ async function() { @@ -16,12 +18,12 @@ export const mochaHooks = { async function() { console.log("stopping mjolnir"); await this.mjolnir.stop(); - // unclobber mjolnir's dirty work, i thought the config was being cached - // and was global, but that might have just been supersitiion, needs confirming. + // Mjolnir resolves config.managementRoom and overwrites it, so we undo this here + // after stopping Mjolnir for the next time we setup a Mjolnir and a management room. let managementRoomId = config.managementRoom; config.managementRoom = this.managementRoomAlias; // remove alias from management room and leave it. await teardownManagementRoom(this.mjolnir.client, managementRoomId, this.managementRoomAlias); } ] - }; \ No newline at end of file + }; diff --git a/test/integration/helloTest.ts b/test/integration/helloTest.ts index 8faaef2..9fa6d31 100644 --- a/test/integration/helloTest.ts +++ b/test/integration/helloTest.ts @@ -3,7 +3,6 @@ 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 () { diff --git a/test/integration/mjolnirSetupUtils.ts b/test/integration/mjolnirSetupUtils.ts index bdadf5d..eb3b818 100644 --- a/test/integration/mjolnirSetupUtils.ts +++ b/test/integration/mjolnirSetupUtils.ts @@ -27,27 +27,20 @@ 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(); - return await client.createRoomAlias(config.managementRoom, roomId); -} - -export async function ensureManagementRoomExists(client: MatrixClient): Promise { - return await client.resolveRoom(config.managementRoom).catch(async e => { - if (e?.body?.errcode === 'M_NOT_FOUND') { - console.info("moderation room hasn't been created yet, so we're making it now.") - return await createManagementRoom(client); - } - throw e; - }); -} - -export async function ensureLobbyRoomExists(client: MatrixClient): Promise { - const alias = Permalinks.parseUrl(config.protectedRooms[0]).roomIdOrAlias; - return await client.resolveRoom(alias).catch(async e => { +/** + * Ensures that a room exists with the alias, if it does not exist we create it. + * @param client The MatrixClient to use to resolve or create the aliased room. + * @param alias The alias of the room. + * @returns The room ID of the aliased room. + */ +export async function ensureAliasedRoomExists(client: MatrixClient, alias: string): Promise { + return await client.resolveRoom(alias) + .catch(async e => { if (e?.body?.errcode === 'M_NOT_FOUND') { console.info(`${alias} hasn't been created yet, so we're making it now.`) - return await client.createRoomAlias(alias, await client.createRoom()); + let roomId = await client.createRoom(); + await client.createRoomAlias(config.managementRoom, roomId); + return roomId } throw e; }); @@ -64,20 +57,23 @@ async function configureMjolnir() { }); } -// 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); + await ensureAliasedRoomExists(client, config.managementRoom); return await setupMjolnir(client, config); } +/** + * Remove the alias and leave the room, can't be implicitly provided from the config because Mjolnir currently mutates it. + * @param client The client to use to leave the room. + * @param roomId The roomId of the room to leave. + * @param alias The alias to remove from the room. + */ export async function teardownManagementRoom(client: MatrixClient, roomId: string, alias: string) { await client.deleteRoomAlias(alias); await client.leaveRoom(roomId); -} \ No newline at end of file +}