Skip to content

Commit 0671b17

Browse files
authored
Add a test for mode changes correctly applying to users. (#1732)
* Add powerlevel e2e test * Log failures properly * Log when we don't know a mode value * Set sensible mode defaults * Remove mistaken log line * Remove consistent return lint rule * changelog * update changelog * Improve userPrefixes check * Catch failures to kill redis * Don't call quit twice * Remove duplicate test * Fix unit test * Disable passkey enc * sneaky fix to stop logging pong timeouts erronously * Remove comment * Drop console line * Review tweaks * Fix changelog * Add a loop to check all power levels * Add ability to check for the right PL * Don't wait for charlie's matrix membership
1 parent d49f41d commit 0671b17

File tree

12 files changed

+160
-75
lines changed

12 files changed

+160
-75
lines changed

.eslintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
],
2020
"rules": {
2121
"camelcase": 0, // Rule is bugged atm
22-
"consistent-return": 2,
22+
"consistent-return": 0,
2323
"curly": 1,
2424
"default-case": 2,
2525
"guard-for-in": 2,

changelog.d/1732.bugfix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixes cases where powerlevel changes may not be correctly applied upon mode change.

spec/e2e/pooling.spec.ts

Lines changed: 1 addition & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ describeif('Connection pooling', () => {
2020
}
2121
});
2222
await testEnv.setUp();
23-
})
23+
});
2424

2525
// Ensure we always tear down
2626
afterEach(() => {
@@ -72,48 +72,4 @@ describeif('Connection pooling', () => {
7272
bob.say(channel, "Hi alice!");
7373
await bobMsg;
7474
});
75-
76-
it('should be able to recover from legacy client state', async () => {
77-
const channel = `#${TestIrcServer.generateUniqueNick("test")}`;
78-
79-
const { homeserver } = testEnv;
80-
const alice = homeserver.users[0].client;
81-
const { bob } = testEnv.ircTest.clients;
82-
83-
// Create the channel
84-
await bob.join(channel);
85-
86-
const adminRoomId = await testEnv.createAdminRoomHelper(alice);
87-
const cRoomId = await testEnv.joinChannelHelper(alice, adminRoomId, channel);
88-
89-
// And finally wait for bob to appear.
90-
const bobUserId = `@irc_${bob.nick}:${homeserver.domain}`;
91-
await alice.waitForRoomEvent(
92-
{eventType: 'm.room.member', sender: bobUserId, stateKey: bobUserId, roomId: cRoomId}
93-
);
94-
95-
96-
// Send some messages
97-
let aliceMsg = bob.waitForEvent('message', 10000);
98-
let bobMsg = alice.waitForRoomEvent(
99-
{eventType: 'm.room.message', sender: bobUserId, roomId: cRoomId}
100-
);
101-
alice.sendText(cRoomId, "Hello bob!");
102-
await aliceMsg;
103-
bob.say(channel, "Hi alice!");
104-
await bobMsg;
105-
106-
// Now kill the bridge, do NOT kill the dependencies.
107-
await testEnv.recreateBridge();
108-
await testEnv.setUp();
109-
110-
aliceMsg = bob.waitForEvent('message', 10000);
111-
bobMsg = alice.waitForRoomEvent(
112-
{eventType: 'm.room.message', sender: bobUserId, roomId: cRoomId}
113-
);
114-
alice.sendText(cRoomId, "Hello bob!");
115-
await aliceMsg;
116-
bob.say(channel, "Hi alice!");
117-
await bobMsg;
118-
});
11975
});

spec/e2e/powerlevels.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/* eslint-disable @typescript-eslint/no-non-null-assertion */
2+
import { TestIrcServer } from "matrix-org-irc";
3+
import { IrcBridgeE2ETest } from "../util/e2e-test";
4+
import { describe, expect, it } from "@jest/globals";
5+
import { PowerLevelContent } from "matrix-appservice-bridge";
6+
7+
8+
describe('Ensure powerlevels are appropriately applied', () => {
9+
let testEnv: IrcBridgeE2ETest;
10+
beforeEach(async () => {
11+
testEnv = await IrcBridgeE2ETest.createTestEnv({
12+
matrixLocalparts: ['alice'],
13+
ircNicks: ['bob', 'charlie'],
14+
});
15+
await testEnv.setUp();
16+
});
17+
afterEach(() => {
18+
return testEnv?.tearDown();
19+
});
20+
it('should update powerlevel of IRC user when OPed by an IRC user', async () => {
21+
const channel = `#${TestIrcServer.generateUniqueNick("test")}`;
22+
const { homeserver } = testEnv;
23+
const alice = homeserver.users[0].client;
24+
const { bob, charlie } = testEnv.ircTest.clients;
25+
const bobUserId = `@irc_${bob.nick}:${homeserver.domain}`;
26+
const charlieUserId = `@irc_${charlie.nick}:${homeserver.domain}`;
27+
28+
// Create the channel
29+
await bob.join(channel);
30+
31+
const cRoomId = await testEnv.joinChannelHelper(alice, await testEnv.createAdminRoomHelper(alice), channel);
32+
33+
// Now have charlie join and be opped.
34+
await charlie.join(channel);
35+
const operatorPL = testEnv.ircBridge.config.ircService.servers.localhost.modePowerMap!.o;
36+
const plEvent = alice.waitForPowerLevel(
37+
cRoomId, {
38+
users: {
39+
[charlieUserId]: operatorPL,
40+
[testEnv.ircBridge.appServiceUserId]: 100,
41+
[bobUserId]: operatorPL,
42+
},
43+
}
44+
);
45+
46+
await bob.send('MODE', channel, '+o', charlie.nick);
47+
await plEvent;
48+
});
49+
});

spec/unit/pool-service/IrcClientRedisState.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ describe("IrcClientRedisState", () => {
5555
it("should be able to create a fresh state", async () => {
5656
const state = await IrcClientRedisState.create(
5757
fakeRedis(),
58-
userId
58+
userId,
59+
true
5960
);
6061
expect(state.loggedIn).toBeFalse();
6162
expect(state.registered).toBeFalse();
@@ -64,7 +65,8 @@ describe("IrcClientRedisState", () => {
6465
it("should be able to load existing state", async () => {
6566
const state = await IrcClientRedisState.create(
6667
fakeRedis(JSON.stringify(EXISTING_STATE)),
67-
userId
68+
userId,
69+
false
6870
);
6971
expect(state.loggedIn).toBeTrue();
7072
expect(state.registered).toBeTrue();
@@ -101,7 +103,10 @@ describe("IrcClientRedisState", () => {
101103
}
102104
const state = await IrcClientRedisState.create(
103105
fakeRedis(JSON.stringify(existingState)),
104-
userId
106+
userId,
107+
false
105108
);
109+
expect(state.chans.get('#matrix-bridge-test')?.users instanceof Map);
110+
expect(state.chans.get('#halfy-plumbs')?.users instanceof Map);
106111
})
107112
});

spec/util/e2e-test.ts

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { AppServiceRegistration } from "matrix-appservice-bridge";
1+
import { AppServiceRegistration, PowerLevelContent } from "matrix-appservice-bridge";
22
import { BridgeConfig } from "../../src/config/BridgeConfig";
33
import { Client as PgClient } from "pg";
44
import { ComplementHomeServer, createHS, destroyHS } from "./homerunner";
@@ -29,12 +29,50 @@ interface Opts {
2929

3030
export class E2ETestMatrixClient extends MatrixClient {
3131

32-
public async waitForRoomEvent(
32+
public async waitForPowerLevel(
33+
roomId: string, expected: Partial<PowerLevelContent>,
34+
): Promise<{roomId: string, data: {
35+
sender: string, type: string, state_key?: string, content: PowerLevelContent, event_id: string,
36+
}}> {
37+
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
38+
sender: string, type: string, content: Record<string, unknown>, event_id: string, state_key: string,
39+
}) => {
40+
if (eventRoomId !== roomId) {
41+
return undefined;
42+
}
43+
44+
if (eventData.type !== "m.room.power_levels") {
45+
return undefined;
46+
}
47+
48+
if (eventData.state_key !== "") {
49+
return undefined;
50+
}
51+
52+
// Check only the keys we care about
53+
for (const [key, value] of Object.entries(expected)) {
54+
console.log(key, value, "---", eventData.content[key]);
55+
if (JSON.stringify(eventData.content[key], undefined, 0) !== JSON.stringify(value, undefined, 0)) {
56+
return undefined;
57+
}
58+
}
59+
60+
console.info(
61+
// eslint-disable-next-line max-len
62+
`${eventRoomId} ${eventData.event_id} ${eventData.sender}`
63+
);
64+
return {roomId: eventRoomId, data: eventData};
65+
}, `Timed out waiting for powerlevel from in ${roomId}`)
66+
}
67+
68+
public async waitForRoomEvent<T extends object = Record<string, unknown>>(
3369
opts: {eventType: string, sender: string, roomId?: string, stateKey?: string}
34-
): Promise<{roomId: string, data: unknown}> {
70+
): Promise<{roomId: string, data: {
71+
sender: string, type: string, state_key?: string, content: T, event_id: string,
72+
}}> {
3573
const {eventType, sender, roomId, stateKey} = opts;
3674
return this.waitForEvent('room.event', (eventRoomId: string, eventData: {
37-
sender: string, type: string, state_key?: string, content: {body?: string}, event_id: string,
75+
sender: string, type: string, state_key?: string, content: T, event_id: string,
3876
}) => {
3977
if (eventData.sender !== sender) {
4078
return undefined;
@@ -48,9 +86,10 @@ export class E2ETestMatrixClient extends MatrixClient {
4886
if (stateKey !== undefined && eventData.state_key !== stateKey) {
4987
return undefined;
5088
}
89+
const body = 'body' in eventData.content && eventData.content.body;
5190
console.info(
5291
// eslint-disable-next-line max-len
53-
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? eventData.content?.body ?? ''}`
92+
`${eventRoomId} ${eventData.event_id} ${eventData.type} ${eventData.sender} ${eventData.state_key ?? body ?? ''}`
5493
);
5594
return {roomId: eventRoomId, data: eventData};
5695
}, `Timed out waiting for ${eventType} from ${sender} in ${roomId || "any room"}`)
@@ -170,6 +209,9 @@ export class IrcBridgeE2ETest {
170209
connectionString: `${process.env.IRCBRIDGE_TEST_PGURL}/${postgresDb}`,
171210
},
172211
ircService: {
212+
ircHandler: {
213+
powerLevelGracePeriodMs: 0,
214+
},
173215
servers: {
174216
localhost: {
175217
...IrcServer.DEFAULT_CONFIG,

src/bridge/IrcHandler.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -906,14 +906,14 @@ export class IrcHandler {
906906
* @return {Promise} which is resolved/rejected when the request finishes.
907907
*/
908908
public async onMode(req: BridgeRequest, server: IrcServer, channel: string, by: string,
909-
mode: string, enabled: boolean, arg: string|null) {
909+
mode: string, enabled: boolean, arg: string|null): Promise<BridgeRequestErr|undefined> {
910910
this.incrementMetric("mode");
911911
req.log.info(
912912
"onMode(%s) in %s by %s (arg=%s)",
913913
(enabled ? ("+" + mode) : ("-" + mode)),
914914
channel, by, arg
915915
);
916-
await this.roomAccessSyncer.onMode(req, server, channel, by, mode, enabled, arg);
916+
return this.roomAccessSyncer.onMode(req, server, channel, by, mode, enabled, arg);
917917
}
918918

919919
/**

src/bridge/RoomAccessSyncer.ts

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { getLogger } from "../logging";
22
import { IrcBridge } from "./IrcBridge";
3-
import { BridgeRequest } from "../models/BridgeRequest";
3+
import { BridgeRequest, BridgeRequestErr } from "../models/BridgeRequest";
44
import { IrcServer } from "../irc/IrcServer";
55
import { MatrixRoom, PowerLevelContent } from "matrix-appservice-bridge";
66
import { BridgedClientStatus } from "../irc/BridgedClient";
@@ -50,8 +50,7 @@ export class RoomAccessSyncer {
5050
constructor(private ircBridge: IrcBridge) { }
5151

5252
get powerLevelGracePeriod() {
53-
const configPeriod = this.ircBridge.config.ircService.ircHandler?.powerLevelGracePeriodMs;
54-
return configPeriod === undefined ? DEFAULT_POWER_LEVEL_GRACE_MS : configPeriod;
53+
return this.ircBridge.config.ircService.ircHandler?.powerLevelGracePeriodMs ?? DEFAULT_POWER_LEVEL_GRACE_MS;
5554
}
5655

5756
/**
@@ -202,7 +201,7 @@ export class RoomAccessSyncer {
202201
* @param {string|null} arg This is usually the affected user, if applicable.
203202
*/
204203
public async onMode(req: BridgeRequest, server: IrcServer, channel: string, by: string,
205-
mode: string, enabled: boolean, arg: string|null) {
204+
mode: string, enabled: boolean, arg: string|null): Promise<BridgeRequestErr|undefined> {
206205
if (PRIVATE_MODES.includes(mode)) {
207206
await this.onPrivateMode(req, server, channel, mode, enabled);
208207
return;
@@ -215,7 +214,8 @@ export class RoomAccessSyncer {
215214

216215
// Bridge usermodes to power levels
217216
const modeToPower = server.getModePowerMap();
218-
if (!Object.keys(modeToPower).includes(mode)) {
217+
if (mode in modeToPower === false) {
218+
req.log.debug(`Mode '${mode}' is not known`);
219219
// Not an operator power mode
220220
return;
221221
}
@@ -226,7 +226,7 @@ export class RoomAccessSyncer {
226226
);
227227
if (matrixRooms.length === 0) {
228228
req.log.info("No mapped matrix rooms for IRC channel %s", channel);
229-
return;
229+
return BridgeRequestErr.ERR_NOT_MAPPED;
230230
}
231231

232232
// Work out what power levels to give
@@ -243,12 +243,17 @@ export class RoomAccessSyncer {
243243
userId = bridgedClient.userId;
244244
if (bridgedClient.status !== BridgedClientStatus.CONNECTED) {
245245
req.log.info(`Bridged client for ${nick} has no IRC client.`);
246-
return;
246+
return BridgeRequestErr.ERR_DROPPED;
247247
}
248-
const userPrefixes = bridgedClient.chanData(channel)?.users.get(nick);
249-
if (!userPrefixes) {
250-
req.log.error(`No channel data for ${channel}/${nick}`);
251-
return;
248+
const chanData = bridgedClient.chanData(channel);
249+
if (!chanData) {
250+
req.log.error(`No channel data for ${channel}`);
251+
return BridgeRequestErr.ERR_DROPPED;
252+
}
253+
const userPrefixes = chanData.users.get(nick);
254+
if (userPrefixes === undefined) {
255+
req.log.error(`No channel data for ${channel}/${nick}. Is the user still joined to the channel?`);
256+
return BridgeRequestErr.ERR_DROPPED;
252257
}
253258
userPrefixes.split('').forEach(
254259
prefix => {
@@ -266,7 +271,8 @@ export class RoomAccessSyncer {
266271

267272
if (userId === null) {
268273
// Probably the BridgeBot or a user we don't know about, drop it.
269-
return;
274+
req.log.info('Could not determine userId for mode, ignoring');
275+
return BridgeRequestErr.ERR_DROPPED;
270276
}
271277

272278
// By default, unset the user's power level. This will be treated
@@ -283,8 +289,10 @@ export class RoomAccessSyncer {
283289
`${enabled ? level : 0} to ${userId}`
284290
);
285291

292+
let failureCause: Error|undefined;
286293
for (const room of matrixRooms) {
287-
const powerLevelMap = await (this.getCurrentPowerlevels(room.getId())) || {};
294+
const roomId = room.getId();
295+
const powerLevelMap = await (this.getCurrentPowerlevels(roomId)) || {};
288296
const users = (powerLevelMap.users || {}) as {[userId: string]: number};
289297

290298
// If the user's present PL is equal to the level,
@@ -310,9 +318,20 @@ export class RoomAccessSyncer {
310318
// set of modes.
311319
level = 0;
312320
}
313-
this.setPowerLevel(room.getId(), userId, level, req);
321+
try {
322+
req.log.info(`Granting PL${level} to ${userId} in ${roomId}`);
323+
await this.setPowerLevel(roomId, userId, level, req);
324+
}
325+
catch (ex) {
326+
req.log.warn(`Failed to grant PL in ${roomId}`, ex);
327+
failureCause = ex;
328+
}
329+
}
330+
if (failureCause) {
331+
// There *can* be multiple failures, but just use the first one.
332+
// We still log all failures above.
333+
throw new Error('Failed to update PL in some rooms', { cause: failureCause });
314334
}
315-
316335
}
317336
/**
318337
* Called when an IRC server responds to a mode request.

src/irc/BridgedClient.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -808,7 +808,6 @@ export class BridgedClient extends EventEmitter {
808808
identResolver: () => void) {
809809
// If this state has carried over from a previous connection, pull in any channels.
810810
[...connInst.client.chans.keys()].forEach(k => this.chanList.add(k));
811-
console.log('Adding existing channels', this.chanList.entries());
812811
// listen for a connect event which is done when the TCP connection is
813812
// established and set ident info (this is different to the connect() callback
814813
// in node-irc which actually fires on a registered event..)

src/irc/IrcServer.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,10 @@ export class IrcServer {
709709
joinChannelsIfNoUsers: true,
710710
enabled: true
711711
},
712+
modePowerMap: {
713+
o: 50,
714+
v: 1,
715+
},
712716
privateMessages: {
713717
enabled: true,
714718
exclude: [],

0 commit comments

Comments
 (0)