mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 05:36:06 +00:00
Rework integration tests to work with mx-tester
This commit is contained in:
parent
67e20e2415
commit
68aa717826
@ -12,6 +12,7 @@
|
|||||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||||
"start:dev": "yarn build && node lib/index.js",
|
"start:dev": "yarn build && node lib/index.js",
|
||||||
"test": "ts-mocha --project ./tsconfig.json test/**/*.ts",
|
"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"
|
"harness": "NODE_ENV=harness ts-node test/harness/launchScript.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -209,6 +209,10 @@ export class Mjolnir {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public stop() {
|
||||||
|
this.client.stop();
|
||||||
|
}
|
||||||
|
|
||||||
public async addProtectedRoom(roomId: string) {
|
public async addProtectedRoom(roomId: string) {
|
||||||
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
||||||
|
|
||||||
|
64
src/index.ts
64
src/index.ts
@ -20,17 +20,13 @@ import {
|
|||||||
LogService,
|
LogService,
|
||||||
MatrixClient,
|
MatrixClient,
|
||||||
PantalaimonClient,
|
PantalaimonClient,
|
||||||
Permalinks,
|
|
||||||
RichConsoleLogger,
|
RichConsoleLogger,
|
||||||
SimpleFsStorageProvider
|
SimpleFsStorageProvider
|
||||||
} from "matrix-bot-sdk";
|
} from "matrix-bot-sdk";
|
||||||
import config from "./config";
|
import config from "./config";
|
||||||
import BanList from "./models/BanList";
|
|
||||||
import { Mjolnir } from "./Mjolnir";
|
|
||||||
import { logMessage } from "./LogProxy";
|
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 { Healthz } from "./health/healthz";
|
||||||
|
import { setupMjolnir } from "./setup";
|
||||||
|
|
||||||
config.RUNTIME = {};
|
config.RUNTIME = {};
|
||||||
|
|
||||||
@ -58,63 +54,7 @@ if (config.health.healthz.enabled) {
|
|||||||
|
|
||||||
config.RUNTIME.client = client;
|
config.RUNTIME.client = client;
|
||||||
|
|
||||||
client.on("room.invite", async (roomId: string, inviteEvent: any) => {
|
let bot = await setupMjolnir(client, config);
|
||||||
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);
|
|
||||||
await bot.start();
|
await bot.start();
|
||||||
})().catch(err => {
|
})().catch(err => {
|
||||||
logMessage(LogLevel.ERROR, "index", err);
|
logMessage(LogLevel.ERROR, "index", err);
|
||||||
|
95
src/setup.ts
Normal file
95
src/setup.ts
Normal 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);
|
||||||
|
}
|
57
test/integration/clientHelper.ts
Normal file
57
test/integration/clientHelper.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
@ -73,8 +73,7 @@ automaticallyRedactForReasons:
|
|||||||
- "advertising"
|
- "advertising"
|
||||||
|
|
||||||
# A list of rooms to protect (matrix.to URLs)
|
# A list of rooms to protect (matrix.to URLs)
|
||||||
protectedRooms:
|
protectedRooms: []
|
||||||
- "https://matrix.to/#/#lobby:localhost:9999"
|
|
||||||
|
|
||||||
# Set this option to true to protect every room the bot is joined to. Note that
|
# 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
|
# this effectively makes the protectedRooms and associated commands useless because
|
19
test/integration/fixtures.ts
Normal file
19
test/integration/fixtures.ts
Normal 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');
|
||||||
|
}
|
32
test/integration/helloTest.ts
Normal file
32
test/integration/helloTest.ts
Normal 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();
|
||||||
|
})
|
||||||
|
})
|
@ -24,6 +24,8 @@ import * as HmacSHA1 from 'crypto-js/hmac-sha1';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
|
import { setupMjolnir } from '../../src/setup';
|
||||||
|
import { registerUser } from "./clientHelper";
|
||||||
|
|
||||||
export async function createManagementRoom(client: MatrixClient) {
|
export async function createManagementRoom(client: MatrixClient) {
|
||||||
let roomId = await client.createRoom();
|
let roomId = await client.createRoom();
|
||||||
@ -52,39 +54,25 @@ export async function ensureLobbyRoomExists(client: MatrixClient): Promise<strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function configureMjolnir() {
|
async function configureMjolnir() {
|
||||||
await fs.copyFile(path.join(__dirname, 'config', 'mjolnir', 'harness.yaml'), path.join(__dirname, '../../config/harness.yaml'));
|
await fs.copyFile(path.join(__dirname, 'config', '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).catch(e => {
|
||||||
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 => {
|
|
||||||
if (e.isAxiosError && e.response.data.errcode === 'M_USER_IN_USE') {
|
if (e.isAxiosError && e.response.data.errcode === 'M_USER_IN_USE') {
|
||||||
console.log(`${username} already registered, skipping`)
|
console.log('mjolnir already registered, skipping');
|
||||||
} else {
|
} else {
|
||||||
throw e;
|
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);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user