Skip to content

Commit

Permalink
Merge pull request #567 from the-draupnir-project/gnuxie/safe-mode-ap…
Browse files Browse the repository at this point in the history
…pservice-integration-test

appservice provisioned draupnir safe mode test

the-draupnir-project/planning#26
  • Loading branch information
Gnuxie authored Sep 19, 2024
2 parents 4c52212 + 4c0e093 commit 5ab32f9
Show file tree
Hide file tree
Showing 7 changed files with 204 additions and 9 deletions.
38 changes: 36 additions & 2 deletions src/Draupnir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -113,6 +121,8 @@ export class Draupnir implements Client, MatrixAdaptorContext {
this.commandDispatcher
);

private readonly JSInterfaceDispatcher: JSInterfaceCommandDispatcher<BasicInvocationInformation> =
makeDraupnirJSCommandDispatcher(this);
private constructor(
public readonly client: MatrixSendClient,
public readonly clientUserID: StringUserID,
Expand Down Expand Up @@ -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<CommandReturn>(
sender: StringUserID,
...items: Presentation[]
): Promise<ActionResult<CommandReturn>> {
return await this.JSInterfaceDispatcher.invokeCommandFromPresentationStream(
{ commandSender: sender },
new StandardPresentationArgumentStream(items)
);
}

public async sendTextCommand<CommandReturn>(
sender: StringUserID,
command: string
): Promise<ActionResult<CommandReturn>> {
return await this.JSInterfaceDispatcher.invokeCommandFromBody(
{ commandSender: sender },
command
);
}
}
23 changes: 22 additions & 1 deletion src/commands/DraupnirCommandDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ import {
MatrixInterfaceCommandDispatcher,
StandardMatrixInterfaceCommandDispatcher,
CommandPrefixExtractor,
JSInterfaceCommandDispatcher,
BasicInvocationInformation,
StandardJSInterfaceCommandDispatcher,
} from "@the-draupnir-project/interface-manager";
import { Draupnir } from "../Draupnir";
import {
Expand All @@ -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 {
Expand Down Expand Up @@ -60,3 +66,18 @@ export function makeDraupnirCommandDispatcher(
}
);
}

export function makeDraupnirJSCommandDispatcher(
draupnir: Draupnir
): JSInterfaceCommandDispatcher<BasicInvocationInformation> {
return new StandardJSInterfaceCommandDispatcher(
DraupnirTopLevelCommands,
DraupnirHelpCommand,
draupnir,
{
...MPSCommandDispatcherCallbacks,
prefixExtractor: makePrefixExtractor(draupnir),
},
DraupnirContextToCommandContextTranslator
);
}
19 changes: 15 additions & 4 deletions src/draupnirfactory/StandardDraupnirManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`
);
Expand All @@ -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;
}
Expand All @@ -93,7 +94,7 @@ export class StandardDraupnirManager {
config: IConfig,
cause: SafeModeCause
): Promise<ActionResult<SafeModeDraupnir>> {
if (this.isDraupnirAvailable(clientUserID)) {
if (this.isSafeModeDraupnir(clientUserID)) {
return ActionError.Result(
`There is a draupnir for ${clientUserID} already running`
);
Expand All @@ -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)
);
}

Expand Down
16 changes: 15 additions & 1 deletion src/safemode/DraupnirSafeMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -111,4 +115,14 @@ export class SafeModeDraupnir implements MatrixAdaptorContext {
) as Promise<Result<void>>
);
}

public async sendTextCommand<CommandReturn>(
sender: StringUserID,
command: string
): Promise<Result<CommandReturn>> {
return await this.JSInterfaceDispatcher.invokeCommandFromBody(
{ commandSender: sender },
command
);
}
}
23 changes: 22 additions & 1 deletion src/safemode/SafeModeCommandDispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down Expand Up @@ -55,3 +61,18 @@ export function makeSafeModeCommandDispatcher(
}
);
}

export function makeSafeModeJSDispatcher(
safeModeDraupnir: SafeModeDraupnir
): JSInterfaceCommandDispatcher<BasicInvocationInformation> {
return new StandardJSInterfaceCommandDispatcher(
SafeModeCommands,
SafeModeHelpCommand,
safeModeDraupnir,
{
...MPSCommandDispatcherCallbacks,
prefixExtractor: makePrefixExtractor(safeModeDraupnir),
},
SafeModeContextToCommandContextTranslator
);
}
54 changes: 54 additions & 0 deletions test/appservice/integration/safeModeToggleTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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<SafeModeDraupnir>(
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");
});
});
40 changes: 40 additions & 0 deletions test/appservice/utils/ProvisionHelper.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// SPDX-FileCopyrightText: 2024 Gnuxie <Gnuxie@protonmail.com>
//
// 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<Result<Draupnir>>;
}

export class StandardProvisionHelper implements ProvisionHelper {
public constructor(private readonly appservice: MjolnirAppService) {
// nothing to do.
}
async provisionDraupnir(
requestingUserID: StringUserID
): Promise<Result<Draupnir>> {
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);
}
}

0 comments on commit 5ab32f9

Please sign in to comment.