1
0
mirror of https://github.com/matrix-org/matrix-js-sdk.git synced 2025-08-09 10:22:46 +03:00

Merge branch 'develop' into robertlong/group-call

This commit is contained in:
David Baker
2022-09-08 15:03:55 +01:00
committed by GitHub
45 changed files with 3096 additions and 1948 deletions

View File

@@ -1,6 +1,6 @@
/*
Copyright 2015, 2016 OpenMarket Ltd
Copyright 2019 The Matrix.org Foundation C.I.C.
Copyright 2019, 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.
@@ -17,31 +17,32 @@ limitations under the License.
/**
* A mock implementation of the webstorage api
* @constructor
*/
export function MockStorageApi() {
this.data = {};
this.keys = [];
this.length = 0;
}
export class MockStorageApi {
public data: Record<string, string> = {};
public keys: string[] = [];
public length = 0;
MockStorageApi.prototype = {
setItem: function(k, v) {
public setItem(k: string, v: string): void {
this.data[k] = v;
this._recalc();
},
getItem: function(k) {
this.recalc();
}
public getItem(k: string): string | null {
return this.data[k] || null;
},
removeItem: function(k) {
}
public removeItem(k: string): void {
delete this.data[k];
this._recalc();
},
key: function(index) {
this.recalc();
}
public key(index: number): string {
return this.keys[index];
},
_recalc: function() {
const keys = [];
}
private recalc(): void {
const keys: string[] = [];
for (const k in this.data) {
if (!this.data.hasOwnProperty(k)) {
continue;
@@ -50,6 +51,5 @@ MockStorageApi.prototype = {
}
this.keys = keys;
this.length = keys.length;
},
};
}
}

View File

@@ -50,7 +50,7 @@ export class TestClient {
options?: Partial<ICreateClientOpts>,
) {
if (sessionStoreBackend === undefined) {
sessionStoreBackend = new MockStorageApi();
sessionStoreBackend = new MockStorageApi() as unknown as Storage;
}
this.httpBackend = new MockHttpBackend();

View File

@@ -168,6 +168,7 @@ describe("SlidingSyncSdk", () => {
const roomD = "!d_with_notif_count:localhost";
const roomE = "!e_with_invite:localhost";
const roomF = "!f_calc_room_name:localhost";
const roomG = "!g_join_invite_counts:localhost";
const data: Record<string, MSC3575RoomData> = {
[roomA]: {
name: "A",
@@ -261,12 +262,25 @@ describe("SlidingSyncSdk", () => {
],
initial: true,
},
[roomG]: {
name: "G",
required_state: [],
timeline: [
mkOwnStateEvent(EventType.RoomCreate, { creator: selfUserId }, ""),
mkOwnStateEvent(EventType.RoomMember, { membership: "join" }, selfUserId),
mkOwnStateEvent(EventType.RoomPowerLevels, { users: { [selfUserId]: 100 } }, ""),
],
joined_count: 5,
invited_count: 2,
initial: true,
},
};
it("can be created with required_state and timeline", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomA, data[roomA]);
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomA].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-2), data[roomA].timeline);
@@ -276,6 +290,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, data[roomB]);
const gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.name).toEqual(data[roomB].name);
expect(gotRoom.getMyMembership()).toEqual("join");
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-5), data[roomB].timeline);
@@ -285,6 +300,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomC, data[roomC]);
const gotRoom = client.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(data[roomC].highlight_count);
@@ -294,15 +310,26 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomD, data[roomD]);
const gotRoom = client.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(data[roomD].notification_count);
});
it("can be created with an invited/joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, data[roomG]);
const gotRoom = client.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getInvitedMemberCount()).toEqual(data[roomG].invited_count);
expect(gotRoom.getJoinedMemberCount()).toEqual(data[roomG].joined_count);
});
it("can be created with invite_state", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomE, data[roomE]);
const gotRoom = client.getRoom(roomE);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getMyMembership()).toEqual("invite");
expect(gotRoom.currentState.getJoinRule()).toEqual(JoinRule.Invite);
});
@@ -311,6 +338,7 @@ describe("SlidingSyncSdk", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomF, data[roomF]);
const gotRoom = client.getRoom(roomF);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.name,
).toEqual(data[roomF].name);
@@ -326,6 +354,7 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
const newTimeline = data[roomA].timeline;
newTimeline.push(newEvent);
assertTimelineEvents(gotRoom.getLiveTimeline().getEvents().slice(-3), newTimeline);
@@ -333,6 +362,8 @@ describe("SlidingSyncSdk", () => {
it("can update with a new required_state event", async () => {
let gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Invite); // default
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomB, {
required_state: [
@@ -343,6 +374,7 @@ describe("SlidingSyncSdk", () => {
});
gotRoom = client.getRoom(roomB);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinRule()).toEqual(JoinRule.Restricted);
});
@@ -355,6 +387,7 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomC);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Highlight),
).toEqual(1);
@@ -369,11 +402,25 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomD);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(
gotRoom.getUnreadNotificationCount(NotificationCountType.Total),
).toEqual(1);
});
it("can update with a new joined_count", () => {
mockSlidingSync.emit(SlidingSyncEvent.RoomData, roomG, {
name: data[roomD].name,
required_state: [],
timeline: [],
joined_count: 1,
});
const gotRoom = client.getRoom(roomG);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
expect(gotRoom.getJoinedMemberCount()).toEqual(1);
});
// Regression test for a bug which caused the timeline entries to be out-of-order
// when the same room appears twice with different timeline limits. E.g appears in
// the list with timeline_limit:1 then appears again as a room subscription with
@@ -394,6 +441,7 @@ describe("SlidingSyncSdk", () => {
});
const gotRoom = client.getRoom(roomA);
expect(gotRoom).toBeDefined();
if (gotRoom == null) { return; }
logger.log("want:", oldTimeline.map((e) => (e.type + " : " + (e.content || {}).body)));
logger.log("got:", gotRoom.getLiveTimeline().getEvents().map(

View File

@@ -558,6 +558,153 @@ describe("SlidingSync", () => {
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
// this refers to a set of operations where the end result is no change.
it("should handle net zero operations correctly", async () => {
const indexToRoomId = {
0: roomB,
1: roomC,
};
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual(indexToRoomId);
httpBackend.when("POST", syncUrl).respond(200, {
pos: "f",
// currently the list is [B,C] so we will insert D then immediately delete it
lists: [{
count: 500,
ops: [
{
op: "DELETE", index: 2,
},
{
op: "INSERT", index: 0, room_id: roomA,
},
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual(indexToRoomId);
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle deletions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomB,
1: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "g",
lists: [{
count: 499,
ops: [
{
op: "DELETE", index: 0,
},
],
},
{
count: 50,
}],
});
const listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(499);
expect(roomIndexToRoomId).toEqual({
0: roomC,
});
return true;
});
const responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
});
it("should handle insertions correctly", async () => {
expect(slidingSync.getListData(0).roomIndexToRoomId).toEqual({
0: roomC,
});
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 500,
ops: [
{
op: "INSERT", index: 1, room_id: roomA,
},
],
},
{
count: 50,
}],
});
let listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(500);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomA,
});
return true;
});
let responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
httpBackend.when("POST", syncUrl).respond(200, {
pos: "h",
lists: [{
count: 501,
ops: [
{
op: "INSERT", index: 1, room_id: roomB,
},
],
},
{
count: 50,
}],
});
listPromise = listenUntil(slidingSync, "SlidingSync.List",
(listIndex, joinedCount, roomIndexToRoomId) => {
expect(listIndex).toEqual(0);
expect(joinedCount).toEqual(501);
expect(roomIndexToRoomId).toEqual({
0: roomC,
1: roomB,
2: roomA,
});
return true;
});
responseProcessed = listenUntil(slidingSync, "SlidingSync.Lifecycle", (state) => {
return state === SlidingSyncState.Complete;
});
await httpBackend.flushAllExpected();
await responseProcessed;
await listPromise;
slidingSync.stop();
});
});

View File

@@ -20,6 +20,7 @@ import * as utils from "../src/utils";
// try to load the olm library.
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
global.Olm = require('@matrix-org/olm');
logger.log('loaded libolm');
} catch (e) {
@@ -28,6 +29,7 @@ try {
// also try to set node crypto
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const crypto = require('crypto');
utils.setCrypto(crypto);
} catch (err) {

View File

@@ -24,5 +24,5 @@ limitations under the License.
* expect(beaconLivenessEmits.length).toBe(1);
* ```
*/
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, unknown[]>) =>
export const filterEmitCallsByEventType = (eventType: string, spy: jest.SpyInstance<any, any[]>) =>
spy.mock.calls.filter((args) => args[0] === eventType);

View File

@@ -147,9 +147,9 @@ export function mkEventCustom<T>(base: T): T & GeneratedMetadata {
interface IPresenceOpts {
user?: string;
sender?: string;
url: string;
name: string;
ago: number;
url?: string;
name?: string;
ago?: number;
presence?: string;
event?: boolean;
}

View File

@@ -15,6 +15,7 @@ import { CRYPTO_ENABLED } from "../../src/client";
import { DeviceInfo } from "../../src/crypto/deviceinfo";
import { logger } from '../../src/logger';
import { MemoryStore } from "../../src";
import { IStore } from '../../src/store';
const Olm = global.Olm;
@@ -158,8 +159,8 @@ describe("Crypto", function() {
let fakeEmitter;
beforeEach(async function() {
const mockStorage = new MockStorageApi();
const clientStore = new MemoryStore({ localStorage: mockStorage });
const mockStorage = new MockStorageApi() as unknown as Storage;
const clientStore = new MemoryStore({ localStorage: mockStorage }) as unknown as IStore;
const cryptoStore = new MemoryCryptoStore();
cryptoStore.storeEndToEndDeviceData({
@@ -469,12 +470,12 @@ describe("Crypto", function() {
jest.setTimeout(10000);
const client = (new TestClient("@a:example.com", "dev")).client;
await client.initCrypto();
client.crypto.getSecretStorageKey = async () => null;
client.crypto.getSecretStorageKey = jest.fn().mockResolvedValue(null);
client.crypto.isCrossSigningReady = async () => false;
client.crypto.baseApis.uploadDeviceSigningKeys = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.setAccountData = () => null;
client.crypto.baseApis.uploadKeySignatures = () => null;
client.crypto.baseApis.http.authedRequest = () => null;
client.crypto.baseApis.setAccountData = jest.fn().mockResolvedValue(null);
client.crypto.baseApis.uploadKeySignatures = jest.fn();
client.crypto.baseApis.http.authedRequest = jest.fn();
const createSecretStorageKey = async () => {
return {
keyInfo: undefined, // Returning undefined here used to cause a crash

View File

@@ -32,8 +32,8 @@ import { ClientEvent, MatrixClient, RoomMember } from '../../../../src';
import { DeviceInfo, IDevice } from '../../../../src/crypto/deviceinfo';
import { DeviceTrustLevel } from '../../../../src/crypto/CrossSigning';
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const MegolmEncryption = algorithms.ENCRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';

View File

@@ -34,7 +34,7 @@ import { IAbortablePromise, MatrixScheduler } from '../../../src';
const Olm = global.Olm;
const MegolmDecryption = algorithms.DECRYPTION_CLASSES['m.megolm.v1.aes-sha2'];
const MegolmDecryption = algorithms.DECRYPTION_CLASSES.get('m.megolm.v1.aes-sha2');
const ROOM_ID = '!ROOM:ID';

View File

@@ -14,9 +14,9 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {
IndexedDBCryptoStore,
} from '../../../src/crypto/store/indexeddb-crypto-store';
import { CryptoStore } from '../../../src/crypto/store/base';
import { IndexedDBCryptoStore } from '../../../src/crypto/store/indexeddb-crypto-store';
import { LocalStorageCryptoStore } from '../../../src/crypto/store/localStorage-crypto-store';
import { MemoryCryptoStore } from '../../../src/crypto/store/memory-crypto-store';
import { RoomKeyRequestState } from '../../../src/crypto/OutgoingRoomKeyRequestManager';
@@ -26,36 +26,39 @@ import 'jest-localstorage-mock';
const requests = [
{
requestId: "A",
requestBody: { session_id: "A", room_id: "A" },
requestBody: { session_id: "A", room_id: "A", sender_key: "A", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
{
requestId: "B",
requestBody: { session_id: "B", room_id: "B" },
requestBody: { session_id: "B", room_id: "B", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Sent,
recipients: [
{ userId: "@alice:example.com", deviceId: "*" },
{ userId: "@carrie:example.com", deviceId: "barbazquux" },
],
},
{
requestId: "C",
requestBody: { session_id: "C", room_id: "C" },
requestBody: { session_id: "C", room_id: "C", sender_key: "B", algorithm: "m.megolm.v1.aes-sha2" },
state: RoomKeyRequestState.Unsent,
recipients: [
{ userId: "@becca:example.com", deviceId: "foobarbaz" },
],
},
];
describe.each([
["IndexedDBCryptoStore",
() => new IndexedDBCryptoStore(global.indexedDB, "tests")],
["LocalStorageCryptoStore",
() => new IndexedDBCryptoStore(undefined, "tests")],
["MemoryCryptoStore", () => {
const store = new IndexedDBCryptoStore(undefined, "tests");
// @ts-ignore set private properties
store.backend = new MemoryCryptoStore();
// @ts-ignore
store.backendPromise = Promise.resolve(store.backend);
return store;
}],
["LocalStorageCryptoStore", () => new LocalStorageCryptoStore(localStorage)],
["MemoryCryptoStore", () => new MemoryCryptoStore()],
])("Outgoing room key requests [%s]", function(name, dbFactory) {
let store;
let store: CryptoStore;
beforeAll(async () => {
store = dbFactory();
@@ -75,6 +78,15 @@ describe.each([
});
});
it("getOutgoingRoomKeyRequestsByTarget retrieves all entries with a given target",
async () => {
const r = await store.getOutgoingRoomKeyRequestsByTarget(
"@becca:example.com", "foobarbaz", [RoomKeyRequestState.Sent],
);
expect(r).toHaveLength(1);
expect(r[0]).toEqual(requests[0]);
});
test("getOutgoingRoomKeyRequestByState retrieves any entry in a given state",
async () => {
const r =

View File

@@ -16,14 +16,15 @@ limitations under the License.
import * as utils from "../test-utils/test-utils";
import {
DuplicateStrategy,
EventTimeline,
EventTimelineSet,
EventType,
Filter,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
Room,
DuplicateStrategy,
} from '../../src';
import { Thread } from "../../src/models/thread";
import { ReEmitter } from "../../src/ReEmitter";
@@ -291,4 +292,34 @@ describe('EventTimelineSet', () => {
expect(eventTimelineSet.canContain(event)).toBeTruthy();
});
});
describe("handleRemoteEcho", () => {
it("should add to liveTimeline only if the event matches the filter", () => {
const filter = new Filter(client.getUserId()!, "test_filter");
filter.setDefinition({
room: {
timeline: {
types: [EventType.RoomMessage],
},
},
});
const eventTimelineSet = new EventTimelineSet(room, { filter }, client);
const roomMessageEvent = new MatrixEvent({
type: EventType.RoomMessage,
content: { body: "test" },
event_id: "!test1:server",
});
eventTimelineSet.handleRemoteEcho(roomMessageEvent, "~!local-event-id:server", roomMessageEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).toContain(roomMessageEvent);
const roomFilteredEvent = new MatrixEvent({
type: "other_event_type",
content: { body: "test" },
event_id: "!test2:server",
});
eventTimelineSet.handleRemoteEcho(roomFilteredEvent, "~!local-event-id:server", roomFilteredEvent.getId());
expect(eventTimelineSet.getLiveTimeline().getEvents()).not.toContain(roomFilteredEvent);
});
});
});

View File

@@ -36,9 +36,14 @@ import { ReceiptType } from "../../src/@types/read_receipts";
import * as testUtils from "../test-utils/test-utils";
import { makeBeaconInfoContent } from "../../src/content-helpers";
import { M_BEACON_INFO } from "../../src/@types/beacon";
import { ContentHelpers, Room } from "../../src";
import { ContentHelpers, EventTimeline, Room } from "../../src";
import { supportsMatrixCall } from "../../src/webrtc/call";
import { makeBeaconEvent } from "../test-utils/beacon";
import {
IGNORE_INVITES_ACCOUNT_EVENT_KEY,
POLICIES_ACCOUNT_EVENT_TYPE,
PolicyScope,
} from "../../src/models/invites-ignorer";
jest.useFakeTimers();
@@ -427,7 +432,7 @@ describe("MatrixClient", function() {
}
});
});
await client.startClient();
await client.startClient({ filter });
await syncPromise;
});
@@ -1412,4 +1417,301 @@ describe("MatrixClient", function() {
expect(client.crypto.encryptAndSendToDevices).toHaveBeenLastCalledWith(deviceInfos, payload);
});
});
describe("support for ignoring invites", () => {
beforeEach(() => {
// Mockup `getAccountData`/`setAccountData`.
const dataStore = new Map();
client.setAccountData = function(eventType, content) {
dataStore.set(eventType, content);
return Promise.resolve();
};
client.getAccountData = function(eventType) {
const data = dataStore.get(eventType);
return new MatrixEvent({
content: data,
});
};
// Mockup `createRoom`/`getRoom`/`joinRoom`, including state.
const rooms = new Map();
client.createRoom = function(options = {}) {
const roomId = options["_roomId"] || `!room-${rooms.size}:example.org`;
const state = new Map();
const room = {
roomId,
_options: options,
_state: state,
getUnfilteredTimelineSet: function() {
return {
getLiveTimeline: function() {
return {
getState: function(direction) {
expect(direction).toBe(EventTimeline.FORWARDS);
return {
getStateEvents: function(type) {
const store = state.get(type) || {};
return Object.keys(store).map(key => store[key]);
},
};
},
};
},
};
},
};
rooms.set(roomId, room);
return Promise.resolve({ room_id: roomId });
};
client.getRoom = function(roomId) {
return rooms.get(roomId);
};
client.joinRoom = function(roomId) {
return this.getRoom(roomId) || this.createRoom({ _roomId: roomId });
};
// Mockup state events
client.sendStateEvent = function(roomId, type, content) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
let store = state.get(type);
if (!store) {
store = {};
state.set(type, store);
}
const eventId = `$event-${Math.random()}:example.org`;
store[eventId] = {
getId: function() {
return eventId;
},
getRoomId: function() {
return roomId;
},
getContent: function() {
return content;
},
};
return { event_id: eventId };
};
client.redactEvent = function(roomId, eventId) {
const room = this.getRoom(roomId);
const state: Map<string, any> = room._state;
for (const store of state.values()) {
delete store[eventId];
}
};
});
it("should initialize and return the same `target` consistently", async () => {
const target1 = await client.ignoredInvites.getOrCreateTargetRoom();
const target2 = await client.ignoredInvites.getOrCreateTargetRoom();
expect(target1).toBeTruthy();
expect(target1).toBe(target2);
});
it("should initialize and return the same `sources` consistently", async () => {
const sources1 = await client.ignoredInvites.getOrCreateSourceRooms();
const sources2 = await client.ignoredInvites.getOrCreateSourceRooms();
expect(sources1).toBeTruthy();
expect(sources1).toHaveLength(1);
expect(sources1).toEqual(sources2);
});
it("should initially not reject any invite", async () => {
const rule = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(rule).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: user)", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: server)", async () => {
const REASON = `Just a test ${Math.random()}`;
await client.ignoredInvites.addRule(PolicyScope.Server, "example.org", REASON);
// We should reject these invites.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
const ruleRoomMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleRoomMatch).toBeTruthy();
expect(ruleRoomMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
});
it("should reject invites once we have added a matching rule in the target room (scope: room)", async () => {
const REASON = `Just a test ${Math.random()}`;
const BAD_ROOM_ID = "!bad:example.org";
const GOOD_ROOM_ID = "!good:example.org";
await client.ignoredInvites.addRule(PolicyScope.Room, BAD_ROOM_ID, REASON);
// We should reject this invite.
const ruleSenderMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: BAD_ROOM_ID,
});
expect(ruleSenderMatch).toBeTruthy();
expect(ruleSenderMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: REASON,
});
// We should let these invites go through.
const ruleWrongRoom = await client.ignoredInvites.getRuleForInvite({
sender: BAD_ROOM_ID,
roomId: GOOD_ROOM_ID,
});
expect(ruleWrongRoom).toBeFalsy();
});
it("should reject invites once we have added a matching rule in a non-target source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
// Add a rule in the new source room.
await client.sendStateEvent(NEW_SOURCE_ROOM_ID, PolicyScope.User, {
entity: "*:example.org",
reason: "just a test",
recommendation: "m.ban",
});
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// We should let these invites go through.
const ruleWrongServer = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleWrongServer).toBeFalsy();
const ruleWrongServerRoom = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:somewhere.org",
roomId: "!snafu:example.org",
});
expect(ruleWrongServerRoom).toBeFalsy();
});
it("should not reject invites anymore once we have removed a rule", async () => {
await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// We should reject this invite.
const ruleMatch = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch).toBeTruthy();
expect(ruleMatch.getContent()).toMatchObject({
recommendation: "m.ban",
reason: "just a test",
});
// After removing the invite, we shouldn't reject it anymore.
await client.ignoredInvites.removeRule(ruleMatch);
const ruleMatch2 = await client.ignoredInvites.getRuleForInvite({
sender: "@foobar:example.org",
roomId: "!snafu:somewhere.org",
});
expect(ruleMatch2).toBeFalsy();
});
it("should add new rules in the target room, rather than any other source room", async () => {
const NEW_SOURCE_ROOM_ID = "!another-source:example.org";
// Make sure that everything is initialized.
await client.ignoredInvites.getOrCreateSourceRooms();
await client.joinRoom(NEW_SOURCE_ROOM_ID);
const newSourceRoom = client.getRoom(NEW_SOURCE_ROOM_ID);
// Fetch the list of sources and check that we do not have the new room yet.
const policies = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies).toBeTruthy();
const ignoreInvites = policies[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites).toBeTruthy();
expect(ignoreInvites.sources).toBeTruthy();
expect(ignoreInvites.sources).not.toContain(NEW_SOURCE_ROOM_ID);
// Add a source.
const added = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added).toBe(true);
const added2 = await client.ignoredInvites.addSource(NEW_SOURCE_ROOM_ID);
expect(added2).toBe(false);
// Fetch the list of sources and check that we have added the new room.
const policies2 = await client.getAccountData(POLICIES_ACCOUNT_EVENT_TYPE.name).getContent();
expect(policies2).toBeTruthy();
const ignoreInvites2 = policies2[IGNORE_INVITES_ACCOUNT_EVENT_KEY.name];
expect(ignoreInvites2).toBeTruthy();
expect(ignoreInvites2.sources).toBeTruthy();
expect(ignoreInvites2.sources).toContain(NEW_SOURCE_ROOM_ID);
// Add a rule.
const eventId = await client.ignoredInvites.addRule(PolicyScope.User, "*:example.org", "just a test");
// Check where it shows up.
const targetRoomId = ignoreInvites2.target;
const targetRoom = client.getRoom(targetRoomId);
expect(targetRoom._state.get(PolicyScope.User)[eventId]).toBeTruthy();
expect(newSourceRoom._state.get(PolicyScope.User)?.[eventId]).toBeFalsy();
});
});
});

View File

@@ -1,12 +1,29 @@
/*
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 * as utils from "../test-utils/test-utils";
import { RoomMember } from "../../src/models/room-member";
import { RoomMember, RoomMemberEvent } from "../../src/models/room-member";
import { RoomState } from "../../src";
describe("RoomMember", function() {
const roomId = "!foo:bar";
const userA = "@alice:bar";
const userB = "@bertha:bar";
const userC = "@clarissa:bar";
let member;
let member = new RoomMember(roomId, userA);
beforeEach(function() {
member = new RoomMember(roomId, userA);
@@ -27,17 +44,17 @@ describe("RoomMember", function() {
avatar_url: "mxc://flibble/wibble",
},
});
const url = member.getAvatarUrl(hsUrl);
const url = member.getAvatarUrl(hsUrl, 1, 1, '', false, false);
// we don't care about how the mxc->http conversion is done, other
// than it contains the mxc body.
expect(url.indexOf("flibble/wibble")).not.toEqual(-1);
expect(url?.indexOf("flibble/wibble")).not.toEqual(-1);
});
it("should return nothing if there is no m.room.member and allowDefault=false",
function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false);
expect(url).toEqual(null);
});
function() {
const url = member.getAvatarUrl(hsUrl, 64, 64, "crop", false, false);
expect(url).toEqual(null);
});
});
describe("setPowerLevelEvent", function() {
@@ -66,92 +83,92 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.powerLevel' if the power level changes.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10, // shouldn't barf on this.
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@bertha:bar": 200,
"@invalid:user": 10, // shouldn't barf on this.
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event);
});
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember).toEqual(member);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(emitCount).toEqual(1);
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(emitCount).toEqual(1);
member.setPowerLevelEvent(event); // no-op
expect(emitCount).toEqual(1);
});
it("should honour power levels of zero.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": 0,
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
// set the power level to something other than zero or we
// won't get an event
member.powerLevel = 1;
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(0);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(0);
expect(emitCount).toEqual(1);
});
it("should not honor string power levels.",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
function() {
const event = utils.mkEvent({
type: "m.room.power_levels",
room: roomId,
user: userA,
content: {
users_default: 20,
users: {
"@alice:bar": "5",
},
},
},
event: true,
});
let emitCount = 0;
event: true,
});
let emitCount = 0;
member.on("RoomMember.powerLevel", function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.on(RoomMemberEvent.PowerLevel, function(emitEvent, emitMember) {
emitCount += 1;
expect(emitMember.userId).toEqual('@alice:bar');
expect(emitMember.powerLevel).toEqual(20);
expect(emitEvent).toEqual(event);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
member.setPowerLevelEvent(event);
expect(member.powerLevel).toEqual(20);
expect(emitCount).toEqual(1);
});
});
describe("setTypingEvent", function() {
@@ -183,34 +200,34 @@ describe("RoomMember", function() {
});
it("should emit 'RoomMember.typing' if the typing state changes",
function() {
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC,
],
},
event: true,
function() {
const event = utils.mkEvent({
type: "m.typing",
room: roomId,
content: {
user_ids: [
userA, userC,
],
},
event: true,
});
let emitCount = 0;
member.on(RoomMemberEvent.Typing, function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
emitCount += 1;
});
member.typing = false;
member.setTypingEvent(event);
expect(emitCount).toEqual(1);
member.setTypingEvent(event); // no-op
expect(emitCount).toEqual(1);
});
let emitCount = 0;
member.on("RoomMember.typing", function(ev, mem) {
expect(mem).toEqual(member);
expect(ev).toEqual(event);
emitCount += 1;
});
member.typing = false;
member.setTypingEvent(event);
expect(emitCount).toEqual(1);
member.setTypingEvent(event); // no-op
expect(emitCount).toEqual(1);
});
});
describe("isOutOfBand", function() {
it("should be set by markOutOfBand", function() {
const member = new RoomMember();
const member = new RoomMember(roomId, userA);
expect(member.isOutOfBand()).toEqual(false);
member.markOutOfBand();
expect(member.isOutOfBand()).toEqual(true);
@@ -235,50 +252,50 @@ describe("RoomMember", function() {
});
it("should set 'membership' and assign the event to 'events.member'.",
function() {
member.setMembershipEvent(inviteEvent);
expect(member.membership).toEqual("invite");
expect(member.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.events.member).toEqual(joinEvent);
});
function() {
member.setMembershipEvent(inviteEvent);
expect(member.membership).toEqual("invite");
expect(member.events.member).toEqual(inviteEvent);
member.setMembershipEvent(joinEvent);
expect(member.membership).toEqual("join");
expect(member.events.member).toEqual(joinEvent);
});
it("should set 'name' based on user_id, displayname and room state",
function() {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
function() {
const roomState = {
getStateEvents: function(type) {
if (type !== "m.room.member") {
return [];
}
return [
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userB,
}),
utils.mkMembership({
event: true, mship: "join", room: roomId,
user: userC, name: "Alice",
}),
joinEvent,
];
},
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent);
expect(member.name).toEqual("Alice"); // prefer displayname
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alice"); // it should disambig.
// user_id should be there somewhere
expect(member.name.indexOf(userA)).not.toEqual(-1);
});
it("should emit 'RoomMember.membership' if the membership changes", function() {
let emitCount = 0;
member.on("RoomMember.membership", function(ev, mem) {
member.on(RoomMemberEvent.Membership, function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(inviteEvent);
@@ -291,7 +308,7 @@ describe("RoomMember", function() {
it("should emit 'RoomMember.name' if the name changes", function() {
let emitCount = 0;
member.on("RoomMember.name", function(ev, mem) {
member.on(RoomMemberEvent.Name, function(ev, mem) {
emitCount += 1;
expect(mem).toEqual(member);
expect(ev).toEqual(joinEvent);
@@ -341,7 +358,7 @@ describe("RoomMember", function() {
getUserIdsWithDisplayName: function(displayName) {
return [userA, userC];
},
};
} as unknown as RoomState;
expect(member.name).toEqual(userA); // default = user_id
member.setMembershipEvent(joinEvent, roomState);
expect(member.name).not.toEqual("Alíce"); // it should disambig.

View File

@@ -1,14 +1,37 @@
/*
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 { MockedObject } from 'jest-mock';
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 { BeaconEvent, getBeaconInfoIdentifier } from "../../src/models/beacon";
import {
Beacon,
BeaconEvent,
getBeaconInfoIdentifier,
} from "../../src/models/beacon";
import { EventType, RelationType, UNSTABLE_MSC2716_MARKER } from "../../src/@types/event";
import {
MatrixEvent,
MatrixEventEvent,
} from "../../src/models/event";
import { M_BEACON } from "../../src/@types/beacon";
import { MatrixClient } from "../../src/client";
describe("RoomState", function() {
const roomId = "!foo:bar";
@@ -17,7 +40,7 @@ describe("RoomState", function() {
const userC = "@cleo:bar";
const userLazy = "@lazy:bar";
let state;
let state = new RoomState(roomId);
beforeEach(function() {
state = new RoomState(roomId);
@@ -67,8 +90,8 @@ describe("RoomState", function() {
it("should return a member which changes as state changes", function() {
const member = state.getMember(userB);
expect(member.membership).toEqual("join");
expect(member.name).toEqual(userB);
expect(member?.membership).toEqual("join");
expect(member?.name).toEqual(userB);
state.setStateEvents([
utils.mkMembership({
@@ -77,40 +100,40 @@ describe("RoomState", function() {
}),
]);
expect(member.membership).toEqual("leave");
expect(member.name).toEqual("BobGone");
expect(member?.membership).toEqual("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");
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: "leave", event: true,
name: "AliceIsGone",
}),
]);
const postLeaveUser = state.getSentinelMember(userA);
function() {
const preLeaveUser = state.getSentinelMember(userA);
state.setStateEvents([
utils.mkMembership({
room: roomId, user: userA, mship: "leave", event: true,
name: "AliceIsGone",
}),
]);
const postLeaveUser = state.getSentinelMember(userA);
expect(preLeaveUser.membership).toEqual("join");
expect(preLeaveUser.name).toEqual(userA);
expect(preLeaveUser?.membership).toEqual("join");
expect(preLeaveUser?.name).toEqual(userA);
expect(postLeaveUser.membership).toEqual("leave");
expect(postLeaveUser.name).toEqual("AliceIsGone");
});
expect(postLeaveUser?.membership).toEqual("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);
});
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() {
@@ -118,21 +141,21 @@ describe("RoomState", function() {
});
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())).not.toEqual(-1);
expect([userA, userB].indexOf(events[1].getStateKey())).not.toEqual(-1);
});
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()).toMatchObject({
membership: "join",
function() {
const event = state.getStateEvents("m.room.member", userA);
expect(event.getContent()).toMatchObject({
membership: "join",
});
});
});
});
describe("setStateEvents", function() {
@@ -146,7 +169,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.members", function(ev, st, mem) {
state.on(RoomStateEvent.Members, function(ev, st, mem) {
expect(ev).toEqual(memberEvents[emitCount]);
expect(st).toEqual(state);
expect(mem).toEqual(state.getMember(ev.getSender()));
@@ -166,7 +189,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.newMember", function(ev, st, mem) {
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
@@ -192,7 +215,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.events", function(ev, st) {
state.on(RoomStateEvent.Events, function(ev, st) {
expect(ev).toEqual(events[emitCount]);
expect(st).toEqual(state);
emitCount += 1;
@@ -272,7 +295,7 @@ describe("RoomState", function() {
}),
];
let emitCount = 0;
state.on("RoomState.Marker", function(markerEvent, markerFoundOptions) {
state.on(RoomStateEvent.Marker, function(markerEvent, markerFoundOptions) {
expect(markerEvent).toEqual(events[emitCount]);
expect(markerFoundOptions).toEqual({ timelineWasEmpty: true });
emitCount += 1;
@@ -296,7 +319,7 @@ describe("RoomState", function() {
it('does not add redacted beacon info events to state', () => {
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId);
const redactionEvent = { event: { type: 'm.room.redaction' } };
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction' });
redactedBeaconEvent.makeRedacted(redactionEvent);
const emitSpy = jest.spyOn(state, 'emit');
@@ -316,27 +339,27 @@ describe("RoomState", function() {
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
expect(beaconInstance.isLive).toEqual(true);
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);
expect(state.beacons.get(getBeaconInfoIdentifier(beaconEvent))?.isLive).toEqual(false);
});
it('destroys and removes redacted beacon events', () => {
const beaconId = '$beacon1';
const beaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactedBeaconEvent = makeBeaconInfoEvent(userA, roomId, { isLive: true }, beaconId);
const redactionEvent = { event: { type: 'm.room.redaction', redacts: beaconEvent.getId() } };
const redactionEvent = new MatrixEvent({ type: 'm.room.redaction', redacts: beaconEvent.getId() });
redactedBeaconEvent.makeRedacted(redactionEvent);
state.setStateEvents([beaconEvent]);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beaconEvent));
const destroySpy = jest.spyOn(beaconInstance, 'destroy');
expect(beaconInstance.isLive).toEqual(true);
const destroySpy = jest.spyOn(beaconInstance as Beacon, 'destroy');
expect(beaconInstance?.isLive).toEqual(true);
state.setStateEvents([redactedBeaconEvent]);
@@ -357,7 +380,7 @@ describe("RoomState", function() {
// live beacon is now not live
const updatedLiveBeaconEvent = makeBeaconInfoEvent(
userA, roomId, { isLive: false }, liveBeaconEvent.getId(), '$beacon1',
userA, roomId, { isLive: false }, liveBeaconEvent.getId(),
);
state.setStateEvents([updatedLiveBeaconEvent]);
@@ -377,8 +400,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const member = state.getMember(userLazy);
expect(member.userId).toEqual(userLazy);
expect(member.isOutOfBand()).toEqual(true);
expect(member?.userId).toEqual(userLazy);
expect(member?.isOutOfBand()).toEqual(true);
});
it("should have no effect when not in correct status", function() {
@@ -394,7 +417,7 @@ describe("RoomState", function() {
user: userLazy, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.newMember', (_, __, member) => {
state.once(RoomStateEvent.NewMember, (_event, _state, member) => {
expect(member.userId).toEqual(userLazy);
eventReceived = true;
});
@@ -410,8 +433,8 @@ describe("RoomState", function() {
state.markOutOfBandMembersStarted();
state.setOutOfBandMembers([oobMemberEvent]);
const memberA = state.getMember(userA);
expect(memberA.events.member.getId()).not.toEqual(oobMemberEvent.getId());
expect(memberA.isOutOfBand()).toEqual(false);
expect(memberA?.events?.member?.getId()).not.toEqual(oobMemberEvent.getId());
expect(memberA?.isOutOfBand()).toEqual(false);
});
it("should emit members when updating a member", function() {
@@ -420,7 +443,7 @@ describe("RoomState", function() {
user: doesntExistYetUserId, mship: "join", room: roomId, event: true,
});
let eventReceived = false;
state.once('RoomState.members', (_, __, member) => {
state.once(RoomStateEvent.Members, (_event, _state, member) => {
expect(member.userId).toEqual(doesntExistYetUserId);
eventReceived = true;
});
@@ -443,8 +466,8 @@ describe("RoomState", function() {
[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());
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));
@@ -496,78 +519,80 @@ describe("RoomState", function() {
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);
});
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 = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
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,
},
},
};
powerLevelEvent.content.users[userA] = 50;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
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 = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
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,
},
},
},
};
powerLevelEvent.content.users[userA] = 30;
powerLevelEvent.content.users[userB] = 29;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendStateEvent('m.room.name', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.name', userB)).toEqual(false);
});
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 = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 76,
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,
},
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 80;
powerLevelEvent.content.users[userB] = 50;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
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.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);
});
expect(state.maySendStateEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendStateEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe("getJoinedMemberCount", function() {
@@ -682,71 +707,73 @@ describe("RoomState", function() {
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);
});
function() {
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendMessage(userA)).toEqual(true);
});
it("should obey events_default",
function() {
const powerLevelEvent = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
users_default: 10,
state_default: 30,
events_default: 25,
users: {
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,
},
},
},
};
powerLevelEvent.content.users[userA] = 26;
powerLevelEvent.content.users[userB] = 24;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(false);
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);
});
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 = {
type: "m.room.power_levels", room: roomId, user: userA, event: true,
content: {
events: {
"m.room.other_thing": 33,
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,
},
},
users_default: 10,
state_default: 50,
events_default: 25,
users: {
},
},
};
powerLevelEvent.content.users[userA] = 40;
powerLevelEvent.content.users[userB] = 30;
});
state.setStateEvents([utils.mkEvent(powerLevelEvent)]);
state.setStateEvents([powerLevelEvent]);
expect(state.maySendEvent('m.room.message', userA)).toEqual(true);
expect(state.maySendEvent('m.room.message', userB)).toEqual(true);
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.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);
});
expect(state.maySendEvent('m.room.other_thing', userA)).toEqual(true);
expect(state.maySendEvent('m.room.other_thing', userB)).toEqual(false);
});
});
describe('processBeaconEvents', () => {
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1', '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2', '$beacon2');
const beacon1 = makeBeaconInfoEvent(userA, roomId, {}, '$beacon1');
const beacon2 = makeBeaconInfoEvent(userB, roomId, {}, '$beacon2');
const mockClient = { decryptEventIfNeeded: jest.fn() };
const mockClient = { decryptEventIfNeeded: jest.fn() } as unknown as MockedObject<MatrixClient>;
beforeEach(() => {
mockClient.decryptEventIfNeeded.mockClear();
@@ -816,11 +843,11 @@ describe("RoomState", function() {
beaconInfoId: 'some-other-beacon',
});
state.setStateEvents([beacon1, beacon2], mockClient);
state.setStateEvents([beacon1, beacon2]);
expect(state.beacons.size).toEqual(2);
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beaconInstance = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beaconInstance, 'addLocations');
state.processBeaconEvents([location1, location2, location3], mockClient);
@@ -885,7 +912,7 @@ describe("RoomState", function() {
});
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([location, otherRelatedEvent], mockClient);
expect(addLocationsSpy).not.toHaveBeenCalled();
@@ -945,13 +972,13 @@ describe("RoomState", function() {
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// this event is a message after decryption
decryptingRelatedEvent.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
decryptingRelatedEvent.event.type = EventType.RoomMessage;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).not.toHaveBeenCalled();
});
@@ -967,14 +994,14 @@ describe("RoomState", function() {
});
jest.spyOn(decryptingRelatedEvent, 'isBeingDecrypted').mockReturnValue(true);
state.setStateEvents([beacon1, beacon2]);
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1));
const beacon = state.beacons.get(getBeaconInfoIdentifier(beacon1)) as Beacon;
const addLocationsSpy = jest.spyOn(beacon, 'addLocations').mockClear();
state.processBeaconEvents([decryptingRelatedEvent], mockClient);
// update type after '''decryption'''
decryptingRelatedEvent.event.type = M_BEACON.name;
decryptingRelatedEvent.event.content = locationEvent.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted);
decryptingRelatedEvent.event.content = locationEvent.event.content;
decryptingRelatedEvent.emit(MatrixEventEvent.Decrypted, decryptingRelatedEvent);
expect(addLocationsSpy).toHaveBeenCalledWith([decryptingRelatedEvent]);
});

View File

@@ -288,11 +288,11 @@ describe("Room", function() {
room.addLiveEvents(events);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[0]],
{ timelineWasEmpty: undefined },
{ timelineWasEmpty: false },
);
expect(room.currentState.setStateEvents).toHaveBeenCalledWith(
[events[1]],
{ timelineWasEmpty: undefined },
{ timelineWasEmpty: false },
);
expect(events[0].forwardLooking).toBe(true);
expect(events[1].forwardLooking).toBe(true);
@@ -426,6 +426,17 @@ describe("Room", function() {
// but without the event ID matching we will still have the local event in pending events
expect(room.getEventForTxnId(txnId)).toBeUndefined();
});
it("should correctly handle remote echoes from other devices", () => {
const remoteEvent = utils.mkMessage({
room: roomId, user: userA, event: true,
});
remoteEvent.event.unsigned = { transaction_id: "TXN_ID" };
// add the remoteEvent
room.addLiveEvents([remoteEvent]);
expect(room.timeline.length).toEqual(1);
});
});
describe('addEphemeralEvents', () => {

View File

@@ -152,6 +152,9 @@ describe("utils", function() {
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
assert.isTrue(utils.deepCompare({
1: { name: "mhc", age: 28 },