Rework integration tests to work with mx-tester

This commit is contained in:
gnuxie 2021-09-22 17:59:11 +01:00
parent 67e20e2415
commit 68aa717826
9 changed files with 228 additions and 93 deletions

View File

@ -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": {

View File

@ -209,6 +209,10 @@ export class Mjolnir {
});
}
public stop() {
this.client.stop();
}
public async addProtectedRoom(roomId: string) {
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);

View File

@ -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 <code>!mjolnir rooms add ${htmlEscape(roomId)}</code> `
+ `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);

95
src/setup.ts Normal file
View File

@ -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 <code>!mjolnir rooms add ${htmlEscape(roomId)}</code> `
+ `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<Mjolnir> {
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);
}

View File

@ -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<MatrixClient> {
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);
}
}

View File

@ -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

View File

@ -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');
}

View File

@ -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();
})
})

View File

@ -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<strin
}
async function configureMjolnir() {
await fs.copyFile(path.join(__dirname, 'config', 'mjolnir', 'harness.yaml'), path.join(__dirname, '../../config/harness.yaml'));
await fs.rm(path.join(__dirname, 'mjolnir-data'), {recursive: true, force: true});
await registerUser('mjolnir', 'mjolnir', 'mjolnir', true);
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
await ensureManagementRoomExists(client);
return await ensureLobbyRoomExists(client);
}
async function startMjolnir() {
await configureMjolnir();
console.info('starting mjolnir');
await import('../../src/index');
}
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()
}).catch(e => {
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);
}