028357f15f
* Fix reactive display name disambiguation When a room member changes their display name, recalculate the disambiguation flag for all other members who share (or previously shared) that display name. This ensures that the 'disambiguate' flag is updated reactively when display name conflicts appear or are resolved. Fixes element-hq/element-web#468 Fixes element-hq/element-web#4795 Fixes element-hq/element-web#31551 Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com> * Refactor: move disambiguation logic per review feedback - Added updateDisambiguation() method to RoomMember for direct disambiguation recalculation - Moved affected display name tracking to setStateEvents() instead of updateDisplayNameCache() - Removed setMembershipEvent() hack, now calls updateDisambiguation() directly Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com> * Exclude processed members from disambiguation loop Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com> --------- Signed-off-by: aditya-cherukuru <cherukuru.aditya01@gmail.com>
1452 lines
57 KiB
TypeScript
1452 lines
57 KiB
TypeScript
/*
|
|
Copyright 2022 The Matrix.org Foundation C.I.C.
|
|
|
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
you may not use this file except in compliance with the License.
|
|
You may obtain a copy of the License at
|
|
|
|
http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
Unless required by applicable law or agreed to in writing, software
|
|
distributed under the License is distributed on an "AS IS" BASIS,
|
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
See the License for the specific language governing permissions and
|
|
limitations under the License.
|
|
*/
|
|
|
|
import { type MockedObject } from "vitest";
|
|
|
|
import * as utils from "../test-utils/test-utils";
|
|
import { makeBeaconEvent, makeBeaconInfoEvent } from "../test-utils/beacon";
|
|
import { filterEmitCallsByEventType } from "../test-utils/emitter";
|
|
import { RoomState, RoomStateEvent } from "../../src/models/room-state";
|
|
import { RoomMemberEvent } from "../../src/models/room-member";
|
|
import { type Beacon, BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
|
|
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
|
|
import { type IContent, MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
|
import { M_BEACON } from "../../src/@types/beacon";
|
|
import { type MatrixClient } from "../../src/client";
|
|
import { Room } from "../../src/models/room";
|
|
import { KnownMembership } from "../../src/@types/membership";
|
|
import { DecryptionFailureCode } from "../../src/crypto-api";
|
|
import { DecryptionError } from "../../src/common-crypto/CryptoBackend";
|
|
|
|
describe("RoomState", function () {
|
|
const roomId = "!foo:bar";
|
|
const userA = "@alice:bar";
|
|
const userB = "@bob:bar";
|
|
const userC = "@cleo:bar";
|
|
const userLazy = "@lazy:bar";
|
|
|
|
let state = new RoomState(roomId);
|
|
let statev12 = new RoomState(roomId);
|
|
|
|
beforeEach(function () {
|
|
const commonEvents = [
|
|
utils.mkMembership({
|
|
// userA joined
|
|
event: true,
|
|
mship: KnownMembership.Join,
|
|
user: userA,
|
|
room: roomId,
|
|
}),
|
|
utils.mkMembership({
|
|
// userB joined
|
|
event: true,
|
|
mship: KnownMembership.Join,
|
|
user: userB,
|
|
room: roomId,
|
|
}),
|
|
utils.mkEvent({
|
|
// Room name is "Room name goes here"
|
|
type: "m.room.name",
|
|
user: userA,
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
name: "Room name goes here",
|
|
},
|
|
}),
|
|
];
|
|
|
|
state = new RoomState(roomId);
|
|
state.setStateEvents([
|
|
...commonEvents,
|
|
utils.mkEvent({
|
|
// Room creation
|
|
type: "m.room.create",
|
|
user: userA,
|
|
room: roomId,
|
|
event: true,
|
|
content: {},
|
|
}),
|
|
]);
|
|
|
|
statev12 = new RoomState(roomId);
|
|
statev12.setStateEvents([
|
|
...commonEvents,
|
|
utils.mkEvent({
|
|
// Room creation (v12 version)
|
|
type: "m.room.create",
|
|
user: userA,
|
|
room: roomId,
|
|
event: true,
|
|
content: { room_version: "12" },
|
|
}),
|
|
]);
|
|
});
|
|
|
|
describe("getMembers", function () {
|
|
it("should return an empty list if there are no members", function () {
|
|
state = new RoomState(roomId);
|
|
expect(state.getMembers().length).toEqual(0);
|
|
});
|
|
|
|
it("should return a member for each m.room.member event", function () {
|
|
const members = state.getMembers();
|
|
expect(members.length).toEqual(2);
|
|
// ordering unimportant
|
|
expect([userA, userB].indexOf(members[0].userId)).not.toEqual(-1);
|
|
expect([userA, userB].indexOf(members[1].userId)).not.toEqual(-1);
|
|
});
|
|
});
|
|
|
|
describe("getMember", function () {
|
|
it("should return null if there is no member", function () {
|
|
expect(state.getMember("@no-one:here")).toEqual(null);
|
|
});
|
|
|
|
it("should return a member if they exist", function () {
|
|
expect(state.getMember(userB)).toBeTruthy();
|
|
});
|
|
|
|
it("should return a member which changes as state changes", function () {
|
|
const member = state.getMember(userB);
|
|
expect(member?.membership).toEqual(KnownMembership.Join);
|
|
expect(member?.name).toEqual(userB);
|
|
|
|
state.setStateEvents([
|
|
utils.mkMembership({
|
|
room: roomId,
|
|
user: userB,
|
|
mship: KnownMembership.Leave,
|
|
event: true,
|
|
name: "BobGone",
|
|
}),
|
|
]);
|
|
|
|
expect(member?.membership).toEqual(KnownMembership.Leave);
|
|
expect(member?.name).toEqual("BobGone");
|
|
});
|
|
});
|
|
|
|
describe("getSentinelMember", function () {
|
|
it("should return a member with the user id as name", function () {
|
|
expect(state.getSentinelMember("@no-one:here")?.name).toEqual("@no-one:here");
|
|
});
|
|
|
|
it("should return a member which doesn't change when the state is updated", function () {
|
|
const preLeaveUser = state.getSentinelMember(userA);
|
|
state.setStateEvents([
|
|
utils.mkMembership({
|
|
room: roomId,
|
|
user: userA,
|
|
mship: KnownMembership.Leave,
|
|
event: true,
|
|
name: "AliceIsGone",
|
|
}),
|
|
]);
|
|
const postLeaveUser = state.getSentinelMember(userA);
|
|
|
|
expect(preLeaveUser?.membership).toEqual(KnownMembership.Join);
|
|
expect(preLeaveUser?.name).toEqual(userA);
|
|
|
|
expect(postLeaveUser?.membership).toEqual(KnownMembership.Leave);
|
|
expect(postLeaveUser?.name).toEqual("AliceIsGone");
|
|
});
|
|
});
|
|
|
|
describe("getStateEvents", function () {
|
|
it("should return null if a state_key was specified and there was no match", function () {
|
|
expect(state.getStateEvents("foo.bar.baz", "keyname")).toEqual(null);
|
|
});
|
|
|
|
it("should return an empty list if a state_key was not specified and there" + " was no match", function () {
|
|
expect(state.getStateEvents("foo.bar.baz")).toEqual([]);
|
|
});
|
|
|
|
it("should return a list of matching events if no state_key was specified", function () {
|
|
const events = state.getStateEvents("m.room.member");
|
|
expect(events.length).toEqual(2);
|
|
// ordering unimportant
|
|
expect([userA, userB].indexOf(events[0].getStateKey() as string)).not.toEqual(-1);
|
|
expect([userA, userB].indexOf(events[1].getStateKey() as string)).not.toEqual(-1);
|
|
});
|
|
|
|
it("should return a single MatrixEvent if a state_key was specified", function () {
|
|
const event = state.getStateEvents("m.room.member", userA);
|
|
expect(event?.getContent<IContent>()).toMatchObject({
|
|
membership: KnownMembership.Join,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("setStateEvents", function () {
|
|
it("should emit 'RoomState.members' for each m.room.member event", function () {
|
|
const memberEvents = [
|
|
utils.mkMembership({
|
|
user: "@cleo:bar",
|
|
mship: KnownMembership.Invite,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
utils.mkMembership({
|
|
user: "@daisy:bar",
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
];
|
|
let emitCount = 0;
|
|
state.on(RoomStateEvent.Members, function (ev, st, mem) {
|
|
expect(ev).toEqual(memberEvents[emitCount]);
|
|
expect(st).toEqual(state);
|
|
expect(mem).toEqual(state.getMember(ev.getSender()!));
|
|
emitCount += 1;
|
|
});
|
|
state.setStateEvents(memberEvents);
|
|
expect(emitCount).toEqual(2);
|
|
});
|
|
|
|
it("should emit 'RoomState.newMember' for each new member added", function () {
|
|
const memberEvents = [
|
|
utils.mkMembership({
|
|
user: "@cleo:bar",
|
|
mship: KnownMembership.Invite,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
utils.mkMembership({
|
|
user: "@daisy:bar",
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
];
|
|
let emitCount = 0;
|
|
state.on(RoomStateEvent.NewMember, function (ev, st, mem) {
|
|
expect(state.getMember(mem.userId)).toEqual(mem);
|
|
expect(mem.userId).toEqual(memberEvents[emitCount].getSender());
|
|
expect(mem.membership).toBeFalsy(); // not defined yet
|
|
emitCount += 1;
|
|
});
|
|
state.setStateEvents(memberEvents);
|
|
expect(emitCount).toEqual(2);
|
|
});
|
|
|
|
it("should emit 'RoomState.events' for each state event", function () {
|
|
const events = [
|
|
utils.mkMembership({
|
|
user: "@cleo:bar",
|
|
mship: KnownMembership.Invite,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
utils.mkEvent({
|
|
user: userB,
|
|
room: roomId,
|
|
type: "m.room.topic",
|
|
event: true,
|
|
content: {
|
|
topic: "boo!",
|
|
},
|
|
}),
|
|
utils.mkMessage({
|
|
// Not a state event
|
|
user: userA,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
];
|
|
let emitCount = 0;
|
|
state.on(RoomStateEvent.Events, function (ev, st) {
|
|
expect(ev).toEqual(events[emitCount]);
|
|
expect(st).toEqual(state);
|
|
emitCount += 1;
|
|
});
|
|
state.setStateEvents(events);
|
|
expect(emitCount).toEqual(2);
|
|
});
|
|
|
|
it("should call setPowerLevel on each RoomMember for m.room.power_levels", function () {
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: {
|
|
users_default: 10,
|
|
state_default: 50,
|
|
events_default: 25,
|
|
},
|
|
});
|
|
|
|
// spy on the room members
|
|
vi.spyOn(state.members[userA], "setPowerLevel");
|
|
vi.spyOn(state.members[userB], "setPowerLevel");
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.members[userA].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
|
expect(state.members[userB].setPowerLevel).toHaveBeenCalledWith(10, powerLevelEvent);
|
|
});
|
|
|
|
it("should call setPowerLevelEvent on a new RoomMember if power levels exist", function () {
|
|
const memberEvent = utils.mkMembership({
|
|
mship: KnownMembership.Join,
|
|
user: userC,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
event: true,
|
|
content: {
|
|
users_default: 10,
|
|
state_default: 50,
|
|
events_default: 25,
|
|
users: {},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
state.setStateEvents([memberEvent]);
|
|
|
|
// TODO: We do this because we don't DI the RoomMember constructor
|
|
// so we can't inject a mock :/ so we have to infer.
|
|
expect(state.members[userC]).toBeTruthy();
|
|
expect(state.members[userC].powerLevel).toEqual(10);
|
|
});
|
|
|
|
it("should calculate power level correctly", function () {
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
[userB]: 200,
|
|
"@invalid:user": 10, // shouldn't barf on this.
|
|
},
|
|
},
|
|
event: true,
|
|
});
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.getMember(userA)?.powerLevel).toEqual(20);
|
|
expect(state.getMember(userB)?.powerLevel).toEqual(200);
|
|
});
|
|
|
|
it("should set 'powerLevel' with a v12 room.", function () {
|
|
const createEventV12 = utils.mkEvent({
|
|
type: "m.room.create",
|
|
room: roomId,
|
|
sender: userA,
|
|
content: { room_version: "12" },
|
|
event: true,
|
|
});
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
[userB]: 200,
|
|
"@invalid:user": 10, // shouldn't barf on this.
|
|
},
|
|
},
|
|
event: true,
|
|
});
|
|
state.setStateEvents([createEventV12, powerLevelEvent]);
|
|
expect(state.getMember(userA)?.powerLevel).toEqual(Infinity);
|
|
});
|
|
|
|
it("should honour power levels of zero.", function () {
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
"@alice:bar": 0,
|
|
},
|
|
},
|
|
event: true,
|
|
});
|
|
let emitCount = 0;
|
|
|
|
const memberA = state.getMember(userA)!;
|
|
// set the power level to something other than zero or we
|
|
// won't get an event
|
|
memberA.powerLevel = 1;
|
|
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
|
emitCount += 1;
|
|
expect(emitMember.userId).toEqual("@alice:bar");
|
|
expect(emitMember.powerLevel).toEqual(0);
|
|
expect(emitEvent).toEqual(powerLevelEvent);
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
expect(memberA.powerLevel).toEqual(0);
|
|
expect(emitCount).toEqual(1);
|
|
});
|
|
|
|
it("should not honor string power levels.", function () {
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: "m.room.power_levels",
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
"@alice:bar": "5",
|
|
},
|
|
},
|
|
event: true,
|
|
});
|
|
let emitCount = 0;
|
|
|
|
const memberA = state.getMember(userA)!;
|
|
memberA.on(RoomMemberEvent.PowerLevel, function (emitEvent, emitMember) {
|
|
emitCount += 1;
|
|
expect(emitMember.userId).toEqual("@alice:bar");
|
|
expect(emitMember.powerLevel).toEqual(20);
|
|
expect(emitEvent).toEqual(powerLevelEvent);
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
expect(memberA.powerLevel).toEqual(20);
|
|
expect(emitCount).toEqual(1);
|
|
});
|
|
|
|
it("should no-op if given a non-state or unrelated event", () => {
|
|
const memberA = state.getMember(userA)!;
|
|
const fn = vi.spyOn(memberA, "emit");
|
|
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
|
|
|
const powerLevelEvent = utils.mkEvent({
|
|
type: EventType.RoomPowerLevels,
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
"@alice:bar": "5",
|
|
},
|
|
},
|
|
skey: "invalid",
|
|
event: true,
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
const nonStateEv = utils.mkEvent({
|
|
type: EventType.RoomPowerLevels,
|
|
room: roomId,
|
|
user: userA,
|
|
content: {
|
|
users_default: 20,
|
|
users: {
|
|
"@alice:bar": "5",
|
|
},
|
|
},
|
|
event: true,
|
|
});
|
|
delete nonStateEv.event.state_key;
|
|
state.setStateEvents([nonStateEv]);
|
|
state.setStateEvents([
|
|
utils.mkEvent({
|
|
type: EventType.Sticker,
|
|
room: roomId,
|
|
user: userA,
|
|
content: {},
|
|
event: true,
|
|
}),
|
|
]);
|
|
expect(fn).not.toHaveBeenCalledWith(RoomMemberEvent.PowerLevel);
|
|
});
|
|
|
|
it("should call setMembershipEvent on the right RoomMember", function () {
|
|
const memberEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Leave,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
// spy on the room members
|
|
vi.spyOn(state.members[userA], "setMembershipEvent");
|
|
vi.spyOn(state.members[userB], "setMembershipEvent");
|
|
state.setStateEvents([memberEvent]);
|
|
|
|
expect(state.members[userA].setMembershipEvent).not.toHaveBeenCalled();
|
|
expect(state.members[userB].setMembershipEvent).toHaveBeenCalledWith(memberEvent, state);
|
|
});
|
|
|
|
it("should emit `RoomStateEvent.Marker` for each marker event", function () {
|
|
const events = [
|
|
utils.mkEvent({
|
|
event: true,
|
|
type: UNSTABLE_MSC2716_MARKER.name,
|
|
room: roomId,
|
|
user: userA,
|
|
skey: "",
|
|
content: {
|
|
"m.insertion_id": "$abc",
|
|
},
|
|
}),
|
|
];
|
|
let emitCount = 0;
|
|
state.on(RoomStateEvent.Marker, function (markerEvent, markerFoundOptions) {
|
|
expect(markerEvent).toEqual(events[emitCount]);
|
|
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
|
|
emitCount += 1;
|
|
});
|
|
state.setStateEvents(events, { timelineWasEmpty: true });
|
|
expect(emitCount).toEqual(1);
|
|
});
|
|
});
|
|
|
|
describe("beacon events", () => {
|
|
it("adds new beacon info events to state and emits", () => {
|
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId);
|
|
const emitSpy = vi.spyOn(state, "emit");
|
|
|
|
state.setStateEvents([beaconEvent]);
|
|
|
|
expect(state.beacons.size).toEqual(1);
|
|
const beaconInstance = state.beacons.get(`${roomId}_${userA}`);
|
|
expect(beaconInstance).toBeTruthy();
|
|
expect(emitSpy).toHaveBeenCalledWith(BeaconEvent.New, beaconEvent, beaconInstance);
|
|
});
|
|
|
|
it("does not add redacted beacon info events to state", () => {
|
|
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
|
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
|
|
const redactionEvent = new MatrixEvent({ type: "m.room.redaction" });
|
|
const room = new Room(roomId, mockClient, userA);
|
|
redactedBeaconEvent.makeRedacted(redactionEvent, room);
|
|
const emitSpy = vi.spyOn(state, "emit");
|
|
|
|
state.setStateEvents([redactedBeaconEvent]);
|
|
|
|
// no beacon added
|
|
expect(state.beacons.size).toEqual(0);
|
|
expect(state.beacons.get(getBeaconInfoIdentifier(redactedBeaconEvent))).toBeFalsy();
|
|
// no new beacon emit
|
|
expect(filterEmitCallsByEventType(BeaconEvent.New, emitSpy).length).toBeFalsy();
|
|
});
|
|
|
|
it("updates existing beacon info events in state", () => {
|
|
const beaconId = "$beacon1";
|
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
|
const updatedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: false }, beaconId);
|
|
|
|
state.setStateEvents([beaconEvent]);
|
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
|
expect(beaconInstance?.isLive).toEqual(true);
|
|
|
|
state.setStateEvents([updatedBeaconEvent]);
|
|
|
|
// same Beacon
|
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(beaconInstance);
|
|
// updated liveness
|
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
|
|
});
|
|
|
|
it("destroys and removes redacted beacon events", () => {
|
|
const mockClient = {} as unknown as MockedObject<MatrixClient>;
|
|
const beaconId = "$beacon1";
|
|
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
|
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
|
|
const redactionEvent = new MatrixEvent({ type: "m.room.redaction", redacts: beaconEvent.getId() });
|
|
const room = new Room(roomId, mockClient, userA);
|
|
redactedBeaconEvent.makeRedacted(redactionEvent, room);
|
|
|
|
state.setStateEvents([beaconEvent]);
|
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
|
|
const destroySpy = vi.spyOn(beaconInstance as Beacon, "destroy");
|
|
expect(beaconInstance?.isLive).toEqual(true);
|
|
|
|
state.setStateEvents([redactedBeaconEvent]);
|
|
|
|
expect(destroySpy).toHaveBeenCalled();
|
|
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))).toBe(undefined);
|
|
});
|
|
|
|
it("updates live beacon ids once after setting state events", () => {
|
|
const liveBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, "$beacon1");
|
|
const deadBeaconEvent = makeBeaconInfoEvent(userB, roomId, { isLive: false }, "$beacon2");
|
|
|
|
const emitSpy = vi.spyOn(state, "emit");
|
|
|
|
state.setStateEvents([liveBeaconEvent, deadBeaconEvent]);
|
|
|
|
// called once
|
|
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(1);
|
|
|
|
// live beacon is now not live
|
|
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
|
|
userA,
|
|
roomId,
|
|
{ isLive: false },
|
|
liveBeaconEvent.getId(),
|
|
);
|
|
|
|
state.setStateEvents([updatedLiveBeaconEvent]);
|
|
|
|
expect(state.hasLiveBeacons).toBe(false);
|
|
expect(filterEmitCallsByEventType(RoomStateEvent.BeaconLiveness, emitSpy).length).toBe(3);
|
|
expect(emitSpy).toHaveBeenCalledWith(RoomStateEvent.BeaconLiveness, state, false);
|
|
});
|
|
});
|
|
|
|
describe("setOutOfBandMembers", function () {
|
|
it("should add a new member", function () {
|
|
const oobMemberEvent = utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
state.markOutOfBandMembersStarted();
|
|
state.setOutOfBandMembers([oobMemberEvent]);
|
|
const member = state.getMember(userLazy);
|
|
expect(member?.userId).toEqual(userLazy);
|
|
expect(member?.isOutOfBand()).toEqual(true);
|
|
});
|
|
|
|
it("should have no effect when not in correct status", function () {
|
|
state.setOutOfBandMembers([
|
|
utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
]);
|
|
expect(state.getMember(userLazy)).toBeFalsy();
|
|
});
|
|
|
|
it("should emit newMember when adding a member", function () {
|
|
const userLazy = "@oob:hs";
|
|
const oobMemberEvent = utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
let eventReceived = false;
|
|
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
|
|
expect(member.userId).toEqual(userLazy);
|
|
eventReceived = true;
|
|
});
|
|
state.markOutOfBandMembersStarted();
|
|
state.setOutOfBandMembers([oobMemberEvent]);
|
|
expect(eventReceived).toEqual(true);
|
|
});
|
|
|
|
it("should never overwrite existing members", function () {
|
|
const oobMemberEvent = utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
state.markOutOfBandMembersStarted();
|
|
state.setOutOfBandMembers([oobMemberEvent]);
|
|
const memberA = state.getMember(userA);
|
|
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
|
|
expect(memberA?.isOutOfBand()).toEqual(false);
|
|
});
|
|
|
|
it("should emit members when updating a member", function () {
|
|
const doesntExistYetUserId = "@doesntexistyet:hs";
|
|
const oobMemberEvent = utils.mkMembership({
|
|
user: doesntExistYetUserId,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
});
|
|
let eventReceived = false;
|
|
state.once(RoomStateEvent.Members, (_event, _state, member) => {
|
|
expect(member.userId).toEqual(doesntExistYetUserId);
|
|
eventReceived = true;
|
|
});
|
|
|
|
state.markOutOfBandMembersStarted();
|
|
state.setOutOfBandMembers([oobMemberEvent]);
|
|
expect(eventReceived).toEqual(true);
|
|
});
|
|
});
|
|
|
|
describe("clone", function () {
|
|
it("should contain same information as original", function () {
|
|
// include OOB members in copy
|
|
state.markOutOfBandMembersStarted();
|
|
state.setOutOfBandMembers([
|
|
utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
]);
|
|
const copy = state.clone();
|
|
// check individual members
|
|
[userA, userB, userLazy].forEach((userId) => {
|
|
const member = state.getMember(userId);
|
|
const memberCopy = copy.getMember(userId);
|
|
expect(member?.name).toEqual(memberCopy?.name);
|
|
expect(member?.isOutOfBand()).toEqual(memberCopy?.isOutOfBand());
|
|
});
|
|
// check member keys
|
|
expect(Object.keys(state.members)).toEqual(Object.keys(copy.members));
|
|
// check join count
|
|
expect(state.getJoinedMemberCount()).toEqual(copy.getJoinedMemberCount());
|
|
});
|
|
|
|
it("should mark old copy as not waiting for out of band anymore", function () {
|
|
state.markOutOfBandMembersStarted();
|
|
const copy = state.clone();
|
|
copy.setOutOfBandMembers([
|
|
utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
]);
|
|
// should have no effect as it should be marked in status finished just like copy
|
|
state.setOutOfBandMembers([
|
|
utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
]);
|
|
expect(state.getMember(userLazy)).toBeFalsy();
|
|
});
|
|
|
|
it("should return copy independent of original", function () {
|
|
const copy = state.clone();
|
|
copy.setStateEvents([
|
|
utils.mkMembership({
|
|
user: userLazy,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
}),
|
|
]);
|
|
|
|
expect(state.getMember(userLazy)).toBeFalsy();
|
|
expect(state.getJoinedMemberCount()).toEqual(2);
|
|
expect(copy.getJoinedMemberCount()).toEqual(3);
|
|
});
|
|
});
|
|
|
|
describe("setTypingEvent", function () {
|
|
it("should call setTypingEvent on each RoomMember", function () {
|
|
const typingEvent = utils.mkEvent({
|
|
type: "m.typing",
|
|
room: roomId,
|
|
event: true,
|
|
content: {
|
|
user_ids: [userA],
|
|
},
|
|
});
|
|
// spy on the room members
|
|
vi.spyOn(state.members[userA], "setTypingEvent");
|
|
vi.spyOn(state.members[userB], "setTypingEvent");
|
|
state.setTypingEvent(typingEvent);
|
|
|
|
expect(state.members[userA].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
|
expect(state.members[userB].setTypingEvent).toHaveBeenCalledWith(typingEvent);
|
|
});
|
|
});
|
|
|
|
describe("maySendStateEvent", function () {
|
|
it("should say any member may send state with no power level event", function () {
|
|
expect(state.maySendStateEvent("m.room.name", userA)).toEqual(true);
|
|
});
|
|
|
|
it(
|
|
"should say members with power >=50 may send state with power level event " + "but no state default",
|
|
function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
users_default: 10,
|
|
// state_default: 50, "intentionally left blank"
|
|
events_default: 25,
|
|
users: {
|
|
[userA]: 50,
|
|
},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.maySendStateEvent("m.room.name", userA)).toEqual(true);
|
|
expect(state.maySendStateEvent("m.room.name", userB)).toEqual(false);
|
|
},
|
|
);
|
|
|
|
it("should obey state_default", function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
users_default: 10,
|
|
state_default: 30,
|
|
events_default: 25,
|
|
users: {
|
|
[userA]: 30,
|
|
[userB]: 29,
|
|
},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.maySendStateEvent("m.room.name", userA)).toEqual(true);
|
|
expect(state.maySendStateEvent("m.room.name", userB)).toEqual(false);
|
|
});
|
|
|
|
it("should honour explicit event power levels in the power_levels event", function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
events: {
|
|
"m.room.other_thing": 76,
|
|
},
|
|
users_default: 10,
|
|
state_default: 50,
|
|
events_default: 25,
|
|
users: {
|
|
[userA]: 80,
|
|
[userB]: 50,
|
|
},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.maySendStateEvent("m.room.name", userA)).toEqual(true);
|
|
expect(state.maySendStateEvent("m.room.name", userB)).toEqual(true);
|
|
|
|
expect(state.maySendStateEvent("m.room.other_thing", userA)).toEqual(true);
|
|
expect(state.maySendStateEvent("m.room.other_thing", userB)).toEqual(false);
|
|
});
|
|
});
|
|
|
|
describe("getJoinedMemberCount", function () {
|
|
beforeEach(() => {
|
|
state = new RoomState(roomId);
|
|
});
|
|
|
|
it("should update after adding joined member", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
|
]);
|
|
expect(state.getJoinedMemberCount()).toEqual(1);
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getJoinedMemberCount()).toEqual(2);
|
|
});
|
|
});
|
|
|
|
describe("getInvitedMemberCount", function () {
|
|
beforeEach(() => {
|
|
state = new RoomState(roomId);
|
|
});
|
|
|
|
it("should update after adding invited member", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userA, room: roomId }),
|
|
]);
|
|
expect(state.getInvitedMemberCount()).toEqual(1);
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getInvitedMemberCount()).toEqual(2);
|
|
});
|
|
});
|
|
|
|
describe("setJoinedMemberCount", function () {
|
|
beforeEach(() => {
|
|
state = new RoomState(roomId);
|
|
});
|
|
|
|
it("should, once used, override counting members from state", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
|
]);
|
|
expect(state.getJoinedMemberCount()).toEqual(1);
|
|
state.setJoinedMemberCount(100);
|
|
expect(state.getJoinedMemberCount()).toEqual(100);
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getJoinedMemberCount()).toEqual(100);
|
|
});
|
|
|
|
it("should, once used, override counting members from state, " + "also after clone", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userA, room: roomId }),
|
|
]);
|
|
state.setJoinedMemberCount(100);
|
|
const copy = state.clone();
|
|
copy.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Join, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getJoinedMemberCount()).toEqual(100);
|
|
});
|
|
});
|
|
|
|
describe("setInvitedMemberCount", function () {
|
|
beforeEach(() => {
|
|
state = new RoomState(roomId);
|
|
});
|
|
|
|
it("should, once used, override counting members from state", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
|
|
]);
|
|
expect(state.getInvitedMemberCount()).toEqual(1);
|
|
state.setInvitedMemberCount(100);
|
|
expect(state.getInvitedMemberCount()).toEqual(100);
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getInvitedMemberCount()).toEqual(100);
|
|
});
|
|
|
|
it("should, once used, override counting members from state, " + "also after clone", function () {
|
|
state.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userB, room: roomId }),
|
|
]);
|
|
state.setInvitedMemberCount(100);
|
|
const copy = state.clone();
|
|
copy.setStateEvents([
|
|
utils.mkMembership({ event: true, mship: KnownMembership.Invite, user: userC, room: roomId }),
|
|
]);
|
|
expect(state.getInvitedMemberCount()).toEqual(100);
|
|
});
|
|
});
|
|
|
|
describe("maySendEvent", function () {
|
|
it("should say any member may send events with no power level event", function () {
|
|
expect(state.maySendEvent("m.room.message", userA)).toEqual(true);
|
|
expect(state.maySendMessage(userA)).toEqual(true);
|
|
});
|
|
|
|
it("should obey events_default", function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
users_default: 10,
|
|
state_default: 30,
|
|
events_default: 25,
|
|
users: {
|
|
[userA]: 26,
|
|
[userB]: 24,
|
|
},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.maySendEvent("m.room.message", userA)).toEqual(true);
|
|
expect(state.maySendEvent("m.room.message", userB)).toEqual(false);
|
|
|
|
expect(state.maySendMessage(userA)).toEqual(true);
|
|
expect(state.maySendMessage(userB)).toEqual(false);
|
|
});
|
|
|
|
it("should honour explicit event power levels in the power_levels event", function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
events: {
|
|
"m.room.other_thing": 33,
|
|
},
|
|
users_default: 10,
|
|
state_default: 50,
|
|
events_default: 25,
|
|
users: {
|
|
[userA]: 40,
|
|
[userB]: 30,
|
|
},
|
|
},
|
|
});
|
|
|
|
state.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(state.maySendEvent("m.room.message", userA)).toEqual(true);
|
|
expect(state.maySendEvent("m.room.message", userB)).toEqual(true);
|
|
|
|
expect(state.maySendMessage(userA)).toEqual(true);
|
|
expect(state.maySendMessage(userB)).toEqual(true);
|
|
|
|
expect(state.maySendEvent("m.room.other_thing", userA)).toEqual(true);
|
|
expect(state.maySendEvent("m.room.other_thing", userB)).toEqual(false);
|
|
});
|
|
|
|
it("should recognise power level of room creators in v12 rooms", function () {
|
|
const powerLevelEvent = new MatrixEvent({
|
|
type: "m.room.power_levels",
|
|
room_id: roomId,
|
|
sender: userA,
|
|
state_key: "",
|
|
content: {
|
|
users_default: 0,
|
|
state_default: 100,
|
|
events_default: 100,
|
|
users: {},
|
|
},
|
|
});
|
|
statev12.setStateEvents([powerLevelEvent]);
|
|
|
|
expect(statev12.maySendEvent("m.room.name", userA)).toEqual(true);
|
|
});
|
|
});
|
|
|
|
describe("processBeaconEvents", () => {
|
|
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, "$beacon1");
|
|
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, "$beacon2");
|
|
|
|
const mockClient = { decryptEventIfNeeded: vi.fn() } as unknown as MockedObject<MatrixClient>;
|
|
|
|
beforeEach(() => {
|
|
mockClient.decryptEventIfNeeded.mockClear();
|
|
});
|
|
|
|
it("does nothing when state has no beacons", () => {
|
|
const emitSpy = vi.spyOn(state, "emit");
|
|
state.processBeaconEvents([makeBeaconEvent(userA, { beaconInfoId: "$beacon1" })], mockClient);
|
|
expect(emitSpy).not.toHaveBeenCalled();
|
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does nothing when there are no events", () => {
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const emitSpy = vi.spyOn(state, "emit").mockClear();
|
|
state.processBeaconEvents([], mockClient);
|
|
expect(emitSpy).not.toHaveBeenCalled();
|
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
|
});
|
|
|
|
describe("without encryption", () => {
|
|
it("discards events for beacons that are not in state", () => {
|
|
const location = makeBeaconEvent(userA, {
|
|
beaconInfoId: "some-other-beacon",
|
|
});
|
|
const otherRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessage,
|
|
content: {
|
|
["m.relates_to"]: {
|
|
event_id: "whatever",
|
|
},
|
|
},
|
|
});
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const emitSpy = vi.spyOn(state, "emit").mockClear();
|
|
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
|
expect(emitSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("discards events that are not beacon type", () => {
|
|
// related to beacon1
|
|
const otherRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessage,
|
|
content: {
|
|
["m.relates_to"]: {
|
|
rel_type: RelationType.Reference,
|
|
event_id: beacon1.getId(),
|
|
},
|
|
},
|
|
});
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const emitSpy = vi.spyOn(state, "emit").mockClear();
|
|
state.processBeaconEvents([otherRelatedEvent], mockClient);
|
|
expect(emitSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("adds locations to beacons", async () => {
|
|
const location1 = makeBeaconEvent(userA, {
|
|
beaconInfoId: "$beacon1",
|
|
timestamp: Date.now() + 1,
|
|
});
|
|
const location2 = makeBeaconEvent(userA, {
|
|
beaconInfoId: "$beacon1",
|
|
timestamp: Date.now() + 2,
|
|
});
|
|
const location3 = makeBeaconEvent(userB, {
|
|
beaconInfoId: "some-other-beacon",
|
|
});
|
|
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
|
|
expect(state.beacons.size).toEqual(2);
|
|
|
|
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
|
const addLocationsSpy = vi.spyOn(beaconInstance, "addLocations");
|
|
|
|
await state.processBeaconEvents([location1, location2, location3], mockClient);
|
|
|
|
expect(addLocationsSpy).toHaveBeenCalledTimes(2);
|
|
// only called with locations for beacon1
|
|
expect(addLocationsSpy).toHaveBeenCalledWith([location1]);
|
|
expect(addLocationsSpy).toHaveBeenCalledWith([location2]);
|
|
});
|
|
});
|
|
|
|
describe("with encryption", () => {
|
|
const beacon1RelationContent = {
|
|
["m.relates_to"]: {
|
|
rel_type: RelationType.Reference,
|
|
event_id: beacon1.getId(),
|
|
},
|
|
};
|
|
const relatedEncryptedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
const decryptingRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
vi.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
|
|
|
const failedDecryptionRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
vi.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
|
|
|
|
it("discards events without relations", () => {
|
|
const unrelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
});
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const emitSpy = vi.spyOn(state, "emit").mockClear();
|
|
state.processBeaconEvents([unrelatedEvent], mockClient);
|
|
expect(emitSpy).not.toHaveBeenCalled();
|
|
// discard unrelated events early
|
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("discards events for beacons that are not in state", () => {
|
|
const location = makeBeaconEvent(userA, {
|
|
beaconInfoId: "some-other-beacon",
|
|
});
|
|
const otherRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: {
|
|
["m.relates_to"]: {
|
|
rel_type: RelationType.Reference,
|
|
event_id: "whatever",
|
|
},
|
|
},
|
|
});
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
|
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
|
const addLocationsSpy = vi.spyOn(beacon, "addLocations").mockClear();
|
|
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
|
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
|
// discard unrelated events early
|
|
expect(mockClient.decryptEventIfNeeded).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("decrypts related events if needed", async () => {
|
|
const location = makeBeaconEvent(userA, {
|
|
beaconInfoId: beacon1.getId(),
|
|
});
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
await state.processBeaconEvents([location, relatedEncryptedEvent], mockClient);
|
|
// discard unrelated events early
|
|
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("awaits for decryption on events that are being decrypted", async () => {
|
|
const decryptingRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
vi.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
|
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
|
|
|
// listener was added
|
|
expect(mockClient.decryptEventIfNeeded).toHaveBeenCalled();
|
|
});
|
|
|
|
it("listens for decryption on events that have decryption failure", async () => {
|
|
const failedDecryptionRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
vi.spyOn(failedDecryptionRelatedEvent, "isDecryptionFailure").mockReturnValue(true);
|
|
mockClient.decryptEventIfNeeded.mockRejectedValue(
|
|
new DecryptionError(DecryptionFailureCode.UNKNOWN_ERROR, "msg"),
|
|
);
|
|
// spy on event.once
|
|
const eventOnceSpy = vi.spyOn(failedDecryptionRelatedEvent, "once");
|
|
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
await state.processBeaconEvents([failedDecryptionRelatedEvent], mockClient);
|
|
|
|
// listener was added
|
|
expect(eventOnceSpy).toHaveBeenCalled();
|
|
});
|
|
|
|
it("discard events that are not m.beacon type after decryption", async () => {
|
|
const decryptingRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
vi.spyOn(decryptingRelatedEvent, "isBeingDecrypted").mockReturnValue(true);
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
|
const addLocationsSpy = vi.spyOn(beacon, "addLocations").mockClear();
|
|
await state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
|
|
|
// this event is a message after decryption
|
|
decryptingRelatedEvent.event.type = EventType.RoomMessage;
|
|
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
|
|
|
|
expect(addLocationsSpy).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("adds locations to beacons after decryption", async () => {
|
|
const decryptingRelatedEvent = new MatrixEvent({
|
|
sender: userA,
|
|
type: EventType.RoomMessageEncrypted,
|
|
content: beacon1RelationContent,
|
|
});
|
|
const locationEvent = makeBeaconEvent(userA, {
|
|
beaconInfoId: "$beacon1",
|
|
timestamp: Date.now() + 1,
|
|
});
|
|
|
|
const decryptEventResolvers = Promise.withResolvers<void>();
|
|
mockClient.decryptEventIfNeeded.mockReturnValue(decryptEventResolvers.promise);
|
|
|
|
state.setStateEvents([beacon1, beacon2]);
|
|
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
|
|
const addLocationsSpy = vi.spyOn(beacon, "addLocations").mockClear();
|
|
const prom = state.processBeaconEvents([decryptingRelatedEvent], mockClient);
|
|
|
|
// update type after '''decryption'''
|
|
decryptingRelatedEvent.event.type = M_BEACON.name;
|
|
decryptingRelatedEvent.event.content = locationEvent.event.content;
|
|
decryptEventResolvers.resolve();
|
|
await prom;
|
|
|
|
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("mayClientSendStateEvent", () => {
|
|
it("should return false if the user isn't authenticated", () => {
|
|
expect(
|
|
state.mayClientSendStateEvent("m.room.message", {
|
|
isGuest: vi.fn().mockReturnValue(false),
|
|
credentials: {},
|
|
} as unknown as MatrixClient),
|
|
).toBeFalsy();
|
|
});
|
|
|
|
it("should return false if the user is a guest", () => {
|
|
expect(
|
|
state.mayClientSendStateEvent("m.room.message", {
|
|
isGuest: vi.fn().mockReturnValue(true),
|
|
credentials: { userId: userA },
|
|
} as unknown as MatrixClient),
|
|
).toBeFalsy();
|
|
});
|
|
});
|
|
|
|
describe("reactive display name disambiguation", function () {
|
|
it("should disambiguate existing member when another member changes to the same name", function () {
|
|
// Create a fresh state
|
|
const testState = new RoomState(roomId);
|
|
|
|
// Alice joins with display name "Alice"
|
|
const aliceJoinEvent = utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
// Bob joins with display name "Bob"
|
|
const bobJoinEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Bob",
|
|
});
|
|
|
|
testState.setStateEvents([aliceJoinEvent, bobJoinEvent]);
|
|
|
|
// Verify no disambiguation needed initially
|
|
const aliceBefore = testState.getMember(userA);
|
|
const bobBefore = testState.getMember(userB);
|
|
expect(aliceBefore?.disambiguate).toBe(false);
|
|
expect(bobBefore?.disambiguate).toBe(false);
|
|
expect(aliceBefore?.name).toBe("Alice");
|
|
expect(bobBefore?.name).toBe("Bob");
|
|
|
|
// Bob changes display name to "Alice"
|
|
const bobRenameEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
testState.setStateEvents([bobRenameEvent]);
|
|
|
|
// Now both should be disambiguated
|
|
const aliceAfter = testState.getMember(userA);
|
|
const bobAfter = testState.getMember(userB);
|
|
expect(aliceAfter?.disambiguate).toBe(true);
|
|
expect(bobAfter?.disambiguate).toBe(true);
|
|
expect(aliceAfter?.name).toContain(userA);
|
|
expect(bobAfter?.name).toContain(userB);
|
|
});
|
|
|
|
it("should un-disambiguate member when conflicting member changes to different name", function () {
|
|
// Create a fresh state
|
|
const testState = new RoomState(roomId);
|
|
|
|
// Both Alice and Bob join with display name "Alice"
|
|
const aliceJoinEvent = utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
const bobJoinEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
testState.setStateEvents([aliceJoinEvent, bobJoinEvent]);
|
|
|
|
// Verify both are disambiguated
|
|
const aliceBefore = testState.getMember(userA);
|
|
const bobBefore = testState.getMember(userB);
|
|
expect(aliceBefore?.disambiguate).toBe(true);
|
|
expect(bobBefore?.disambiguate).toBe(true);
|
|
|
|
// Bob changes display name to "Bob"
|
|
const bobRenameEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Bob",
|
|
});
|
|
|
|
testState.setStateEvents([bobRenameEvent]);
|
|
|
|
// Alice should no longer be disambiguated, Bob should not be either
|
|
const aliceAfter = testState.getMember(userA);
|
|
const bobAfter = testState.getMember(userB);
|
|
expect(aliceAfter?.disambiguate).toBe(false);
|
|
expect(bobAfter?.disambiguate).toBe(false);
|
|
expect(aliceAfter?.name).toBe("Alice");
|
|
expect(bobAfter?.name).toBe("Bob");
|
|
});
|
|
|
|
it("should emit RoomState.members for affected members when disambiguation changes", function () {
|
|
// Create a fresh state
|
|
const testState = new RoomState(roomId);
|
|
|
|
// Alice joins with display name "Alice"
|
|
const aliceJoinEvent = utils.mkMembership({
|
|
user: userA,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
testState.setStateEvents([aliceJoinEvent]);
|
|
|
|
// Set up listener for Members event
|
|
const membersEmitted: string[] = [];
|
|
testState.on(RoomStateEvent.Members, (_ev, _state, member) => {
|
|
membersEmitted.push(member.userId);
|
|
});
|
|
|
|
// Bob joins with display name "Alice" - should trigger disambiguation for Alice
|
|
const bobJoinEvent = utils.mkMembership({
|
|
user: userB,
|
|
mship: KnownMembership.Join,
|
|
room: roomId,
|
|
event: true,
|
|
name: "Alice",
|
|
});
|
|
|
|
testState.setStateEvents([bobJoinEvent]);
|
|
|
|
// Both Alice and Bob should have emitted Members events
|
|
expect(membersEmitted).toContain(userA);
|
|
expect(membersEmitted).toContain(userB);
|
|
});
|
|
});
|
|
});
|