From a5afdbe9ada88abcbb3f8c3e64d53f68dcc7e40f Mon Sep 17 00:00:00 2001 From: Gnuxie <50846879+Gnuxie@users.noreply.github.com> Date: Wed, 11 Sep 2024 10:24:01 +0100 Subject: [PATCH] Unit tests for the ban and unban commands. (#542) * Unit test Ban and UnbanCommands. Fixes https://github.com/the-draupnir-project/Draupnir/issues/441 will follow up with https://github.com/the-draupnir-project/interface-manager/issues/7. Part of https://github.com/the-draupnir-project/planning/issues/22. * Update to @the-draupnir-project/interface-manager@2.1.0. * Rest parameters can only give one argument from prompts. * Update to @the-draupnir-project/interface-manager@2.2.0. --- package.json | 2 +- src/commands/Ban.tsx | 82 ++++++++---- src/commands/CreateBanListCommand.ts | 15 ++- src/commands/Rules.tsx | 10 +- src/commands/StatusCommand.tsx | 17 ++- src/commands/Unban.ts | 117 ++++++++++++---- src/protections/BanPropagation.tsx | 5 +- test/unit/commands/BanCommandTest.ts | 176 +++++++++++++++++++++++++ test/unit/commands/UnbanCommandTest.ts | 132 +++++++++++++++++++ yarn.lock | 9 +- 10 files changed, 496 insertions(+), 69 deletions(-) create mode 100644 test/unit/commands/BanCommandTest.ts create mode 100644 test/unit/commands/UnbanCommandTest.ts diff --git a/package.json b/package.json index e2b3faa3..15076839 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@sentry/node": "^7.17.2", "@sentry/tracing": "^7.17.2", "@sinclair/typebox": "0.32.34", - "@the-draupnir-project/interface-manager": "2.0.0", + "@the-draupnir-project/interface-manager": "2.2.0", "@the-draupnir-project/matrix-basic-types": "^0.1.1", "await-lock": "^2.2.2", "better-sqlite3": "^9.4.3", diff --git a/src/commands/Ban.tsx b/src/commands/Ban.tsx index e76b7973..542efb5e 100644 --- a/src/commands/Ban.tsx +++ b/src/commands/Ban.tsx @@ -8,19 +8,21 @@ // https://github.com/matrix-org/mjolnir // -import { Draupnir } from "../Draupnir"; import { ActionResult, PolicyRoomEditor, PolicyRuleType, isError, Ok, + PolicyRoomManager, + RoomResolver, + PolicyListConfig, } from "matrix-protection-suite"; -import { resolveRoomReferenceSafe } from "matrix-protection-suite-for-matrix-bot-sdk"; import { findPolicyRoomIDFromShortcode } from "./CreateBanListCommand"; import { MatrixRoomReference, MatrixUserID, + StringUserID, } from "@the-draupnir-project/matrix-basic-types"; import { BasicInvocationInformation, @@ -32,22 +34,31 @@ import { tuple, union, } from "@the-draupnir-project/interface-manager"; -import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites"; +import { + DraupnirContextToCommandContextTranslator, + DraupnirInterfaceAdaptor, +} from "./DraupnirCommandPrerequisites"; export async function findPolicyRoomEditorFromRoomReference( - draupnir: Draupnir, + roomResolver: RoomResolver, + policyRoomManager: PolicyRoomManager, policyRoomReference: MatrixRoomReference ): Promise> { - const policyRoomID = await resolveRoomReferenceSafe( - draupnir.client, - policyRoomReference - ); + const policyRoomID = await roomResolver.resolveRoom(policyRoomReference); if (isError(policyRoomID)) { return policyRoomID; } - return await draupnir.policyRoomManager.getPolicyRoomEditor(policyRoomID.ok); + return await policyRoomManager.getPolicyRoomEditor(policyRoomID.ok); } +export type DraupnirBanCommandContext = { + policyRoomManager: PolicyRoomManager; + issuerManager: PolicyListConfig; + defaultReasons: string[]; + roomResolver: RoomResolver; + clientUserID: StringUserID; +}; + export const DraupnirBanCommand = describeCommand({ summary: "Bans an entity from the policy list.", parameters: tuple( @@ -67,13 +78,13 @@ export const DraupnirBanCommand = describeCommand({ MatrixRoomReferencePresentationSchema, StringPresentationType ), - prompt: async function (draupnir: Draupnir) { + prompt: async function ({ + policyRoomManager, + clientUserID, + }: DraupnirBanCommandContext) { return Ok({ - suggestions: draupnir.policyRoomManager - .getEditablePolicyRoomIDs( - draupnir.clientUserID, - PolicyRuleType.User - ) + suggestions: policyRoomManager + .getEditablePolicyRoomIDs(clientUserID, PolicyRuleType.User) .map((room) => MatrixRoomIDPresentationType.wrap(room)), }); }, @@ -83,16 +94,21 @@ export const DraupnirBanCommand = describeCommand({ name: "reason", description: "The reason for the ban.", acceptor: StringPresentationType, - prompt: async function (draupnir: Draupnir) { + prompt: async function ({ defaultReasons }: DraupnirBanCommandContext) { return Ok({ - suggestions: draupnir.config.commands.ban.defaultReasons.map( - (reason) => [StringPresentationType.wrap(reason)] + suggestions: defaultReasons.map((reason) => + StringPresentationType.wrap(reason) ), }); }, }, async executor( - draupnir: Draupnir, + { + issuerManager, + policyRoomManager, + roomResolver, + clientUserID, + }: DraupnirBanCommandContext, _info: BasicInvocationInformation, _keywords, reasonParts, @@ -101,13 +117,19 @@ export const DraupnirBanCommand = describeCommand({ ): Promise> { const policyRoomReference = typeof policyRoomDesignator === "string" - ? await findPolicyRoomIDFromShortcode(draupnir, policyRoomDesignator) + ? await findPolicyRoomIDFromShortcode( + issuerManager, + policyRoomManager, + clientUserID, + policyRoomDesignator + ) : Ok(policyRoomDesignator); if (isError(policyRoomReference)) { return policyRoomReference; } const policyListEditorResult = await findPolicyRoomEditorFromRoomReference( - draupnir, + roomResolver, + policyRoomManager, policyRoomReference.ok ); if (isError(policyListEditorResult)) { @@ -128,10 +150,7 @@ export const DraupnirBanCommand = describeCommand({ reason ); } else { - const resolvedRoomReference = await resolveRoomReferenceSafe( - draupnir.client, - entity - ); + const resolvedRoomReference = await roomResolver.resolveRoom(entity); if (isError(resolvedRoomReference)) { return resolvedRoomReference; } @@ -144,6 +163,19 @@ export const DraupnirBanCommand = describeCommand({ }, }); +DraupnirContextToCommandContextTranslator.registerTranslation( + DraupnirBanCommand, + function (draupnir) { + return { + policyRoomManager: draupnir.policyRoomManager, + issuerManager: draupnir.protectedRoomsSet.issuerManager, + defaultReasons: draupnir.config.commands.ban.defaultReasons, + roomResolver: draupnir.clientPlatform.toRoomResolver(), + clientUserID: draupnir.clientUserID, + }; + } +); + DraupnirInterfaceAdaptor.describeRenderer(DraupnirBanCommand, { isAlwaysSupposedToUseDefaultRenderer: true, }); diff --git a/src/commands/CreateBanListCommand.ts b/src/commands/CreateBanListCommand.ts index 6334e668..dfaf9a6b 100644 --- a/src/commands/CreateBanListCommand.ts +++ b/src/commands/CreateBanListCommand.ts @@ -12,13 +12,18 @@ import { ActionError, ActionResult, Ok, + PolicyListConfig, + PolicyRoomManager, PolicyRuleType, PropagationType, isError, } from "matrix-protection-suite"; import { listInfo } from "./StatusCommand"; import { Draupnir } from "../Draupnir"; -import { MatrixRoomID } from "@the-draupnir-project/matrix-basic-types"; +import { + MatrixRoomID, + StringUserID, +} from "@the-draupnir-project/matrix-basic-types"; import { BasicInvocationInformation, ParsedKeywords, @@ -84,10 +89,12 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirListCreateCommand, { }); export async function findPolicyRoomIDFromShortcode( - draupnir: Draupnir, + issuerManager: PolicyListConfig, + policyRoomManager: PolicyRoomManager, + editingClientUserID: StringUserID, shortcode: string ): Promise> { - const info = await listInfo(draupnir); + const info = await listInfo(issuerManager, policyRoomManager); const matchingRevisions = info.filter( (list) => list.revision.shortcode === shortcode ); @@ -99,7 +106,7 @@ export async function findPolicyRoomIDFromShortcode( return Ok(matchingRevisions[0].revision.room); } else { const remainingRevisions = matchingRevisions.filter((revision) => - revision.revision.isAbleToEdit(draupnir.clientUserID, PolicyRuleType.User) + revision.revision.isAbleToEdit(editingClientUserID, PolicyRuleType.User) ); if ( remainingRevisions.length !== 1 || diff --git a/src/commands/Rules.tsx b/src/commands/Rules.tsx index da737598..b954d52b 100644 --- a/src/commands/Rules.tsx +++ b/src/commands/Rules.tsx @@ -90,7 +90,10 @@ export const DraupnirListRulesCommand = describeCommand({ summary: "Lists the rules currently in use by Draupnir.", parameters: [], async executor(draupnir: Draupnir): Promise> { - const infoResult = await listInfo(draupnir); + const infoResult = await listInfo( + draupnir.protectedRoomsSet.issuerManager, + draupnir.policyRoomManager + ); return Ok( infoResult.map((policyRoom) => ({ room: policyRoom.revision.room, @@ -124,7 +127,10 @@ export const DraupnirRulesMatchingCommand = describeCommand({ _rest, entity ): Promise> { - const policyRooms = await listInfo(draupnir); + const policyRooms = await listInfo( + draupnir.protectedRoomsSet.issuerManager, + draupnir.policyRoomManager + ); return Ok( policyRooms.map((policyRoom) => { return { diff --git a/src/commands/StatusCommand.tsx b/src/commands/StatusCommand.tsx index 484b27bd..bf6918c0 100644 --- a/src/commands/StatusCommand.tsx +++ b/src/commands/StatusCommand.tsx @@ -12,6 +12,8 @@ import { DOCUMENTATION_URL, PACKAGE_JSON, SOFTWARE_VERSION } from "../config"; import { ActionResult, Ok, + PolicyListConfig, + PolicyRoomManager, PolicyRoomRevision, PolicyRoomWatchProfile, PolicyRuleType, @@ -47,12 +49,14 @@ export interface StatusInfo { documentationURL: string; } -export async function listInfo(draupnir: Draupnir): Promise { - const watchedListProfiles = - draupnir.protectedRoomsSet.issuerManager.allWatchedLists; +export async function listInfo( + issuerManager: PolicyListConfig, + policyRoomManager: PolicyRoomManager +): Promise { + const watchedListProfiles = issuerManager.allWatchedLists; const issuerResults = await Promise.all( watchedListProfiles.map((profile) => - draupnir.policyRoomManager.getPolicyRoomRevisionIssuer(profile.room) + policyRoomManager.getPolicyRoomRevisionIssuer(profile.room) ) ); return issuerResults.map((result) => { @@ -89,7 +93,10 @@ DraupnirInterfaceAdaptor.describeRenderer(DraupnirStatusCommand, { export async function draupnirStatusInfo( draupnir: Draupnir ): Promise { - const watchedListInfo = await listInfo(draupnir); + const watchedListInfo = await listInfo( + draupnir.protectedRoomsSet.issuerManager, + draupnir.policyRoomManager + ); const protectedWatchedLists = watchedListInfo.filter((info) => draupnir.protectedRoomsSet.isProtectedRoom( info.revision.room.toRoomIDOrAlias() diff --git a/src/commands/Unban.ts b/src/commands/Unban.ts index ae0025ca..5e9ae2d5 100644 --- a/src/commands/Unban.ts +++ b/src/commands/Unban.ts @@ -8,14 +8,23 @@ // https://github.com/matrix-org/mjolnir // -import { Draupnir } from "../Draupnir"; -import { isError, Ok, PolicyRuleType } from "matrix-protection-suite"; +import { + isError, + Ok, + PolicyListConfig, + PolicyRoomManager, + PolicyRuleType, + RoomResolver, + RoomUnbanner, + SetMembership, +} from "matrix-protection-suite"; import { LogLevel } from "matrix-bot-sdk"; import { findPolicyRoomIDFromShortcode } from "./CreateBanListCommand"; import { isStringUserID, MatrixGlob, MatrixUserID, + StringUserID, } from "@the-draupnir-project/matrix-basic-types"; import { describeCommand, @@ -27,33 +36,47 @@ import { union, } from "@the-draupnir-project/interface-manager"; import { Result } from "@gnuxie/typescript-result"; -import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites"; +import { + DraupnirContextToCommandContextTranslator, + DraupnirInterfaceAdaptor, +} from "./DraupnirCommandPrerequisites"; +import ManagementRoomOutput from "../ManagementRoomOutput"; +import { DraupnirBanCommandContext } from "./Ban"; +import { UnlistedUserRedactionQueue } from "../queues/UnlistedUserRedactionQueue"; -async function unbanUserFromRooms(draupnir: Draupnir, rule: MatrixGlob) { - await draupnir.managementRoomOutput.logMessage( +async function unbanUserFromRooms( + { + managementRoomOutput, + setMembership, + roomUnbanner, + noop, + }: DraupnirUnbanCommandContext, + rule: MatrixGlob +) { + await managementRoomOutput.logMessage( LogLevel.INFO, "Unban", `Unbanning users that match glob: ${rule.regex}` ); - for (const revision of draupnir.protectedRoomsSet.setMembership.allRooms) { + for (const revision of setMembership.allRooms) { for (const member of revision.members()) { if (member.membership !== "ban") { continue; } if (rule.test(member.userID)) { - await draupnir.managementRoomOutput.logMessage( + await managementRoomOutput.logMessage( LogLevel.DEBUG, "Unban", `Unbanning ${member.userID} in ${revision.room.toRoomIDOrAlias()}`, revision.room.toRoomIDOrAlias() ); - if (!draupnir.config.noop) { - await draupnir.client.unbanUser( - member.userID, - revision.room.toRoomIDOrAlias() + if (!noop) { + await roomUnbanner.unbanUser( + revision.room.toRoomIDOrAlias(), + member.userID ); } else { - await draupnir.managementRoomOutput.logMessage( + await managementRoomOutput.logMessage( LogLevel.WARN, "Unban", `Attempted to unban ${member.userID} in ${revision.room.toRoomIDOrAlias()} but Mjolnir is running in no-op mode`, @@ -65,6 +88,18 @@ async function unbanUserFromRooms(draupnir: Draupnir, rule: MatrixGlob) { } } +export type DraupnirUnbanCommandContext = { + policyRoomManager: PolicyRoomManager; + issuerManager: PolicyListConfig; + roomResolver: RoomResolver; + clientUserID: StringUserID; + setMembership: SetMembership; + managementRoomOutput: ManagementRoomOutput; + noop: boolean; + roomUnbanner: RoomUnbanner; + unlistedUserRedactionQueue: UnlistedUserRedactionQueue; +}; + export const DraupnirUnbanCommand = describeCommand({ summary: "Removes an entity from a policy list. If the entity is a glob, then the flag --true must be provided to unban users matching the glob from all protected rooms.", @@ -85,13 +120,13 @@ export const DraupnirUnbanCommand = describeCommand({ MatrixRoomReferencePresentationSchema, StringPresentationType ), - prompt: async function (draupnir: Draupnir) { + prompt: async function ({ + policyRoomManager, + clientUserID, + }: DraupnirBanCommandContext) { return Ok({ - suggestions: draupnir.policyRoomManager - .getEditablePolicyRoomIDs( - draupnir.clientUserID, - PolicyRuleType.User - ) + suggestions: policyRoomManager + .getEditablePolicyRoomIDs(clientUserID, PolicyRuleType.User) .map((room) => MatrixRoomIDPresentationType.wrap(room)), }); }, @@ -105,17 +140,29 @@ export const DraupnirUnbanCommand = describeCommand({ }, }, async executor( - draupnir: Draupnir, + context: DraupnirUnbanCommandContext, _info, keywords, _rest, entity, policyRoomDesignator ): Promise> { - const roomResolver = draupnir.clientPlatform.toRoomResolver(); + const { + roomResolver, + policyRoomManager, + issuerManager, + clientUserID, + unlistedUserRedactionQueue, + managementRoomOutput, + } = context; const policyRoomReference = typeof policyRoomDesignator === "string" - ? await findPolicyRoomIDFromShortcode(draupnir, policyRoomDesignator) + ? await findPolicyRoomIDFromShortcode( + issuerManager, + policyRoomManager, + clientUserID, + policyRoomDesignator + ) : Ok(policyRoomDesignator); if (isError(policyRoomReference)) { return policyRoomReference; @@ -124,8 +171,9 @@ export const DraupnirUnbanCommand = describeCommand({ if (isError(policyRoom)) { return policyRoom; } - const policyRoomEditor = - await draupnir.policyRoomManager.getPolicyRoomEditor(policyRoom.ok); + const policyRoomEditor = await policyRoomManager.getPolicyRoomEditor( + policyRoom.ok + ); if (isError(policyRoomEditor)) { return policyRoomEditor; } @@ -156,15 +204,15 @@ export const DraupnirUnbanCommand = describeCommand({ string.includes("*") ? true : string.includes("?"); const rule = new MatrixGlob(entity.toString()); if (isStringUserID(rawEnttiy)) { - draupnir.unlistedUserRedactionQueue.removeUser(rawEnttiy); + unlistedUserRedactionQueue.removeUser(rawEnttiy); } if ( !isGlob(rawEnttiy) || keywords.getKeywordValue("true", false) ) { - await unbanUserFromRooms(draupnir, rule); + await unbanUserFromRooms(context, rule); } else { - await draupnir.managementRoomOutput.logMessage( + await managementRoomOutput.logMessage( LogLevel.WARN, "Unban", "Running unban without `unban true` will not override existing room level bans" @@ -175,6 +223,23 @@ export const DraupnirUnbanCommand = describeCommand({ }, }); +DraupnirContextToCommandContextTranslator.registerTranslation( + DraupnirUnbanCommand, + function (draupnir) { + return { + policyRoomManager: draupnir.policyRoomManager, + issuerManager: draupnir.protectedRoomsSet.issuerManager, + roomResolver: draupnir.clientPlatform.toRoomResolver(), + clientUserID: draupnir.clientUserID, + setMembership: draupnir.protectedRoomsSet.setMembership, + managementRoomOutput: draupnir.managementRoomOutput, + noop: draupnir.config.noop, + roomUnbanner: draupnir.clientPlatform.toRoomUnbanner(), + unlistedUserRedactionQueue: draupnir.unlistedUserRedactionQueue, + }; + } +); + DraupnirInterfaceAdaptor.describeRenderer(DraupnirUnbanCommand, { isAlwaysSupposedToUseDefaultRenderer: true, }); diff --git a/src/protections/BanPropagation.tsx b/src/protections/BanPropagation.tsx index b24888e4..740b6214 100644 --- a/src/protections/BanPropagation.tsx +++ b/src/protections/BanPropagation.tsx @@ -298,7 +298,10 @@ export class BanPropagationProtection PolicyRuleType.User, Recommendation.Ban ); - const policyRoomInfo = await listInfo(draupnir); + const policyRoomInfo = await listInfo( + draupnir.protectedRoomsSet.issuerManager, + draupnir.policyRoomManager + ); if (rulesMatchingUser.length === 0) { return; // user is already unbanned. } diff --git a/test/unit/commands/BanCommandTest.ts b/test/unit/commands/BanCommandTest.ts new file mode 100644 index 00000000..a0892da1 --- /dev/null +++ b/test/unit/commands/BanCommandTest.ts @@ -0,0 +1,176 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { + CommandExecutorHelper, + MatrixRoomIDPresentationType, + MatrixUserIDPresentationType, + PromptRequiredError, +} from "@the-draupnir-project/interface-manager"; +import { + MatrixRoomID, + MatrixUserID, + StringUserID, +} from "@the-draupnir-project/matrix-basic-types"; +import { + Ok, + PolicyRoomEditor, + PolicyRoomManager, + PolicyRuleType, + PowerLevelsEventContent, + Recommendation, + RoomResolver, + describeProtectedRoomsSet, + isOk, + randomEventID, +} from "matrix-protection-suite"; +import { createMock } from "ts-auto-mock"; +import expect from "expect"; +import { DraupnirBanCommand } from "../../../src/commands/Ban"; + +const DraupnirUserID = `@draupnir:ourserver.example.com` as StringUserID; + +async function createProtectedRooms() { + return await describeProtectedRoomsSet({ + rooms: [ + { + policyDescriptions: [ + { + entity: "@existing-spam:spam.example.com", + recommendation: Recommendation.Ban, + type: PolicyRuleType.User, + }, + ], + stateDescriptions: [ + { + sender: DraupnirUserID, + type: "m.room.power_levels", + state_key: "", + content: { + users: { + [DraupnirUserID]: 100, + }, + } satisfies PowerLevelsEventContent, + }, + ], + }, + ], + }); +} + +describe("Test the DraupnirBanCommand", function () { + const roomResolver = createMock({ + async resolveRoom(roomReference) { + if (roomReference instanceof MatrixRoomID) { + return Ok(roomReference); + } + throw new TypeError(`We don't really expect to resolve anything`); + }, + }); + it("Will prompt for the policy list when it is missing", async function () { + const { protectedRoomsSet, policyRoomManager } = + await createProtectedRooms(); + const banResult = await CommandExecutorHelper.parseAndInvoke( + DraupnirBanCommand, + { + policyRoomManager, + roomResolver, + issuerManager: protectedRoomsSet.issuerManager, + defaultReasons: ["spam"], + clientUserID: `@draupnir:ourserver.example.com` as StringUserID, + }, + {}, + MatrixUserIDPresentationType.wrap( + MatrixUserID.fromUserID("@spam:spam.example.com" as StringUserID) + ), + undefined // no policy room provided, we expect a prompt. + ); + if (isOk(banResult)) { + throw new TypeError(`We expect a prompt error to be returned`); + } + if (!(banResult.error instanceof PromptRequiredError)) { + throw new TypeError(`Expected to have a prompt required error`); + } + expect(banResult.error.parameterRequiringPrompt.name).toBe("list"); + }); + it("Will prompt for the reason when it is missing", async function () { + const { protectedRoomsSet, policyRoomManager } = + await createProtectedRooms(); + const policyRoom = protectedRoomsSet.allProtectedRooms[0]; + if (policyRoom === undefined) { + throw new TypeError( + `There should be a policy room available from the setup` + ); + } + const banResult = await CommandExecutorHelper.parseAndInvoke( + DraupnirBanCommand, + { + policyRoomManager, + roomResolver, + issuerManager: protectedRoomsSet.issuerManager, + defaultReasons: ["spam"], + clientUserID: `@draupnir:ourserver.example.com` as StringUserID, + }, + {}, + MatrixUserIDPresentationType.wrap( + MatrixUserID.fromUserID("@spam:spam.example.com" as StringUserID) + ), + MatrixRoomIDPresentationType.wrap(policyRoom) + ); + if (isOk(banResult)) { + throw new TypeError(`We expect a prompt error to be returned`); + } + if (!(banResult.error instanceof PromptRequiredError)) { + throw new TypeError(`Expected to have a prompt required error`); + } + expect(banResult.error.parameterRequiringPrompt.name).toBe("reason"); + }); + it("Will add a user to the policy list when they are banned", async function () { + const { protectedRoomsSet } = await createProtectedRooms(); + const policyRoom = protectedRoomsSet.allProtectedRooms[0]; + if (policyRoom === undefined) { + throw new TypeError( + `There should be a policy room available from the setup` + ); + } + const policyRoomManager = createMock({ + async getPolicyRoomEditor(room) { + expect(room).toBe(policyRoom); + return Ok( + createMock({ + async createPolicy( + entityType, + recommendation, + entity, + reason, + _additionalProperties + ) { + expect(entityType).toBe(PolicyRuleType.User); + expect(recommendation).toBe(Recommendation.Ban); + expect(entity).toBe("@spam:spam.example.com"); + expect(reason).toBe("spam"); + return Ok(randomEventID()); + }, + }) + ); + }, + }); + const banResult = await CommandExecutorHelper.execute( + DraupnirBanCommand, + { + policyRoomManager, + roomResolver, + issuerManager: protectedRoomsSet.issuerManager, + defaultReasons: ["spam"], + clientUserID: `@draupnir:ourserver.example.com` as StringUserID, + }, + { + rest: ["spam"], + }, + MatrixUserID.fromUserID("@spam:spam.example.com" as StringUserID), + policyRoom + ); + expect(banResult.isOkay).toBe(true); + }); +}); diff --git a/test/unit/commands/UnbanCommandTest.ts b/test/unit/commands/UnbanCommandTest.ts new file mode 100644 index 00000000..ec0b9669 --- /dev/null +++ b/test/unit/commands/UnbanCommandTest.ts @@ -0,0 +1,132 @@ +// SPDX-FileCopyrightText: 2024 Gnuxie +// +// SPDX-License-Identifier: AFL-3.0 + +import { CommandExecutorHelper } from "@the-draupnir-project/interface-manager"; +import { + MatrixRoomID, + MatrixUserID, + StringUserID, +} from "@the-draupnir-project/matrix-basic-types"; +import { + Membership, + Ok, + PolicyRoomEditor, + PolicyRoomManager, + PolicyRuleType, + PowerLevelsEventContent, + Recommendation, + RoomResolver, + RoomUnbanner, + describeProtectedRoomsSet, + isError, +} from "matrix-protection-suite"; +import { createMock } from "ts-auto-mock"; +import expect from "expect"; +import { DraupnirUnbanCommand } from "../../../src/commands/Unban"; +import ManagementRoomOutput from "../../../src/ManagementRoomOutput"; +import { UnlistedUserRedactionQueue } from "../../../src/queues/UnlistedUserRedactionQueue"; + +const DraupnirUserID = `@draupnir:ourserver.example.com` as StringUserID; +const ExistingBanUserID = "@existing-spam:spam.example.com" as StringUserID; + +async function createProtectedRooms() { + return await describeProtectedRoomsSet({ + rooms: [ + { + membershipDescriptions: [ + { + sender: DraupnirUserID, + target: ExistingBanUserID, + membership: Membership.Ban, + }, + ], + policyDescriptions: [ + { + entity: ExistingBanUserID, + recommendation: Recommendation.Ban, + type: PolicyRuleType.User, + }, + ], + stateDescriptions: [ + { + sender: DraupnirUserID, + type: "m.room.power_levels", + state_key: "", + content: { + users: { + [DraupnirUserID]: 100, + }, + } satisfies PowerLevelsEventContent, + }, + ], + }, + ], + }); +} + +describe("Test the DraupnirUnbanCommand", function () { + const roomResolver = createMock({ + async resolveRoom(roomReference) { + if (roomReference instanceof MatrixRoomID) { + return Ok(roomReference); + } + throw new TypeError(`We don't really expect to resolve anything`); + }, + }); + it("Will add a user to the policy list when they are banned", async function () { + const roomUnbanner = createMock({ + async unbanUser(_room, userID, _reason) { + expect(userID).toBe(ExistingBanUserID); + return Ok(undefined); + }, + }); + const { protectedRoomsSet, policyRoomManager } = + await createProtectedRooms(); + const policyRoom = protectedRoomsSet.allProtectedRooms[0]; + if (policyRoom === undefined) { + throw new TypeError( + `There should be a policy room available from the setup` + ); + } + const revisionIssuer = + await policyRoomManager.getPolicyRoomRevisionIssuer(policyRoom); + if (isError(revisionIssuer)) { + throw new TypeError(`Test is wrong`); + } + const mockPolicyRoomManager = createMock({ + async getPolicyRoomEditor(room) { + expect(room).toBe(policyRoom); + return Ok( + createMock({ + async unbanEntity(ruleType, entity) { + expect(ruleType).toBe(PolicyRuleType.User); + expect(entity).toBe(ExistingBanUserID); + return Ok(revisionIssuer.ok.currentRevision.allRules()); + }, + }) + ); + }, + }); + const banResult = await CommandExecutorHelper.execute( + DraupnirUnbanCommand, + { + policyRoomManager: mockPolicyRoomManager, + setMembership: protectedRoomsSet.setMembership, + managementRoomOutput: createMock(), + roomResolver, + issuerManager: protectedRoomsSet.issuerManager, + clientUserID: `@draupnir:ourserver.example.com` as StringUserID, + noop: false, + roomUnbanner, + unlistedUserRedactionQueue: createMock(), + }, + { + rest: ["spam"], + }, + MatrixUserID.fromUserID(ExistingBanUserID), + policyRoom + ); + expect(banResult.isOkay).toBe(true); + }); +}); diff --git a/yarn.lock b/yarn.lock index d5e6579b..95c0a84c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -256,14 +256,13 @@ resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e" integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA== -"@the-draupnir-project/interface-manager@2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.0.0.tgz#1386bb7d59257d867f963adf600cc506f3ec591d" - integrity sha512-DD3D3cOx0LqPJ4oET1+ttpUlVZf+4u8RTqFwxL0Qh8KOWLS9oxBwiw+U0IPDbTa+07hbhzY/hRMRBiqheXS/LQ== +"@the-draupnir-project/interface-manager@2.2.0": + version "2.2.0" + resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-2.2.0.tgz#2326aca4ccbf0b0ed743efb3a20222a5d78ab23e" + integrity sha512-ycNBKT78RsRhU+HfHe3gjgtQY2pCn7Umw8uAJBHGzLFpXwk2E9kmv6dGbtGubINjsFdkacr7lVxZwm69K4bSUg== dependencies: "@gnuxie/super-cool-stream" "^0.2.1" "@gnuxie/typescript-result" "^0.2.0" - "@the-draupnir-project/matrix-basic-types" "^0.1.1" "@the-draupnir-project/matrix-basic-types@^0.1.1": version "0.1.1"