From f1f1440316c4a1212587dc769b88193feff1ca05 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 19 Sep 2024 15:26:35 +0100 Subject: [PATCH 1/3] Add ProvisionTestHelper for testing appservice draupnir commands. --- test/appservice/utils/ProvisionHelper.ts | 40 ++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 test/appservice/utils/ProvisionHelper.ts diff --git a/test/appservice/utils/ProvisionHelper.ts b/test/appservice/utils/ProvisionHelper.ts new file mode 100644 index 00000000..a1e7d879 --- /dev/null +++ b/test/appservice/utils/ProvisionHelper.ts @@ -0,0 +1,40 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { Ok, Result, ResultError, isError } from "@gnuxie/typescript-result"; +import { Draupnir } from "../../../src/Draupnir"; +import { MjolnirAppService } from "../../../src/appservice/AppService"; +import { StringUserID } from "@the-draupnir-project/matrix-basic-types"; + +export interface ProvisionHelper { + /** + * Automatically make a draupnir and a management room. + */ + provisionDraupnir(requestingUserID: StringUserID): Promise>; +} + +export class StandardProvisionHelper implements ProvisionHelper { + public constructor(private readonly appservice: MjolnirAppService) { + // nothing to do. + } + async provisionDraupnir( + requestingUserID: StringUserID + ): Promise> { + const provisionResult = + await this.appservice.draupnirManager.provisionNewDraupnir( + requestingUserID + ); + if (isError(provisionResult)) { + return provisionResult; + } + const draupnir = await this.appservice.draupnirManager.getRunningDraupnir( + this.appservice.draupnirManager.draupnirMXID(provisionResult.ok), + requestingUserID + ); + if (draupnir === undefined) { + return ResultError.Result(`Failed to find draupnir after provisioning`); + } + return Ok(draupnir); + } +} From 8f95c987adcc4f30caae8065b544d536c09d5a55 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 19 Sep 2024 17:52:53 +0100 Subject: [PATCH 2/3] Create a test for turning provisioned Draupnir to safe mode and back Currently failing because the appservice draupnir manager is broken. --- src/Draupnir.ts | 38 ++++++++++++- src/commands/DraupnirCommandDispatcher.ts | 23 +++++++- src/safemode/DraupnirSafeMode.ts | 16 +++++- src/safemode/SafeModeCommandDispatcher.ts | 23 +++++++- .../integration/safeModeToggleTest.ts | 54 +++++++++++++++++++ 5 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 test/appservice/integration/safeModeToggleTest.ts diff --git a/src/Draupnir.ts b/src/Draupnir.ts index 07024c0e..ef867b10 100644 --- a/src/Draupnir.ts +++ b/src/Draupnir.ts @@ -64,10 +64,18 @@ import { MatrixAdaptorContext, sendMatrixEventsFromDeadDocument, } from "./commands/interface-manager/MPSMatrixInterfaceAdaptor"; -import { makeDraupnirCommandDispatcher } from "./commands/DraupnirCommandDispatcher"; +import { + makeDraupnirCommandDispatcher, + makeDraupnirJSCommandDispatcher, +} from "./commands/DraupnirCommandDispatcher"; import { SafeModeToggle } from "./safemode/SafeModeToggle"; import { makeCommandDispatcherTimelineListener } from "./safemode/ManagementRoom"; - +import { + BasicInvocationInformation, + JSInterfaceCommandDispatcher, + Presentation, + StandardPresentationArgumentStream, +} from "@the-draupnir-project/interface-manager"; const log = new Logger("Draupnir"); // webAPIS should not be included on the Draupnir class. @@ -113,6 +121,8 @@ export class Draupnir implements Client, MatrixAdaptorContext { this.commandDispatcher ); + private readonly JSInterfaceDispatcher: JSInterfaceCommandDispatcher = + makeDraupnirJSCommandDispatcher(this); private constructor( public readonly client: MatrixSendClient, public readonly clientUserID: StringUserID, @@ -354,4 +364,28 @@ export class Draupnir implements Client, MatrixAdaptorContext { public get commandRoomID() { return this.managementRoomID; } + + /** + * API for integration tests to be able to test commands, mostly to ensure + * functionality of the appservice bots. + */ + public async sendPresentationCommand( + sender: StringUserID, + ...items: Presentation[] + ): Promise> { + return await this.JSInterfaceDispatcher.invokeCommandFromPresentationStream( + { commandSender: sender }, + new StandardPresentationArgumentStream(items) + ); + } + + public async sendTextCommand( + sender: StringUserID, + command: string + ): Promise> { + return await this.JSInterfaceDispatcher.invokeCommandFromBody( + { commandSender: sender }, + command + ); + } } diff --git a/src/commands/DraupnirCommandDispatcher.ts b/src/commands/DraupnirCommandDispatcher.ts index 5de8376c..2b499214 100644 --- a/src/commands/DraupnirCommandDispatcher.ts +++ b/src/commands/DraupnirCommandDispatcher.ts @@ -11,6 +11,9 @@ import { MatrixInterfaceCommandDispatcher, StandardMatrixInterfaceCommandDispatcher, CommandPrefixExtractor, + JSInterfaceCommandDispatcher, + BasicInvocationInformation, + StandardJSInterfaceCommandDispatcher, } from "@the-draupnir-project/interface-manager"; import { Draupnir } from "../Draupnir"; import { @@ -21,7 +24,10 @@ import { import { DraupnirHelpCommand } from "./Help"; import { userLocalpart } from "@the-draupnir-project/matrix-basic-types"; import { DraupnirTopLevelCommands } from "./DraupnirCommandTable"; -import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites"; +import { + DraupnirContextToCommandContextTranslator, + DraupnirInterfaceAdaptor, +} from "./DraupnirCommandPrerequisites"; import "./DraupnirCommands"; function makePrefixExtractor(draupnir: Draupnir): CommandPrefixExtractor { @@ -60,3 +66,18 @@ export function makeDraupnirCommandDispatcher( } ); } + +export function makeDraupnirJSCommandDispatcher( + draupnir: Draupnir +): JSInterfaceCommandDispatcher { + return new StandardJSInterfaceCommandDispatcher( + DraupnirTopLevelCommands, + DraupnirHelpCommand, + draupnir, + { + ...MPSCommandDispatcherCallbacks, + prefixExtractor: makePrefixExtractor(draupnir), + }, + DraupnirContextToCommandContextTranslator + ); +} diff --git a/src/safemode/DraupnirSafeMode.ts b/src/safemode/DraupnirSafeMode.ts index 994e1969..ad297a58 100644 --- a/src/safemode/DraupnirSafeMode.ts +++ b/src/safemode/DraupnirSafeMode.ts @@ -22,7 +22,10 @@ import { MatrixSendClient } from "matrix-protection-suite-for-matrix-bot-sdk"; import { MatrixReactionHandler } from "../commands/interface-manager/MatrixReactionHandler"; import { IConfig } from "../config"; import { SafeModeCause } from "./SafeModeCause"; -import { makeSafeModeCommandDispatcher } from "./SafeModeCommandDispatcher"; +import { + makeSafeModeCommandDispatcher, + makeSafeModeJSDispatcher, +} from "./SafeModeCommandDispatcher"; import { ARGUMENT_PROMPT_LISTENER, DEFAUILT_ARGUMENT_PROMPT_LISTENER, @@ -48,6 +51,7 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { this.client, this.commandDispatcher ); + private readonly JSInterfaceDispatcher = makeSafeModeJSDispatcher(this); public constructor( public readonly cause: SafeModeCause, public readonly client: MatrixSendClient, @@ -111,4 +115,14 @@ export class SafeModeDraupnir implements MatrixAdaptorContext { ) as Promise> ); } + + public async sendTextCommand( + sender: StringUserID, + command: string + ): Promise> { + return await this.JSInterfaceDispatcher.invokeCommandFromBody( + { commandSender: sender }, + command + ); + } } diff --git a/src/safemode/SafeModeCommandDispatcher.ts b/src/safemode/SafeModeCommandDispatcher.ts index b0ae6d46..f763d9fd 100644 --- a/src/safemode/SafeModeCommandDispatcher.ts +++ b/src/safemode/SafeModeCommandDispatcher.ts @@ -3,8 +3,11 @@ // SPDX-License-Identifier: AFL-3.0 import { + BasicInvocationInformation, CommandPrefixExtractor, + JSInterfaceCommandDispatcher, MatrixInterfaceCommandDispatcher, + StandardJSInterfaceCommandDispatcher, StandardMatrixInterfaceCommandDispatcher, } from "@the-draupnir-project/interface-manager"; import { SafeModeDraupnir } from "./DraupnirSafeMode"; @@ -16,7 +19,10 @@ import { import { userLocalpart } from "@the-draupnir-project/matrix-basic-types"; import { SafeModeCommands } from "./commands/SafeModeCommands"; import { SafeModeHelpCommand } from "./commands/HelpCommand"; -import { SafeModeInterfaceAdaptor } from "./commands/SafeModeAdaptor"; +import { + SafeModeContextToCommandContextTranslator, + SafeModeInterfaceAdaptor, +} from "./commands/SafeModeAdaptor"; function makePrefixExtractor( safeModeDraupnir: SafeModeDraupnir @@ -55,3 +61,18 @@ export function makeSafeModeCommandDispatcher( } ); } + +export function makeSafeModeJSDispatcher( + safeModeDraupnir: SafeModeDraupnir +): JSInterfaceCommandDispatcher { + return new StandardJSInterfaceCommandDispatcher( + SafeModeCommands, + SafeModeHelpCommand, + safeModeDraupnir, + { + ...MPSCommandDispatcherCallbacks, + prefixExtractor: makePrefixExtractor(safeModeDraupnir), + }, + SafeModeContextToCommandContextTranslator + ); +} diff --git a/test/appservice/integration/safeModeToggleTest.ts b/test/appservice/integration/safeModeToggleTest.ts new file mode 100644 index 00000000..39ea11eb --- /dev/null +++ b/test/appservice/integration/safeModeToggleTest.ts @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { StringUserID } from "@the-draupnir-project/matrix-basic-types"; +import { MjolnirAppService } from "../../../src/appservice/AppService"; +import { newTestUser } from "../../integration/clientHelper"; +import { StandardProvisionHelper } from "../utils/ProvisionHelper"; +import { setupHarness } from "../utils/harness"; +import { SafeModeDraupnir } from "../../../src/safemode/DraupnirSafeMode"; + +interface Context extends Mocha.Context { + appservice?: MjolnirAppService; +} + +describe("Test safe mode commands on a provisioned Draupnir", function () { + beforeEach(async function (this: Context) { + this.appservice = await setupHarness(); + }); + afterEach(function (this: Context) { + if (this.appservice) { + return this.appservice.close(); + } else { + console.warn("Missing Appservice in this context, so cannot stop it."); + return Promise.resolve(); // TS7030: Not all code paths return a value. + } + }); + it("Provisioned draupnir can switch to safe mode and back.", async function (this: Context) { + const appservice = this.appservice; + if (appservice === undefined) { + throw new TypeError(`Test setup failed`); + } + const provisionHelper = new StandardProvisionHelper(appservice); + const moderator = await newTestUser(appservice.config.homeserver.url, { + name: { contains: "moderator" }, + }); + const moderatorUserID = (await moderator.getUserId()) as StringUserID; + const initialDraupnir = ( + await provisionHelper.provisionDraupnir(moderatorUserID) + ).expect("Failed to provision a draupnir for the test"); + const safeModeDraupnir = ( + await initialDraupnir.sendTextCommand( + moderatorUserID, + "!draupnir safe mode" + ) + ).expect("Failed to switch to safe mode"); + ( + await safeModeDraupnir.sendTextCommand( + moderatorUserID, + "!draupnir restart" + ) + ).expect("Failed to restart back to draupnir from safe mode"); + }); +}); From 4c0e093c77eee3219e34bb2ee4b1f7731cc19e54 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Thu, 19 Sep 2024 20:12:07 +0100 Subject: [PATCH 3/3] Fix draupnir manager for safe mode. --- .../StandardDraupnirManager.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/draupnirfactory/StandardDraupnirManager.ts b/src/draupnirfactory/StandardDraupnirManager.ts index 254ece75..dac0b5cb 100644 --- a/src/draupnirfactory/StandardDraupnirManager.ts +++ b/src/draupnirfactory/StandardDraupnirManager.ts @@ -68,7 +68,7 @@ export class StandardDraupnirManager { config, this.makeSafeModeToggle(clientUserID, managementRoom, config) ); - if (this.isDraupnirAvailable(clientUserID)) { + if (this.isNormalDraupnir(clientUserID)) { return ActionError.Result( `There is a draupnir for ${clientUserID} already running` ); @@ -83,6 +83,7 @@ export class StandardDraupnirManager { } this.draupnir.set(clientUserID, draupnir.ok); this.failedDraupnir.delete(clientUserID); + this.safeModeDraupnir.delete(clientUserID); draupnir.ok.start(); return draupnir; } @@ -93,7 +94,7 @@ export class StandardDraupnirManager { config: IConfig, cause: SafeModeCause ): Promise> { - if (this.isDraupnirAvailable(clientUserID)) { + if (this.isSafeModeDraupnir(clientUserID)) { return ActionError.Result( `There is a draupnir for ${clientUserID} already running` ); @@ -115,16 +116,26 @@ export class StandardDraupnirManager { } safeModeDraupnir.ok.start(); this.safeModeDraupnir.set(clientUserID, safeModeDraupnir.ok); + this.draupnir.delete(clientUserID); + this.failedDraupnir.delete(clientUserID); return safeModeDraupnir; } + private isNormalDraupnir(drapunirClientID: StringUserID): boolean { + return this.draupnir.has(drapunirClientID); + } + + private isSafeModeDraupnir(drapunirClientID: StringUserID): boolean { + return this.safeModeDraupnir.has(drapunirClientID); + } + /** * Whether the draupnir is available to the user, either normally or via safe mode. */ public isDraupnirAvailable(draupnirClientID: StringUserID): boolean { return ( - this.draupnir.has(draupnirClientID) || - this.safeModeDraupnir.has(draupnirClientID) + this.isNormalDraupnir(draupnirClientID) || + this.isSafeModeDraupnir(draupnirClientID) ); }