Skip to content

Commit 8d1af84

Browse files
authored
Unit test the DraupnirKickCommand. (#539)
* Update to MPS v1.2.1. This will allow us to use `describeProtectedRoomsSet` in unit tests. * Update to @the-draupnir-project/[email protected]. This allows for better type inference from the command and also for partial keywords to be provided to commands as arguments. * Unit test the DraupnirKickCommand.
1 parent c34eff5 commit 8d1af84

File tree

4 files changed

+205
-31
lines changed

4 files changed

+205
-31
lines changed

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"@sentry/node": "^7.17.2",
5757
"@sentry/tracing": "^7.17.2",
5858
"@sinclair/typebox": "0.32.34",
59-
"@the-draupnir-project/interface-manager": "1.1.0",
59+
"@the-draupnir-project/interface-manager": "1.1.1",
6060
"@the-draupnir-project/matrix-basic-types": "^0.1.1",
6161
"await-lock": "^2.2.2",
6262
"better-sqlite3": "^9.4.3",
@@ -69,8 +69,8 @@
6969
"js-yaml": "^4.1.0",
7070
"jsdom": "^24.0.0",
7171
"matrix-appservice-bridge": "^9.0.1",
72-
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.1.0",
73-
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.1.0",
72+
"matrix-protection-suite": "npm:@gnuxie/matrix-protection-suite@1.2.0",
73+
"matrix-protection-suite-for-matrix-bot-sdk": "npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.2.1",
7474
"parse-duration": "^1.0.2",
7575
"pg": "^8.8.0",
7676
"shell-quote": "^1.7.3",

src/commands/KickCommand.tsx

Lines changed: 48 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,14 @@
99
// </text>
1010

1111
import { MatrixGlob } from "matrix-bot-sdk";
12-
import { ActionError, Ok, isError } from "matrix-protection-suite";
12+
import {
13+
ActionError,
14+
Ok,
15+
RoomKicker,
16+
RoomResolver,
17+
SetMembership,
18+
isError,
19+
} from "matrix-protection-suite";
1320
import {
1421
StringUserID,
1522
StringRoomID,
@@ -25,8 +32,11 @@ import {
2532
tuple,
2633
} from "@the-draupnir-project/interface-manager";
2734
import { Result } from "@gnuxie/typescript-result";
28-
import { Draupnir } from "../Draupnir";
29-
import { DraupnirInterfaceAdaptor } from "./DraupnirCommandPrerequisites";
35+
import {
36+
DraupnirContextToCommandContextTranslator,
37+
DraupnirInterfaceAdaptor,
38+
} from "./DraupnirCommandPrerequisites";
39+
import { ThrottlingQueue } from "../queues/ThrottlingQueue";
3040

3141
type UsersToKick = Map<StringUserID, StringRoomID[]>;
3242

@@ -65,6 +75,14 @@ function renderUsersToKick(usersToKick: UsersToKick): DocumentNode {
6575
);
6676
}
6777

78+
export type DraupnirKickCommandContext = {
79+
roomKicker: RoomKicker;
80+
roomResolver: RoomResolver;
81+
setMembership: SetMembership;
82+
taskQueue: ThrottlingQueue;
83+
noop: boolean;
84+
};
85+
6886
export const DraupnirKickCommand = describeCommand({
6987
summary:
7088
"Kicks a user or all of those matching a glob in a particular room or all protected rooms. `--glob` must be provided to use globs. Can be scoped to a specific room with `--room`. Can be dry run with `--dry-run`.",
@@ -96,7 +114,13 @@ export const DraupnirKickCommand = describeCommand({
96114
acceptor: StringPresentationType,
97115
},
98116
async executor(
99-
draupnir: Draupnir,
117+
{
118+
roomKicker,
119+
roomResolver,
120+
setMembership,
121+
taskQueue,
122+
noop,
123+
}: DraupnirKickCommandContext,
100124
_info,
101125
keywords,
102126
reasonParts,
@@ -105,8 +129,7 @@ export const DraupnirKickCommand = describeCommand({
105129
const restrictToRoomReference =
106130
keywords.getKeywordValue<MatrixRoomReference>("room", undefined);
107131
const isDryRun =
108-
draupnir.config.noop ||
109-
keywords.getKeywordValue<boolean>("dry-run", false);
132+
noop || keywords.getKeywordValue<boolean>("dry-run", false);
110133
const allowGlob = keywords.getKeywordValue<boolean>("glob", false);
111134
const isGlob =
112135
user.toString().includes("*") || user.toString().includes("?");
@@ -116,23 +139,19 @@ export const DraupnirKickCommand = describeCommand({
116139
);
117140
}
118141
const restrictToRoom = restrictToRoomReference
119-
? await draupnir.clientPlatform
120-
.toRoomResolver()
121-
.resolveRoom(restrictToRoomReference)
142+
? await roomResolver.resolveRoom(restrictToRoomReference)
122143
: undefined;
123144
if (restrictToRoom !== undefined && isError(restrictToRoom)) {
124145
return restrictToRoom;
125146
}
126147
const restrictToRoomRevision =
127148
restrictToRoom === undefined
128149
? undefined
129-
: draupnir.protectedRoomsSet.setMembership.getRevision(
130-
restrictToRoom.ok.toRoomIDOrAlias()
131-
);
150+
: setMembership.getRevision(restrictToRoom.ok.toRoomIDOrAlias());
132151
const roomsToKickWithin =
133152
restrictToRoomRevision !== undefined
134153
? [restrictToRoomRevision]
135-
: draupnir.protectedRoomsSet.setMembership.allRooms;
154+
: setMembership.allRooms;
136155
const reason = reasonParts.join(" ");
137156
const kickRule = new MatrixGlob(user.toString());
138157
const usersToKick: UsersToKick = new Map();
@@ -146,10 +165,10 @@ export const DraupnirKickCommand = describeCommand({
146165
);
147166
}
148167
if (!isDryRun) {
149-
void draupnir.taskQueue.push(async () => {
150-
return draupnir.client.kickUser(
151-
member.userID,
168+
void taskQueue.push(async () => {
169+
return roomKicker.kickUser(
152170
revision.room.toRoomIDOrAlias(),
171+
member.userID,
153172
reason
154173
);
155174
});
@@ -160,6 +179,19 @@ export const DraupnirKickCommand = describeCommand({
160179
},
161180
});
162181

182+
DraupnirContextToCommandContextTranslator.registerTranslation(
183+
DraupnirKickCommand,
184+
function (draupnir) {
185+
return {
186+
roomKicker: draupnir.clientPlatform.toRoomKicker(),
187+
roomResolver: draupnir.clientPlatform.toRoomResolver(),
188+
setMembership: draupnir.protectedRoomsSet.setMembership,
189+
taskQueue: draupnir.taskQueue,
190+
noop: draupnir.config.noop,
191+
};
192+
}
193+
);
194+
163195
DraupnirInterfaceAdaptor.describeRenderer(DraupnirKickCommand, {
164196
JSXRenderer(result) {
165197
if (isError(result)) {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// SPDX-FileCopyrightText: 2024 Gnuxie <[email protected]>
2+
//
3+
// SPDX-License-Identifier: AFL-3.0
4+
5+
import { CommandExecutorHelper } from "@the-draupnir-project/interface-manager";
6+
import {
7+
MatrixRoomID,
8+
MatrixUserID,
9+
StringUserID,
10+
userServerName,
11+
} from "@the-draupnir-project/matrix-basic-types";
12+
import {
13+
Membership,
14+
Ok,
15+
RoomKicker,
16+
RoomResolver,
17+
describeProtectedRoomsSet,
18+
isError,
19+
} from "matrix-protection-suite";
20+
import { DraupnirKickCommand } from "../../../src/commands/KickCommand";
21+
import { ThrottlingQueue } from "../../../src/queues/ThrottlingQueue";
22+
import ManagementRoomOutput from "../../../src/ManagementRoomOutput";
23+
import { createMock } from "ts-auto-mock";
24+
import expect from "expect";
25+
26+
async function createProtectedRooms() {
27+
return await describeProtectedRoomsSet({
28+
rooms: [
29+
{
30+
membershipDescriptions: [...Array(50)].map((_, index) => ({
31+
membership: Membership.Join,
32+
sender: `@${index}:testserver.example.com` as StringUserID,
33+
})),
34+
},
35+
{
36+
membershipDescriptions: [
37+
{
38+
membership: Membership.Join,
39+
sender: `@alice:testserver.example.com` as StringUserID,
40+
},
41+
{
42+
membership: Membership.Join,
43+
sender: `@bob:bob.example.com` as StringUserID,
44+
},
45+
],
46+
},
47+
],
48+
});
49+
}
50+
51+
const taskQueue = new ThrottlingQueue(createMock<ManagementRoomOutput>(), 0);
52+
53+
describe("Test the KickCommand", function () {
54+
const roomResolver = createMock<RoomResolver>({
55+
async resolveRoom(roomReference) {
56+
if (roomReference instanceof MatrixRoomID) {
57+
return Ok(roomReference);
58+
}
59+
throw new TypeError(`We don't really expect to resolve anything`);
60+
},
61+
});
62+
it("Will kick users from protected rooms when a glob is used", async function () {
63+
const { protectedRoomsSet } = await createProtectedRooms();
64+
const roomKicker = createMock<RoomKicker>({
65+
async kickUser(_room, userID, _reason) {
66+
// We should only kick users that match the glob...
67+
expect(userServerName(userID)).toBe("testserver.example.com");
68+
return Ok(undefined);
69+
},
70+
});
71+
const kickResult = await CommandExecutorHelper.execute(
72+
DraupnirKickCommand,
73+
{
74+
taskQueue,
75+
setMembership: protectedRoomsSet.setMembership,
76+
roomKicker,
77+
roomResolver,
78+
noop: false,
79+
},
80+
{
81+
keywords: { glob: true },
82+
},
83+
MatrixUserID.fromUserID(`@*:testserver.example.com` as StringUserID)
84+
);
85+
if (isError(kickResult)) {
86+
throw new TypeError(`We don't expect the kick command itself to fail`);
87+
}
88+
const usersToKick = kickResult.ok;
89+
expect(usersToKick.size).toBe(51);
90+
});
91+
it("Will refuse to kick anyone with a glob if the glob flag is not set", async function () {
92+
const { protectedRoomsSet } = await createProtectedRooms();
93+
const kickResult = await CommandExecutorHelper.execute(
94+
DraupnirKickCommand,
95+
{
96+
taskQueue,
97+
setMembership: protectedRoomsSet.setMembership,
98+
roomKicker: createMock<RoomKicker>(),
99+
roomResolver,
100+
noop: false,
101+
},
102+
{},
103+
MatrixUserID.fromUserID(`@*:testserver.example.com` as StringUserID)
104+
);
105+
expect(isError(kickResult)).toBe(true);
106+
});
107+
it("Will limit the scope to one room if the --room option is provided", async function () {
108+
const { protectedRoomsSet } = await createProtectedRooms();
109+
const targetRoom = protectedRoomsSet.allProtectedRooms[1];
110+
if (targetRoom === undefined) {
111+
throw new TypeError(`Something is wrong with the test!!`);
112+
}
113+
const roomKicker = createMock<RoomKicker>({
114+
async kickUser(room, userID, _reason) {
115+
// We should only kick users that match the glob...
116+
expect(userServerName(userID)).toBe("testserver.example.com");
117+
// We should only kick users in the target room.
118+
expect(room.toString()).toBe(targetRoom.toRoomIDOrAlias());
119+
return Ok(undefined);
120+
},
121+
});
122+
const kickResult = await CommandExecutorHelper.execute(
123+
DraupnirKickCommand,
124+
{
125+
taskQueue,
126+
setMembership: protectedRoomsSet.setMembership,
127+
roomKicker,
128+
roomResolver,
129+
noop: false,
130+
},
131+
{
132+
keywords: { glob: true, room: targetRoom },
133+
},
134+
MatrixUserID.fromUserID(`@*:testserver.example.com` as StringUserID)
135+
);
136+
if (isError(kickResult)) {
137+
throw new TypeError(`We don't expect the kick command itself to fail`);
138+
}
139+
const usersToKick = kickResult.ok;
140+
expect(usersToKick.size).toBe(1);
141+
});
142+
});

yarn.lock

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -256,10 +256,10 @@
256256
resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.27.8.tgz#6667fac16c436b5434a387a34dedb013198f6e6e"
257257
integrity sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==
258258

259-
"@the-draupnir-project/[email protected].0":
260-
version "1.1.0"
261-
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-1.1.0.tgz#ed559d2f5a5ef1c079f784cd0061c47f0bb73102"
262-
integrity sha512-FLGklCbM6QcwWZ0hC6YqjgubHd0qJ7st4aRvRdrtbBRiruuIpYVKqDwHF5nFvWaGvS8CM7bE8a4J4z2K68fxtA==
259+
"@the-draupnir-project/[email protected].1":
260+
version "1.1.1"
261+
resolved "https://registry.yarnpkg.com/@the-draupnir-project/interface-manager/-/interface-manager-1.1.1.tgz#065201a1554cb6e2a4bdd20eb04da1feb413bc53"
262+
integrity sha512-83U5y9u77sAW2bLWmtDBzhAjX3Rbcle7xkQ86ee6sJKjevrjTf7FIqf2CAV1uic1JcQUCJDvfORPPpfNqEqjfw==
263263
dependencies:
264264
"@gnuxie/super-cool-stream" "^0.2.1"
265265
"@gnuxie/typescript-result" "^0.2.0"
@@ -2477,15 +2477,15 @@ matrix-appservice@^2.0.0:
24772477
request-promise "^4.2.6"
24782478
sanitize-html "^2.8.0"
24792479

2480-
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.1.0":
2481-
version "1.1.0"
2482-
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.1.0.tgz#a1b3a338b1b5ca68a1d10e84dcd99415602f99ea"
2483-
integrity sha512-s8w3kHfmuYHQW+9l1OvDjwwd512BC0lr60wegUxM0llBJAH031bOSQd4a4Ro4HbwXHbhGzyrxIckoIiDOd3xoA==
2480+
"matrix-protection-suite-for-matrix-bot-sdk@npm:@gnuxie/matrix-protection-suite-for-matrix-bot-sdk@1.2.1":
2481+
version "1.2.1"
2482+
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite-for-matrix-bot-sdk/-/matrix-protection-suite-for-matrix-bot-sdk-1.2.1.tgz#abbfb08abba5dcec6c5affd775a0577e35aa3c31"
2483+
integrity sha512-EurAFV2gvWV1RZnSIflVW2n0tSh9nJRyXL09zDTzy21Z6l6UvVyvCZ06uvizrqOfT7Ns9S5/N1wsW7wxlkIjfg==
24842484

2485-
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.1.0":
2486-
version "1.1.0"
2487-
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.1.0.tgz#7cbff6dc52870c4eaf24ec783dd8c95a14a20331"
2488-
integrity sha512-vGwj3aN+3M2+1LNpfHGi1ULJxVbZ87WISE0ULVqAlUXSz0Vv9j2Gb1uXoU4naO2By7dcJzQIzYdRVywthkOTYQ==
2485+
"matrix-protection-suite@npm:@gnuxie/matrix-protection-suite@1.2.0":
2486+
version "1.2.0"
2487+
resolved "https://registry.yarnpkg.com/@gnuxie/matrix-protection-suite/-/matrix-protection-suite-1.2.0.tgz#bbdf1df56ea18118e8cfefe21463e929ba5df484"
2488+
integrity sha512-5CWGu7Qu5XjatfvQblMbYsv6c3rqN1bw6mS/AGaB1mOYBWftbR30nUMMkMQ3jzWjptRIJdpX2I7VOnATr8B6bA==
24892489
dependencies:
24902490
"@gnuxie/typescript-result" "^0.2.0"
24912491
await-lock "^2.2.2"

0 commit comments

Comments
 (0)