update tests to remove use of panataliamon

This commit is contained in:
H. Shay 2024-09-30 13:04:44 -07:00
parent c9a757f6e2
commit e640fc968b
8 changed files with 143 additions and 82 deletions

View File

@ -16,7 +16,7 @@ rawHomeserverUrl: "http://localhost:8081"
pantalaimon: pantalaimon:
# If true, accessToken above is ignored and the username/password below will be # 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. # used instead. The access token of the bot will be stored in the dataPath.
use: true use: false
# The username to login with. # The username to login with.
username: mjolnir username: mjolnir
@ -25,6 +25,11 @@ pantalaimon:
# stored the access token. # stored the access token.
password: mjolnir password: mjolnir
encryption:
use: true
username: test
password: testPassword
# The directory the bot should store various bits of information in # The directory the bot should store various bits of information in
dataPath: "./test/harness/mjolnir-data/" dataPath: "./test/harness/mjolnir-data/"

View File

@ -44,6 +44,7 @@
"typescript-formatter": "^7.2" "typescript-formatter": "^7.2"
}, },
"dependencies": { "dependencies": {
"axios": "^1.7.6",
"@sentry/node": "^7.17.2", "@sentry/node": "^7.17.2",
"@sentry/tracing": "^7.17.2", "@sentry/tracing": "^7.17.2",
"@tensorflow/tfjs-node": "^4.21.0", "@tensorflow/tfjs-node": "^4.21.0",

View File

@ -1,8 +1,7 @@
import { strict as assert } from "assert"; import { strict as assert } from "assert";
import { matrixClient } from "./mjolnirSetupUtils";
import { newTestUser } from "./clientHelper"; import { newTestUser } from "./clientHelper";
import { ReportManager, ABUSE_ACTION_CONFIRMATION_KEY, ABUSE_REPORT_KEY } from "../../src/report/ReportManager"; import { ABUSE_REPORT_KEY } from "../../src/report/ReportManager";
/** /**
* Test the ability to turn abuse reports into room messages. * Test the ability to turn abuse reports into room messages.
@ -30,8 +29,9 @@ describe("Test: Reporting abuse", async () => {
this.timeout(90000); this.timeout(90000);
// Listen for any notices that show up. // Listen for any notices that show up.
await new Promise(resolve => setTimeout(resolve, 1000));
let notices: any[] = []; let notices: any[] = [];
this.mjolnir.client.on("room.event", (roomId, event) => { this.mjolnir.client.on("room.event", (roomId: string, event: any) => {
if (roomId = this.mjolnir.managementRoomId) { if (roomId = this.mjolnir.managementRoomId) {
notices.push(event); notices.push(event);
} }
@ -225,8 +225,9 @@ describe("Test: Reporting abuse", async () => {
this.timeout(60000); this.timeout(60000);
// Listen for any notices that show up. // Listen for any notices that show up.
await new Promise(resolve => setTimeout(resolve, 1000));
let notices: any[] = []; let notices: any[] = [];
this.mjolnir.client.on("room.event", (roomId, event) => { this.mjolnir.client.on("room.event", (roomId: string, event: any) => {
if (roomId = this.mjolnir.managementRoomId) { if (roomId = this.mjolnir.managementRoomId) {
notices.push(event); notices.push(event);
} }
@ -244,8 +245,8 @@ describe("Test: Reporting abuse", async () => {
let badUserId = await badUser.getUserId(); let badUserId = await badUser.getUserId();
let roomId = await moderatorUser.createRoom({ invite: [await badUser.getUserId()] }); let roomId = await moderatorUser.createRoom({ invite: [await badUser.getUserId()] });
await moderatorUser.inviteUser(await goodUser.getUserId(), roomId); await moderatorUser.inviteUser(goodUserId, roomId);
await moderatorUser.inviteUser(await badUser.getUserId(), roomId); await moderatorUser.inviteUser(badUserId, roomId);
await badUser.joinRoom(roomId); await badUser.joinRoom(roomId);
await goodUser.joinRoom(roomId); await goodUser.joinRoom(roomId);
@ -257,9 +258,9 @@ describe("Test: Reporting abuse", async () => {
// Exchange a few messages. // Exchange a few messages.
let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported. let goodText = `GOOD: ${Math.random()}`; // Will NOT be reported.
let badText = `BAD: ${Math.random()}`; // Will be reported as abuse. let badText = `BAD: ${Math.random()}`; // Will be reported as abuse.
let goodEventId = await goodUser.sendText(roomId, goodText); await goodUser.sendText(roomId, goodText);
let badEventId = await badUser.sendText(roomId, badText); let badEventId = await badUser.sendText(roomId, badText);
let goodEventId2 = await goodUser.sendText(roomId, goodText); await goodUser.sendText(roomId, goodText);
console.log("Test: Reporting abuse - send reports"); console.log("Test: Reporting abuse - send reports");

View File

@ -1,36 +1,40 @@
import { HmacSHA1 } from "crypto-js"; import { HmacSHA1 } from "crypto-js";
import { getRequestFn, LogService, MatrixClient, MemoryStorageProvider, PantalaimonClient } from "@vector-im/matrix-bot-sdk"; import {
MatrixClient,
MemoryStorageProvider,
RustSdkCryptoStorageProvider
} from "@vector-im/matrix-bot-sdk";
import { PathLike, promises as fs} from "fs";
import axios from "axios";
const REGISTRATION_ATTEMPTS = 10; const REGISTRATION_ATTEMPTS = 10;
const REGISTRATION_RETRY_BASE_DELAY_MS = 100; const REGISTRATION_RETRY_BASE_DELAY_MS = 100;
let CryptoStorePaths: any = [];
/** /**
* Register a user using the synapse admin api that requires the use of a registration secret rather than an admin user. * 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 * 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. * either by explicit imports or copy pasting.
* *
* @param homeserver the homeserver url
* @param username The username to give the user. * @param username The username to give the user.
* @param displayname The displayname to give the user. * @param displayname The displayname to give the user.
* @param password The password to use. * @param password The password to use.
* @param admin True to make the user an admin, false otherwise. * @param admin True to make the user an admin, false otherwise.
* @returns The response from synapse. * @returns The access token from logging in.
*/ */
export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise<void> { export async function registerUser(homeserver: string, username: string, displayname: string, password: string, admin: boolean): Promise<string> {
let registerUrl = `${homeserver}/_synapse/admin/v1/register` let registerUrl = `${homeserver}/_synapse/admin/v1/register`
const data: {nonce: string} = await new Promise((resolve, reject) => { const response = await axios({method: 'get', url: registerUrl, timeout: 60000})
getRequestFn()({uri: registerUrl, method: "GET", timeout: 60000}, (error: any, response: any, resBody: any) => { const nonce = response.data.nonce
error ? reject(error) : resolve(JSON.parse(resBody))
});
});
const nonce = data.nonce!;
let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET'); let mac = HmacSHA1(`${nonce}\0${username}\0${password}\0${admin ? 'admin' : 'notadmin'}`, 'REGISTRATION_SHARED_SECRET');
for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) { for (let i = 1; i <= REGISTRATION_ATTEMPTS; ++i) {
try { const registerConfig = {
const params = { url: registerUrl,
uri: registerUrl, method: "post",
method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify({ data: JSON.stringify({
nonce, nonce,
username, username,
displayname, displayname,
@ -40,16 +44,37 @@ export async function registerUser(homeserver: string, username: string, display
}), }),
timeout: 60000 timeout: 60000
} }
return await new Promise((resolve, reject) => { try {
getRequestFn()(params, (error: any) => error ? reject(error) : resolve()); let resp = await axios(registerConfig)
}); return resp.data?.access_token
} catch (ex) { } catch (ex) {
const code = ex.response.data.errcode
// In case of timeout or throttling, backoff and retry. // In case of timeout or throttling, backoff and retry.
if (ex?.code === 'ESOCKETTIMEDOUT' || ex?.code === 'ETIMEDOUT' if (code === 'ESOCKETTIMEDOUT' || code === 'ETIMEDOUT'
|| ex?.body?.errcode === 'M_LIMIT_EXCEEDED') { || code === 'M_LIMIT_EXCEEDED') {
await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i)); await new Promise(resolve => setTimeout(resolve, REGISTRATION_RETRY_BASE_DELAY_MS * i * i));
continue; continue;
} }
if (code === 'M_USER_IN_USE') {
const loginUrl = `${homeserver}/_matrix/client/r0/login`
const loginConfig = {
url: loginUrl,
method: "post",
headers: {"Content-Type": "application/json"},
data: JSON.stringify({
"type": "m.login.password",
"identifier": {
"type": "m.id.user",
"user": username
},
"password": password
}),
timeout: 60000
}
let resp2 = await axios(loginConfig)
return resp2.data?.access_token
}
throw ex; throw ex;
} }
} }
@ -78,31 +103,24 @@ export type RegistrationOptions = {
/** /**
* Register a new test user. * Register a new test user.
* *
* @returns A string that is both the username and password of a new user. * @returns an access token for a new test account
*/ */
async function registerNewTestUser(homeserver: string, options: RegistrationOptions) { async function registerNewTestUser(homeserver: string, options: RegistrationOptions): Promise<string> {
do { do {
let username; let username;
let accessToken: string;
if ("exact" in options.name) { if ("exact" in options.name) {
username = options.name.exact; username = options.name.exact;
} else { } else {
username = `mjolnir-test-user-${options.name.contains}${Math.floor(Math.random() * 100000)}` username = `mjolnir-test-user-${options.name.contains}${Math.floor(Math.random() * 100000)}`
} }
try { try {
await registerUser(homeserver, username, username, username, Boolean(options.isAdmin)); accessToken = await registerUser(homeserver, username, username, username, Boolean(options.isAdmin));
return username; return accessToken;
} catch (e) { } catch (e) {
if (e?.body?.errcode === 'M_USER_IN_USE') {
if ("exact" in options.name) {
LogService.debug("test/clientHelper", `${username} already registered, reusing`);
return username;
} else {
LogService.debug("test/clientHelper", `${username} already registered, trying another`);
}
} else {
console.error(`failed to register user ${e}`); console.error(`failed to register user ${e}`);
throw e; throw e;
}
} }
} while (true); } while (true);
} }
@ -112,10 +130,17 @@ async function registerNewTestUser(homeserver: string, options: RegistrationOpti
* *
* @returns A new `MatrixClient` session for a unique test user. * @returns A new `MatrixClient` session for a unique test user.
*/ */
export async function newTestUser(homeserver: string, options: RegistrationOptions): Promise<MatrixClient> { export async function newTestUser(homeserver: string, options: RegistrationOptions, encrypted: boolean = false): Promise<MatrixClient> {
const username = await registerNewTestUser(homeserver, options); const accessToken = await registerNewTestUser(homeserver, options);
const pantalaimon = new PantalaimonClient(homeserver, new MemoryStorageProvider()); let client;
const client = await pantalaimon.createClientWithCredentials(username, username); if (encrypted) {
const cStore = await getTempCryptoStore();
client = new MatrixClient(homeserver, accessToken, new MemoryStorageProvider(), cStore);
await client.crypto.prepare()
} else {
client = new MatrixClient(homeserver, accessToken, new MemoryStorageProvider())
}
if (!options.isThrottled) { if (!options.isThrottled) {
let userId = await client.getUserId(); let userId = await client.getUserId();
await overrideRatelimitForUser(homeserver, userId); await overrideRatelimitForUser(homeserver, userId);
@ -132,17 +157,11 @@ let _globalAdminUser: MatrixClient;
async function getGlobalAdminUser(homeserver: string): Promise<MatrixClient> { async function getGlobalAdminUser(homeserver: string): Promise<MatrixClient> {
// Initialize global admin user if needed. // Initialize global admin user if needed.
if (!_globalAdminUser) { if (!_globalAdminUser) {
let accessToken: string;
const USERNAME = "mjolnir-test-internal-admin-user"; const USERNAME = "mjolnir-test-internal-admin-user";
try { accessToken = await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true);
await registerUser(homeserver, USERNAME, USERNAME, USERNAME, true); _globalAdminUser = await new MatrixClient(homeserver, accessToken, new MemoryStorageProvider())
} catch (e) { await _globalAdminUser
if (e.isAxiosError && e?.response?.data?.errcode === 'M_USER_IN_USE') {
// Then we've already registered the user in a previous run and that is ok.
} else {
throw e;
}
}
_globalAdminUser = await new PantalaimonClient(homeserver, new MemoryStorageProvider()).createClientWithCredentials(USERNAME, USERNAME);
} }
return _globalAdminUser; return _globalAdminUser;
} }
@ -180,3 +199,20 @@ export function noticeListener(targetRoomdId: string, cb: (event: any) => void)
cb(event); cb(event);
} }
} }
/**
* Tears down temporary crypto stores created for testing
*/
export async function teardownCryptoStores() {
await Promise.all(CryptoStorePaths.map((p: PathLike) => fs.rm(p, { force: true, recursive: true})));
CryptoStorePaths = [];
}
/**
* Helper function to create temp crypto store for testing
*/
export async function getTempCryptoStore() {
const cryptoDir = await fs.mkdtemp('mjolnir-integration-test');
CryptoStorePaths.push(cryptoDir);
return new RustSdkCryptoStorageProvider(cryptoDir, 0);
}

View File

@ -33,6 +33,8 @@ describe("Test: The make admin command", function () {
LogService.debug("makeadminTest", `moderator creating targetRoom: ${targetRoom}; and inviting ${mjolnirUserId}`); LogService.debug("makeadminTest", `moderator creating targetRoom: ${targetRoom}; and inviting ${mjolnirUserId}`);
await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}` }); await moderator.sendMessage(this.mjolnir.managementRoomId, { msgtype: 'm.text.', body: `!mjolnir rooms add ${targetRoom}` });
LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`); LogService.debug("makeadminTest", `Adding targetRoom: ${targetRoom}`);
// allow bot time to join room
await new Promise(resolve => setTimeout(resolve, 1000));
try { try {
await moderator.start(); await moderator.start();
await userA.start(); await userA.start();

View File

@ -1,6 +1,7 @@
import { read as configRead } from "../../src/config"; import { read as configRead } from "../../src/config";
import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils"; import { makeMjolnir, teardownManagementRoom } from "./mjolnirSetupUtils";
import dns from 'node:dns'; import dns from 'node:dns';
import {teardownCryptoStores} from "./clientHelper";
// Necessary for CI: Node 17+ defaults to using ipv6 first, but Github Actions does not support ipv6 // Necessary for CI: Node 17+ defaults to using ipv6 first, but Github Actions does not support ipv6
dns.setDefaultResultOrder('ipv4first'); dns.setDefaultResultOrder('ipv4first');
@ -39,7 +40,8 @@ export const mochaHooks = {
]); ]);
// remove alias from management room and leave it. // remove alias from management room and leave it.
await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, this.managementRoomAlias); await teardownManagementRoom(this.mjolnir.client, this.mjolnir.managementRoomId, this.managementRoomAlias);
await teardownCryptoStores()
console.error("---- completed test", JSON.stringify(this.currentTest.title), "\n\n"); // Makes MatrixClient error logs a bit easier to parse. console.error("---- completed test", JSON.stringify(this.currentTest.title), "\n\n"); // Makes MatrixClient error logs a bit easier to parse.
} }
] ],
}; };

View File

@ -15,7 +15,6 @@ limitations under the License.
*/ */
import { import {
MatrixClient, MatrixClient,
PantalaimonClient,
MemoryStorageProvider, MemoryStorageProvider,
LogService, LogService,
LogLevel, LogLevel,
@ -23,7 +22,7 @@ import {
} from "@vector-im/matrix-bot-sdk"; } from "@vector-im/matrix-bot-sdk";
import { Mjolnir} from '../../src/Mjolnir'; import { Mjolnir} from '../../src/Mjolnir';
import { overrideRatelimitForUser, registerUser } from "./clientHelper"; import {getTempCryptoStore, overrideRatelimitForUser, registerUser} from "./clientHelper";
import { initializeGlobalPerformanceMetrics, initializeSentry, patchMatrixClient } from "../../src/utils"; import { initializeGlobalPerformanceMetrics, initializeSentry, patchMatrixClient } from "../../src/utils";
import { IConfig } from "../../src/config"; import { IConfig } from "../../src/config";
@ -54,15 +53,9 @@ async function configureMjolnir(config: IConfig) {
initializeSentry(config); initializeSentry(config);
initializeGlobalPerformanceMetrics(config); initializeGlobalPerformanceMetrics(config);
try { let accessToken = await registerUser(config.homeserverUrl, config.encryption.username, config.encryption.username, config.encryption.password, true)
await registerUser(config.homeserverUrl, config.pantalaimon.username, config.pantalaimon.username, config.pantalaimon.password, true)
} catch (e) { return accessToken
if (e?.body?.errcode === 'M_USER_IN_USE') {
console.log(`${config.pantalaimon.username} already registered, skipping`);
return;
}
throw e;
};
} }
export function mjolnir(): Mjolnir | null { export function mjolnir(): Mjolnir | null {
@ -78,12 +71,14 @@ let globalMjolnir: Mjolnir | null;
* Return a test instance of Mjolnir. * Return a test instance of Mjolnir.
*/ */
export async function makeMjolnir(config: IConfig): Promise<Mjolnir> { export async function makeMjolnir(config: IConfig): Promise<Mjolnir> {
await configureMjolnir(config); let accessToken = await configureMjolnir(config);
let cryptoStore = await getTempCryptoStore()
LogService.setLogger(new RichConsoleLogger()); LogService.setLogger(new RichConsoleLogger());
LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG)); LogService.setLevel(LogLevel.fromString(config.logLevel, LogLevel.DEBUG));
LogService.info("test/mjolnirSetupUtils", "Starting bot..."); LogService.info("test/mjolnirSetupUtils", "Starting bot...");
const pantalaimon = new PantalaimonClient(config.homeserverUrl, new MemoryStorageProvider()); let client = new MatrixClient(config.homeserverUrl, accessToken, new MemoryStorageProvider(), cryptoStore);
const client = await pantalaimon.createClientWithCredentials(config.pantalaimon.username, config.pantalaimon.password); await client.crypto.prepare()
await overrideRatelimitForUser(config.homeserverUrl, await client.getUserId()); await overrideRatelimitForUser(config.homeserverUrl, await client.getUserId());
patchMatrixClient(); patchMatrixClient();
await ensureAliasedRoomExists(client, config.managementRoom); await ensureAliasedRoomExists(client, config.managementRoom);

View File

@ -775,6 +775,15 @@ aws4@^1.8.0:
resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz" resolved "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA== integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axios@^1.7.6:
version "1.7.7"
resolved "https://registry.yarnpkg.com/axios/-/axios-1.7.7.tgz#2f554296f9892a72ac8d8e4c5b79c14a91d0a47f"
integrity sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==
dependencies:
follow-redirects "^1.15.6"
form-data "^4.0.0"
proxy-from-env "^1.1.0"
balanced-match@^1.0.0: balanced-match@^1.0.0:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
@ -1741,6 +1750,11 @@ fn.name@1.x.x:
resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz" resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz"
integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==
follow-redirects@^1.15.6:
version "1.15.9"
resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.9.tgz#a604fa10e443bf98ca94228d9eebcc2e8a2c8ee1"
integrity sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==
forever-agent@~0.6.1: forever-agent@~0.6.1:
version "0.6.1" version "0.6.1"
resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" resolved "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz"
@ -3237,6 +3251,11 @@ proxy-addr@~2.0.7:
forwarded "0.2.0" forwarded "0.2.0"
ipaddr.js "1.9.1" ipaddr.js "1.9.1"
proxy-from-env@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
pseudomap@^1.0.2: pseudomap@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz" resolved "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz"