Let's make sure that we can still check for errors with concise error handling

This commit is contained in:
David Teller 2022-01-06 12:19:19 +01:00 committed by David Teller
parent 3f2039f6a7
commit c48a1e8ffc
9 changed files with 59 additions and 24 deletions

View File

@ -10,9 +10,9 @@
"scripts": { "scripts": {
"build": "tsc", "build": "tsc",
"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 --async-stack-traces lib/index.js",
"test": "ts-mocha --project ./tsconfig.json test/commands/**/*.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:integration": "NODE_ENV=harness ts-mocha --async-stack-traces --require test/integration/fixtures.ts --project ./tsconfig.json \"test/integration/**/*Test.ts\"",
"test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts" "test:manual": "NODE_ENV=harness ts-node test/integration/manualLaunchScript.ts"
}, },
"devDependencies": { "devDependencies": {

View File

@ -27,7 +27,7 @@ import config from "./config";
import { logMessage } from "./LogProxy"; import { logMessage } from "./LogProxy";
import { Healthz } from "./health/healthz"; import { Healthz } from "./health/healthz";
import { Mjolnir } from "./Mjolnir"; import { Mjolnir } from "./Mjolnir";
import { patchMatrixClientForConciseExceptions } from "./utils"; import { patchMatrixClient } from "./utils";
config.RUNTIME = {}; config.RUNTIME = {};
@ -52,7 +52,7 @@ if (config.health.healthz.enabled) {
} else { } else {
client = new MatrixClient(config.homeserverUrl, config.accessToken, storage); client = new MatrixClient(config.homeserverUrl, config.accessToken, storage);
} }
patchMatrixClientForConciseExceptions(); patchMatrixClient();
config.RUNTIME.client = client; config.RUNTIME.client = client;
let bot = await Mjolnir.setupMjolnirFromConfig(client); let bot = await Mjolnir.setupMjolnirFromConfig(client);

View File

@ -22,7 +22,6 @@ import { JSDOM } from 'jsdom';
import config from "../config"; import config from "../config";
import { Mjolnir } from "../Mjolnir"; import { Mjolnir } from "../Mjolnir";
import { patchMatrixClientForConciseExceptions } from "../utils";
/// Regexp, used to extract the action label from an action reaction /// Regexp, used to extract the action label from an action reaction
/// such as `⚽ Kick user @foobar:localhost from room [kick-user]`. /// such as `⚽ Kick user @foobar:localhost from room [kick-user]`.
@ -876,7 +875,6 @@ class DisplayManager {
if (!await action.canExecute(this.owner, report, moderationRoomId)) { if (!await action.canExecute(this.owner, report, moderationRoomId)) {
continue; continue;
} }
patchMatrixClientForConciseExceptions();
await this.owner.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", { await this.owner.mjolnir.client.sendEvent(config.managementRoom, "m.reaction", {
"m.relates_to": { "m.relates_to": {
"rel_type": "m.annotation", "rel_type": "m.annotation",

View File

@ -220,14 +220,15 @@ let isMatrixClientPatchedForConciseExceptions = false;
* This method configures `MatrixClient` to ensure that methods that may throw * This method configures `MatrixClient` to ensure that methods that may throw
* instead throws more reasonable insetances of `Error`. * instead throws more reasonable insetances of `Error`.
*/ */
export function patchMatrixClientForConciseExceptions() { function patchMatrixClientForConciseExceptions() {
if (isMatrixClientPatchedForConciseExceptions) { if (isMatrixClientPatchedForConciseExceptions) {
return; return;
} }
let originalRequestFn = getRequestFn(); let originalRequestFn = getRequestFn();
setRequestFn((params, cb) => { setRequestFn((params, cb) => {
let stack = new Error().stack;
originalRequestFn(params, function conciseExceptionRequestFn(err, response, resBody) { originalRequestFn(params, function conciseExceptionRequestFn(err, response, resBody) {
if (!err && (response.statusCode < 200 || response.statusCode >= 300)) { if (!err && (response?.statusCode < 200 || response?.statusCode >= 300)) {
// Normally, converting HTTP Errors into rejections is done by the caller // Normally, converting HTTP Errors into rejections is done by the caller
// of `requestFn` within matrix-bot-sdk. However, this always ends up rejecting // of `requestFn` within matrix-bot-sdk. However, this always ends up rejecting
// with an `IncomingMessage` - exactly what we wish to avoid here. // with an `IncomingMessage` - exactly what we wish to avoid here.
@ -277,12 +278,47 @@ export function patchMatrixClientForConciseExceptions() {
} }
} }
if ("body" in err) { if ("body" in err) {
body = JSON.stringify((err as any).body); body = (err as any).body;
} }
let error = new Error(`Error during MatrixClient request ${method} ${path}: ${err.statusCode} ${err.statusMessage} -- ${body}`); let error = new Error(`Error during MatrixClient request ${method} ${path}: ${err.statusCode} ${err.statusMessage} -- ${body}`);
error.stack = stack;
if (body) {
// Calling code may use `body` to check for errors, so let's
// make sure that we're providing it.
try {
body = JSON.parse(body);
} catch (ex) {
// Not JSON.
}
// Define the property but don't make it visible during logging.
Object.defineProperty(error, "body", {
value: body,
enumerable: false,
});
}
// Calling code may use `statusCode` to check for errors, so let's
// make sure that we're providing it.
if ("statusCode" in err) {
Object.defineProperty(error, "statusCode", {
value: err.statusCode,
enumerable: false,
});
}
return cb(error, response, resBody); return cb(error, response, resBody);
}) })
}); });
isMatrixClientPatchedForConciseExceptions = true; isMatrixClientPatchedForConciseExceptions = true;
} }
/**
* Perform any patching deemed necessary to MatrixClient.
*/
export function patchMatrixClient() {
// Note that the order of patches is meaningful.
//
// - `patchMatrixClientForConciseExceptions` converts all `IncomingMessage`
// errors into instances of `Error` handled as errors;
// - `patchMatrixClientForRetry` expects that all errors are returned as
// errors.
patchMatrixClientForConciseExceptions();
}

View File

@ -21,7 +21,7 @@ const REPORT_NOTICE_REGEXPS = {
describe("Test: Reporting abuse", async () => { describe("Test: Reporting abuse", async () => {
it('Mjölnir intercepts abuse reports', async function() { it('Mjölnir intercepts abuse reports', async function() {
this.timeout(10000); this.timeout(60000);
// Listen for any notices that show up. // Listen for any notices that show up.
let notices = []; let notices = [];
@ -216,7 +216,7 @@ describe("Test: Reporting abuse", async () => {
} }
}); });
it('The redact action works', async function() { it('The redact action works', async function() {
this.timeout(10000); this.timeout(60000);
// Listen for any notices that show up. // Listen for any notices that show up.
let notices = []; let notices = [];

View File

@ -1,6 +1,6 @@
import axios from "axios"; import axios from "axios";
import { HmacSHA1 } from "crypto-js"; import { HmacSHA1 } from "crypto-js";
import { extractRequestError, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk"; import { LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "matrix-bot-sdk";
import config from "../../src/config"; import config from "../../src/config";
/** /**

View File

@ -8,7 +8,7 @@ import { onReactionTo } from "./commandUtils";
describe("Test: The redaction command", function () { describe("Test: The redaction command", function () {
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id and a room id.', async function() { it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id and a room id.', async function() {
this.timeout(20000); this.timeout(60000);
// Create a few users and a room. // Create a few users and a room.
let badUser = await newTestUser(false, "spammer-needs-redacting"); let badUser = await newTestUser(false, "spammer-needs-redacting");
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
@ -52,7 +52,7 @@ import { onReactionTo } from "./commandUtils";
}); });
}) })
it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id in multiple rooms.', async function() { it('Mjölnir redacts all of the events sent by a spammer when instructed to by giving their id in multiple rooms.', async function() {
this.timeout(20000); this.timeout(60000);
// Create a few users and a room. // Create a few users and a room.
let badUser = await newTestUser(false, "spammer-needs-redacting"); let badUser = await newTestUser(false, "spammer-needs-redacting");
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
@ -101,7 +101,7 @@ import { onReactionTo } from "./commandUtils";
}); });
}); });
it("Redacts a single event when instructed to.", async function () { it("Redacts a single event when instructed to.", async function () {
this.timeout(20000); this.timeout(60000);
// Create a few users and a room. // Create a few users and a room.
let badUser = await newTestUser(false, "spammer-needs-redacting"); let badUser = await newTestUser(false, "spammer-needs-redacting");
const mjolnir = config.RUNTIME.client! const mjolnir = config.RUNTIME.client!

View File

@ -24,7 +24,7 @@ import {
import { Mjolnir} from '../../src/Mjolnir'; import { Mjolnir} from '../../src/Mjolnir';
import config from "../../src/config"; import config from "../../src/config";
import { registerUser } from "./clientHelper"; import { registerUser } from "./clientHelper";
import { patchMatrixClientForConciseExceptions } from "../../src/utils"; import { patchMatrixClient } from "../../src/utils";
/** /**
* Ensures that a room exists with the alias, if it does not exist we create it. * Ensures that a room exists with the alias, if it does not exist we create it.
@ -33,8 +33,9 @@ import { patchMatrixClientForConciseExceptions } from "../../src/utils";
* @returns The room ID of the aliased room. * @returns The room ID of the aliased room.
*/ */
export async function ensureAliasedRoomExists(client: MatrixClient, alias: string): Promise<string> { export async function ensureAliasedRoomExists(client: MatrixClient, alias: string): Promise<string> {
return await client.resolveRoom(alias) try {
.catch(async e => { return await client.resolveRoom(alias);
} catch (e) {
if (e?.body?.errcode === 'M_NOT_FOUND') { if (e?.body?.errcode === 'M_NOT_FOUND') {
console.info(`${alias} hasn't been created yet, so we're making it now.`) console.info(`${alias} hasn't been created yet, so we're making it now.`)
let roomId = await client.createRoom({ let roomId = await client.createRoom({
@ -44,7 +45,7 @@ export async function ensureAliasedRoomExists(client: MatrixClient, alias: strin
return roomId return roomId
} }
throw e; throw e;
}); }
} }
async function configureMjolnir() { async function configureMjolnir() {
@ -81,7 +82,7 @@ export async function makeMjolnir(): Promise<Mjolnir> {
LogService.info("test/mjolnirSetupUtils", "Starting bot..."); LogService.info("test/mjolnirSetupUtils", "Starting bot...");
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider());
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password);
patchMatrixClientForConciseExceptions(); patchMatrixClient();
await ensureAliasedRoomExists(client, config.managementRoom); await ensureAliasedRoomExists(client, config.managementRoom);
let mjolnir = await Mjolnir.setupMjolnirFromConfig(client); let mjolnir = await Mjolnir.setupMjolnirFromConfig(client);
globalClient = client; globalClient = client;

View File

@ -8,7 +8,7 @@ import { getMessagesByUserIn } from "../../src/utils";
*/ */
describe("Test: timeline pagination", function () { describe("Test: timeline pagination", function () {
it('does not paginate across the entire room history while backfilling.', async function() { it('does not paginate across the entire room history while backfilling.', async function() {
this.timeout(20000); this.timeout(60000);
// Create a few users and a room. // Create a few users and a room.
let badUser = await newTestUser(false, "spammer"); let badUser = await newTestUser(false, "spammer");
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
@ -38,7 +38,7 @@ describe("Test: timeline pagination", function () {
assert.equal(eventCount, 7, "There shouldn't be any more events (1 member event and 6 messages), and they should all be from the same account."); assert.equal(eventCount, 7, "There shouldn't be any more events (1 member event and 6 messages), and they should all be from the same account.");
}) })
it('does not call the callback with an empty array when there are no relevant events', async function() { it('does not call the callback with an empty array when there are no relevant events', async function() {
this.timeout(20000); this.timeout(60000);
let badUser = await newTestUser(false, "spammer"); let badUser = await newTestUser(false, "spammer");
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
let moderator = await newTestUser(false, "moderator"); let moderator = await newTestUser(false, "moderator");
@ -53,7 +53,7 @@ describe("Test: timeline pagination", function () {
assert.equal(cbCount, 0, "The callback should never get called"); assert.equal(cbCount, 0, "The callback should never get called");
}) })
it("The limit provided is respected", async function() { it("The limit provided is respected", async function() {
this.timeout(20000); this.timeout(60000);
let badUser = await newTestUser(false, "spammer"); let badUser = await newTestUser(false, "spammer");
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
let moderator = await newTestUser(false, "moderator"); let moderator = await newTestUser(false, "moderator");
@ -82,7 +82,7 @@ describe("Test: timeline pagination", function () {
assert.equal(cbCount, 1, "The callback should be called once with events matching the glob."); assert.equal(cbCount, 1, "The callback should be called once with events matching the glob.");
}); });
it("Gives the events to the callback ordered by youngest first (even more important when the limit is reached halfway through a chunk).", async function() { it("Gives the events to the callback ordered by youngest first (even more important when the limit is reached halfway through a chunk).", async function() {
this.timeout(20000); this.timeout(60000);
let moderator = await newTestUser(false, "moderator"); let moderator = await newTestUser(false, "moderator");
let moderatorId = await moderator.getUserId(); let moderatorId = await moderator.getUserId();
let targetRoom = await moderator.createRoom(); let targetRoom = await moderator.createRoom();