mirror of
https://github.com/matrix-org/mjolnir.git
synced 2024-10-01 01:36:06 -04:00
Merge pull request #136 from matrix-org/gnuxie/mx-tester
An Integration test setup that can be used with mx-tester
This commit is contained in:
commit
725d400650
52
README.md
52
README.md
@ -133,3 +133,55 @@ to restart Synapse to install the plugin.
|
||||
## Development
|
||||
|
||||
TODO. It's a TypeScript project with a linter.
|
||||
|
||||
### Development and testing with mx-tester
|
||||
|
||||
WARNING: mx-tester is currently work in progress, but it can still save you some time and is better than struggling with nothing.
|
||||
|
||||
If you have docker installed you can quickly get setup with a development environment by using
|
||||
[mx-tester](https://github.com/matrix-org/mx-tester).
|
||||
|
||||
To use mx-tester you will need to have rust installed. You can do that at [rustup](https://rustup.rs/) or [here](https://rust-lang.github.io/rustup/installation/other.html), you should probably also check your distro's documentation first to see if they have specific instructions for installing rust.
|
||||
|
||||
Once rust is installed you can install mx-tester like so.
|
||||
|
||||
```
|
||||
$ cargo install mx-tester
|
||||
```
|
||||
|
||||
Once you have mx-tester installed you we will want to build a synapse image with synapse_antispam from the mjolnir project root.
|
||||
|
||||
```
|
||||
$ mx-tester build
|
||||
```
|
||||
|
||||
Then we can start a container that uses that image and the config in `mx-tester.yml`.
|
||||
|
||||
```
|
||||
$ mx-tester up
|
||||
```
|
||||
|
||||
Once you have called `mx-tester up` you can run the integration tests.
|
||||
```
|
||||
$ yarn test:integration
|
||||
```
|
||||
|
||||
After calling `mx-tester up`, if we want to play with mojlnir locally we can run the following and then point a matrix client to http://localhost:9999.
|
||||
You should then be able to join the management room at `#moderators:localhost:9999`.
|
||||
|
||||
```
|
||||
yarn test:manual
|
||||
```
|
||||
|
||||
Once we are finished developing we can stop the synapse container.
|
||||
|
||||
```
|
||||
mx-tester down
|
||||
```
|
||||
|
||||
### Running integration tests
|
||||
|
||||
The integration tests can be run with `yarn test:integration`.
|
||||
The config that the tests use is in `config/harness.yaml`
|
||||
and by default this is configured to work with the server specified in `mx-tester.yml`,
|
||||
but you can configure it however you like to run against your own setup.
|
||||
|
161
config/harness.yaml
Normal file
161
config/harness.yaml
Normal file
@ -0,0 +1,161 @@
|
||||
# This configuration file is for the integration tests run by yarn:integration.
|
||||
# Editing this will do nothing and you shouldn't use it as a template.
|
||||
# For a template use default.yaml
|
||||
# Where the homeserver is located (client-server URL). This should point at
|
||||
# pantalaimon if you're using that.
|
||||
homeserverUrl: "http://localhost:9999"
|
||||
|
||||
# Pantalaimon options (https://github.com/matrix-org/pantalaimon)
|
||||
pantalaimon:
|
||||
# If true, accessToken above is ignored and the username/password below will be
|
||||
# used instead. The access token of the bot will be stored in the dataPath.
|
||||
use: true
|
||||
|
||||
# The username to login with.
|
||||
username: mjolnir
|
||||
|
||||
# The password to login with. Can be removed after the bot has logged in once and
|
||||
# stored the access token.
|
||||
password: mjolnir
|
||||
|
||||
# The directory the bot should store various bits of information in
|
||||
dataPath: "./test/harness/mjolnir-data/"
|
||||
|
||||
# If true (the default), only users in the `managementRoom` can invite the bot
|
||||
# to new rooms.
|
||||
autojoinOnlyIfManager: true
|
||||
|
||||
# If `autojoinOnlyIfManager` is false, only the members in this group can invite
|
||||
# the bot to new rooms.
|
||||
acceptInvitesFromGroup: '+example:example.org'
|
||||
|
||||
# If the bot is invited to a room and it won't accept the invite (due to the
|
||||
# conditions above), report it to the management room. Defaults to disabled (no
|
||||
# reporting).
|
||||
recordIgnoredInvites: false
|
||||
|
||||
# The room ID where people can use the bot. The bot has no access controls, so
|
||||
# anyone in this room can use the bot - secure your room!
|
||||
# This should be a room alias or room ID - not a matrix.to URL.
|
||||
# Note: Mjolnir is fairly verbose - expect a lot of messages from it.
|
||||
managementRoom: "#moderators:localhost:9999"
|
||||
|
||||
# Set to false to make the management room a bit quieter.
|
||||
verboseLogging: true
|
||||
|
||||
# The log level for the logs themselves. One of DEBUG, INFO, WARN, and ERROR.
|
||||
# This should be at INFO or DEBUG in order to get support for Mjolnir problems.
|
||||
logLevel: "DEBUG"
|
||||
|
||||
# Set to false to disable synchronizing the ban lists on startup. If true, this
|
||||
# is the same as running !mjolnir sync immediately after startup.
|
||||
syncOnStartup: true
|
||||
|
||||
# Set to false to prevent Mjolnir from checking its permissions on startup. This
|
||||
# is recommended to be left as "true" to catch room permission problems (state
|
||||
# resets, etc) before Mjolnir is needed.
|
||||
verifyPermissionsOnStartup: true
|
||||
|
||||
# If true, Mjolnir won't actually ban users or apply server ACLs, but will
|
||||
# think it has. This is useful to see what it does in a scenario where the
|
||||
# bot might not be trusted fully, yet. Default false (do bans/ACLs).
|
||||
noop: false
|
||||
|
||||
# Set to true to use /joined_members instead of /state to figure out who is
|
||||
# in the room. Using /state is preferred because it means that users are
|
||||
# banned when they are invited instead of just when they join, though if your
|
||||
# server struggles with /state requests then set this to true.
|
||||
fasterMembershipChecks: false
|
||||
|
||||
# A case-insensitive list of ban reasons to automatically redact a user's
|
||||
# messages for. Typically this is useful to avoid having to type two commands
|
||||
# to the bot. Use asterisks to represent globs (ie: "spam*testing" would match
|
||||
# "spam for testing" as well as "spamtesting").
|
||||
automaticallyRedactForReasons:
|
||||
- "spam"
|
||||
- "advertising"
|
||||
|
||||
# A list of rooms to protect (matrix.to URLs)
|
||||
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
|
||||
# the bot by nature must be joined to the room to protect it.
|
||||
#
|
||||
# Note: the management room is *excluded* from this condition. Add it to the
|
||||
# protected rooms to protect it.
|
||||
#
|
||||
# Note: ban list rooms the bot is watching but didn't create will not be protected.
|
||||
# Manually add these rooms to the protected rooms list if you want them protected.
|
||||
protectAllJoinedRooms: false
|
||||
|
||||
# Misc options for command handling and commands
|
||||
commands:
|
||||
# If true, Mjolnir will respond to commands like !help and !ban instead of
|
||||
# requiring a prefix. This is useful if Mjolnir is the only bot running in
|
||||
# your management room.
|
||||
#
|
||||
# Note that Mjolnir can be pinged by display name instead of having to use
|
||||
# the !mjolnir prefix. For example, "my_moderator_bot: ban @spammer:example.org"
|
||||
# will ban a user.
|
||||
allowNoPrefix: false
|
||||
|
||||
# In addition to the bot's display name, !mjolnir, and optionally no prefix
|
||||
# above, the bot will respond to these names. The items here can be used either
|
||||
# as display names or prefixed with exclamation points.
|
||||
additionalPrefixes:
|
||||
- "mjolnir_bot"
|
||||
|
||||
# If true, ban commands that use wildcard characters require confirmation with
|
||||
# an extra `--force` argument
|
||||
confirmWildcardBan: true
|
||||
|
||||
# Configuration specific to certain toggleable protections
|
||||
protections:
|
||||
# Configuration for the wordlist plugin, which can ban users based if they say certain
|
||||
# blocked words shortly after joining.
|
||||
wordlist:
|
||||
# A list of words which should be monitored by the bot. These will match if any part
|
||||
# of the word is present in the message in any case. e.g. "hello" also matches
|
||||
# "HEllO". Additionally, regular expressions can be used.
|
||||
words:
|
||||
- "CaSe"
|
||||
- "InSeNsAtIve"
|
||||
- "WoRd"
|
||||
- "LiSt"
|
||||
|
||||
# How long after a user joins the server should the bot monitor their messages. After
|
||||
# this time, users can say words from the wordlist without being banned automatically.
|
||||
# Set to zero to disable (users will always be banned if they say a bad word)
|
||||
minutesBeforeTrusting: 20
|
||||
|
||||
# Options for monitoring the health of the bot
|
||||
health:
|
||||
# healthz options. These options are best for use in container environments
|
||||
# like Kubernetes to detect how healthy the service is. The bot will report
|
||||
# that it is unhealthy until it is able to process user requests. Typically
|
||||
# this means that it'll flag itself as unhealthy for a number of minutes
|
||||
# before saying "Now monitoring rooms" and flagging itself healthy.
|
||||
#
|
||||
# Health is flagged through HTTP status codes, defined below.
|
||||
healthz:
|
||||
# Whether the healthz integration should be enabled (default false)
|
||||
enabled: false
|
||||
|
||||
# The port to expose the webserver on. Defaults to 8080.
|
||||
port: 8080
|
||||
|
||||
# The address to listen for requests on. Defaults to all addresses.
|
||||
address: "0.0.0.0"
|
||||
|
||||
# The path to expose the monitoring endpoint at. Defaults to `/healthz`
|
||||
endpoint: "/healthz"
|
||||
|
||||
# The HTTP status code which reports that the bot is healthy/ready to
|
||||
# process requests. Typically this should not be changed. Defaults to
|
||||
# 200.
|
||||
healthyStatus: 200
|
||||
|
||||
# The HTTP status code which reports that the bot is not healthy/ready.
|
||||
# Defaults to 418.
|
||||
unhealthyStatus: 418
|
63
mx-tester.yml
Normal file
63
mx-tester.yml
Normal file
@ -0,0 +1,63 @@
|
||||
name: Mjolnir Testing
|
||||
modules:
|
||||
- name: synapse_antispam
|
||||
build:
|
||||
- cp -r synapse_antispam $MX_TEST_SYNAPSE_DIR
|
||||
run:
|
||||
- yarn test-integration
|
||||
homeserver_config:
|
||||
server_name: localhost:9999
|
||||
pid_file: /data/homeserver.pid
|
||||
public_baseurl: http://localhost:9999
|
||||
listeners:
|
||||
- port: 9999
|
||||
tls: false
|
||||
type: http
|
||||
x_forwarded: true
|
||||
resources:
|
||||
- names: [client, federation]
|
||||
compress: false
|
||||
database:
|
||||
name: sqlite3
|
||||
args:
|
||||
database: /data/homeserver.db
|
||||
media_store_path: "/data/media_store"
|
||||
enable_registration: true
|
||||
report_stats: false
|
||||
registration_shared_secret: "REGISTRATION_SHARED_SECRET"
|
||||
macaroon_secret_key: "MACROON_SECRET_KEY"
|
||||
signing_key_path: "/data/localhost:9999.signing.key"
|
||||
trusted_key_servers:
|
||||
- server_name: "matrix.org"
|
||||
suppress_key_server_warning: true
|
||||
|
||||
rc_message:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_registration:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_login:
|
||||
address:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
account:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
failed_attempts:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_admin_redaction:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
|
||||
rc_joins:
|
||||
local:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
||||
remote:
|
||||
per_second: 10000
|
||||
burst_count: 10000
|
11
package.json
11
package.json
@ -11,14 +11,21 @@
|
||||
"build": "tsc",
|
||||
"lint": "tslint --project ./tsconfig.json -t stylish",
|
||||
"start:dev": "yarn build && node lib/index.js",
|
||||
"test": "ts-mocha --project ./tsconfig.json test/**/*.ts"
|
||||
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.ts",
|
||||
"test:integration": "NODE_ENV=harness ts-mocha --require test/integration/fixtures.ts --project ./tsconfig.json test/integration/**/*Test.ts",
|
||||
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/crypto-js": "^4.0.2",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "11",
|
||||
"@types/node": "^16.7.10",
|
||||
"axios": "^0.21.4",
|
||||
"crypto-js": "^4.1.1",
|
||||
"expect": "^27.0.6",
|
||||
"mocha": "^9.0.1",
|
||||
"ts-mocha": "^8.0.0",
|
||||
"ts-node": "^10.2.1",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "^4.3.5"
|
||||
},
|
||||
|
@ -21,6 +21,7 @@ import {
|
||||
LogService,
|
||||
MatrixClient,
|
||||
MatrixGlob,
|
||||
MembershipEvent,
|
||||
Permalinks,
|
||||
UserID
|
||||
} from "matrix-bot-sdk";
|
||||
@ -37,6 +38,7 @@ import { PROTECTIONS } from "./protections/protections";
|
||||
import { UnlistedUserRedactionQueue } from "./queues/UnlistedUserRedactionQueue";
|
||||
import { Healthz } from "./health/healthz";
|
||||
import { EventRedactionQueue, RedactUserInRoom } from "./queues/EventRedactionQueue";
|
||||
import * as htmlEscape from "escape-html";
|
||||
|
||||
export const STATE_NOT_STARTED = "not_started";
|
||||
export const STATE_CHECKING_PERMISSIONS = "checking_permissions";
|
||||
@ -69,6 +71,84 @@ export class Mjolnir {
|
||||
private explicitlyProtectedRoomIds: string[] = [];
|
||||
private knownUnprotectedRooms: string[] = [];
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private static 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);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Mjolnir instance from a client and the options in the configuration file, ready to be started.
|
||||
* @param {MatrixClient} client The client for Mjolnir to use.
|
||||
* @returns A new Mjolnir instance that can be started without further setup.
|
||||
*/
|
||||
static async setupMjolnirFromConfig(client: MatrixClient): Promise<Mjolnir> {
|
||||
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);
|
||||
}
|
||||
|
||||
constructor(
|
||||
public readonly client: MatrixClient,
|
||||
public readonly protectedRooms: { [roomId: string]: string },
|
||||
@ -209,6 +289,14 @@ export class Mjolnir {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop Mjolnir from syncing and processing commands.
|
||||
*/
|
||||
public stop() {
|
||||
LogService.info("Mjolnir", "Stopping Mjolnir...");
|
||||
this.client.stop();
|
||||
}
|
||||
|
||||
public async addProtectedRoom(roomId: string) {
|
||||
this.protectedRooms[roomId] = Permalinks.forRoom(roomId);
|
||||
|
||||
|
69
src/index.ts
69
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 { Mjolnir } from "./Mjolnir";
|
||||
|
||||
config.RUNTIME = {};
|
||||
|
||||
@ -45,7 +41,8 @@ if (config.health.healthz.enabled) {
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const storage = new SimpleFsStorageProvider(path.join(config.dataPath, "bot.json"));
|
||||
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;
|
||||
if (config.pantalaimon.use) {
|
||||
@ -57,65 +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 Mjolnir.setupMjolnirFromConfig(client);
|
||||
await bot.start();
|
||||
})().catch(err => {
|
||||
logMessage(LogLevel.ERROR, "index", err);
|
||||
|
77
test/integration/clientHelper.ts
Normal file
77
test/integration/clientHelper.ts
Normal file
@ -0,0 +1,77 @@
|
||||
import axios from "axios";
|
||||
import { HmacSHA1 } from "crypto-js";
|
||||
import { LogService, 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);
|
||||
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 = `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') {
|
||||
LogService.debug("test/clientHelper", `${username} already registered, trying another`);
|
||||
false // continue and try again
|
||||
} else {
|
||||
console.error(`failed to register user ${e}`);
|
||||
throw e;
|
||||
}
|
||||
})
|
||||
} while (!isUserValid);
|
||||
return username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<MatrixClient> {
|
||||
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);
|
||||
}
|
||||
}
|
28
test/integration/fixtures.ts
Normal file
28
test/integration/fixtures.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import config from "../../src/config";
|
||||
import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils";
|
||||
|
||||
// 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() {
|
||||
this.managementRoomAlias = config.managementRoom
|
||||
this.mjolnir = await makeMjolnir()
|
||||
this.mjolnir.start()
|
||||
}
|
||||
],
|
||||
afterEach: [
|
||||
async function() {
|
||||
await this.mjolnir.stop();
|
||||
// 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);
|
||||
}
|
||||
]
|
||||
};
|
30
test/integration/helloTest.ts
Normal file
30
test/integration/helloTest.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import config from "../../src/config";
|
||||
import { newTestUser, noticeListener } from "./clientHelper"
|
||||
|
||||
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);
|
||||
console.log(`management room ${config.managementRoom}`);
|
||||
// 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) => {
|
||||
if (event.content.body.includes("Print status information")) {
|
||||
resolve(event);
|
||||
}
|
||||
}))});
|
||||
// check we get one back
|
||||
console.log(config);
|
||||
await client.sendMessage(config.managementRoom, {msgtype: "m.text", body: "!mjolnir help"})
|
||||
await reply
|
||||
})
|
||||
after(async function () {
|
||||
await client.stop();
|
||||
})
|
||||
})
|
10
test/integration/manualLaunchScript.ts
Normal file
10
test/integration/manualLaunchScript.ts
Normal file
@ -0,0 +1,10 @@
|
||||
/**
|
||||
* This file is used to launch mjolnir for manual testing, creating a user and management room automatically if it doesn't already exist.
|
||||
*/
|
||||
|
||||
import { makeMjolnir } from "./mjolnirSetupUtils";
|
||||
|
||||
(async () => {
|
||||
let mjolnir = await makeMjolnir();
|
||||
await mjolnir.start()
|
||||
})();
|
78
test/integration/mjolnirSetupUtils.ts
Normal file
78
test/integration/mjolnirSetupUtils.ts
Normal file
@ -0,0 +1,78 @@
|
||||
/*
|
||||
Copyright 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 {
|
||||
MatrixClient,
|
||||
PantalaimonClient,
|
||||
MemoryStorageProvider,
|
||||
LogService,
|
||||
LogLevel,
|
||||
RichConsoleLogger
|
||||
} from "matrix-bot-sdk";
|
||||
import { Mjolnir} from '../../src/Mjolnir';
|
||||
import config from "../../src/config";
|
||||
import { registerUser } from "./clientHelper";
|
||||
|
||||
/**
|
||||
* 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<string> {
|
||||
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.`)
|
||||
let roomId = await client.createRoom();
|
||||
await client.createRoomAlias(config.managementRoom, roomId);
|
||||
return roomId
|
||||
}
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async function configureMjolnir() {
|
||||
await registerUser('mjolnir', 'mjolnir', 'mjolnir', true).catch(e => {
|
||||
if (e.isAxiosError && e.response.data.errcode === 'M_USER_IN_USE') {
|
||||
console.log('mjolnir already registered, skipping');
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export async function makeMjolnir() {
|
||||
await configureMjolnir();
|
||||
LogService.setLogger(new RichConsoleLogger());
|
||||
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
|
||||
LogService.info("test/mjolnirSetupUtils", "Starting bot...");
|
||||
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
|
||||
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
|
||||
await ensureAliasedRoomExists(client, config.managementRoom);
|
||||
return await Mjolnir.setupMjolnirFromConfig(client);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
Loading…
Reference in New Issue
Block a user