You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-08-07 23:02:56 +03:00
Merge branch 'develop' into madlittlemods/stablize-msc3030-timestamp-to-event
This commit is contained in:
@@ -24,7 +24,7 @@ import MockHttpBackend from "matrix-mock-request";
|
||||
import { LocalStorageCryptoStore } from "../src/crypto/store/localStorage-crypto-store";
|
||||
import { logger } from "../src/logger";
|
||||
import { syncPromise } from "./test-utils/test-utils";
|
||||
import { createClient } from "../src/matrix";
|
||||
import { createClient, IStartClientOpts } from "../src/matrix";
|
||||
import { ICreateClientOpts, IDownloadKeyResult, MatrixClient, PendingEventOrdering } from "../src/client";
|
||||
import { MockStorageApi } from "./MockStorageApi";
|
||||
import { encodeUri } from "../src/utils";
|
||||
@@ -79,9 +79,12 @@ export class TestClient {
|
||||
/**
|
||||
* start the client, and wait for it to initialise.
|
||||
*/
|
||||
public start(): Promise<void> {
|
||||
public start(opts: IStartClientOpts = {}): Promise<void> {
|
||||
logger.log(this + ": starting");
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {});
|
||||
this.httpBackend.when("GET", "/versions").respond(200, {
|
||||
// we have tests that rely on support for lazy-loading members
|
||||
versions: ["r0.5.0"],
|
||||
});
|
||||
this.httpBackend.when("GET", "/pushrules").respond(200, {});
|
||||
this.httpBackend.when("POST", "/filter").respond(200, { filter_id: "fid" });
|
||||
this.expectDeviceKeyUpload();
|
||||
@@ -93,6 +96,8 @@ export class TestClient {
|
||||
this.client.startClient({
|
||||
// set this so that we can get hold of failed events
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
|
||||
...opts,
|
||||
});
|
||||
|
||||
return Promise.all([this.httpBackend.flushAllExpected(), syncPromise(this.client)]).then(() => {
|
||||
|
@@ -1016,6 +1016,61 @@ describe("MatrixClient event timelines", function () {
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("should create threads for thread roots discovered", function () {
|
||||
const room = client.getRoom(roomId)!;
|
||||
const timelineSet = room.getTimelineSets()[0];
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/context/" + encodeURIComponent(EVENTS[0].event_id!))
|
||||
.respond(200, function () {
|
||||
return {
|
||||
start: "start_token0",
|
||||
events_before: [],
|
||||
event: EVENTS[0],
|
||||
events_after: [],
|
||||
end: "end_token0",
|
||||
state: [],
|
||||
};
|
||||
});
|
||||
|
||||
httpBackend
|
||||
.when("GET", "/rooms/!foo%3Abar/messages")
|
||||
.check(function (req) {
|
||||
const params = req.queryParams!;
|
||||
expect(params.dir).toEqual("b");
|
||||
expect(params.from).toEqual("start_token0");
|
||||
expect(params.limit).toEqual("30");
|
||||
})
|
||||
.respond(200, function () {
|
||||
return {
|
||||
chunk: [EVENTS[1], EVENTS[2], THREAD_ROOT],
|
||||
end: "start_token1",
|
||||
};
|
||||
});
|
||||
|
||||
let tl: EventTimeline;
|
||||
return Promise.all([
|
||||
client
|
||||
.getEventTimeline(timelineSet, EVENTS[0].event_id!)
|
||||
.then(function (tl0) {
|
||||
tl = tl0!;
|
||||
return client.paginateEventTimeline(tl, { backwards: true });
|
||||
})
|
||||
.then(function (success) {
|
||||
expect(success).toBeTruthy();
|
||||
expect(tl!.getEvents().length).toEqual(4);
|
||||
expect(tl!.getEvents()[0].event).toEqual(THREAD_ROOT);
|
||||
expect(tl!.getEvents()[1].event).toEqual(EVENTS[2]);
|
||||
expect(tl!.getEvents()[2].event).toEqual(EVENTS[1]);
|
||||
expect(tl!.getEvents()[3].event).toEqual(EVENTS[0]);
|
||||
expect(room.getThreads().map((it) => it.id)).toEqual([THREAD_ROOT.event_id!]);
|
||||
expect(tl!.getPaginationToken(EventTimeline.BACKWARDS)).toEqual("start_token1");
|
||||
expect(tl!.getPaginationToken(EventTimeline.FORWARDS)).toEqual("end_token0");
|
||||
}),
|
||||
httpBackend.flushAllExpected(),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should ensure thread events are ordered correctly", async () => {
|
||||
|
@@ -35,9 +35,7 @@ describe("MatrixClient", function () {
|
||||
let store: MemoryStore | undefined;
|
||||
|
||||
const defaultClientOpts: IStoredClientOpts = {
|
||||
canResetEntireTimeline: (roomId) => false,
|
||||
experimentalThreadSupport: false,
|
||||
crypto: {} as unknown as IStoredClientOpts["crypto"],
|
||||
};
|
||||
const setupTests = (): [MatrixClient, HttpBackend, MemoryStore] => {
|
||||
const store = new MemoryStore();
|
||||
|
@@ -1543,6 +1543,52 @@ describe("MatrixClient syncing", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("only replays receipts relevant to the current context", async () => {
|
||||
const THREAD_ID = "$unknownthread:localhost";
|
||||
|
||||
const receipt = {
|
||||
type: "m.receipt",
|
||||
room_id: "!foo:bar",
|
||||
content: {
|
||||
"$event1:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 666, thread_id: THREAD_ID },
|
||||
},
|
||||
},
|
||||
"$otherevent:localhost": {
|
||||
[ReceiptType.Read]: {
|
||||
"@alice:localhost": { ts: 999, thread_id: "$otherthread:localhost" },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
syncData.rooms.join[roomOne].ephemeral.events = [receipt];
|
||||
|
||||
httpBackend!.when("GET", "/sync").respond(200, syncData);
|
||||
client!.startClient();
|
||||
|
||||
return Promise.all([httpBackend!.flushAllExpected(), awaitSyncEvent()]).then(() => {
|
||||
const room = client?.getRoom(roomOne);
|
||||
expect(room).toBeInstanceOf(Room);
|
||||
|
||||
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(true);
|
||||
|
||||
const thread = room!.createThread(THREAD_ID, undefined, [], true);
|
||||
|
||||
expect(room?.cachedThreadReadReceipts.has(THREAD_ID)).toBe(false);
|
||||
|
||||
const receipt = thread.getReadReceiptForUserId("@alice:localhost");
|
||||
|
||||
expect(receipt).toStrictEqual({
|
||||
data: {
|
||||
thread_id: "$unknownthread:localhost",
|
||||
ts: 666,
|
||||
},
|
||||
eventId: "$event1:localhost",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("of a room", () => {
|
||||
|
@@ -1590,4 +1590,92 @@ describe("megolm", () => {
|
||||
aliceTestClient.httpBackend.flush("/send/m.room.encrypted/", 1),
|
||||
]);
|
||||
});
|
||||
|
||||
describe("Lazy-loading member lists", () => {
|
||||
let p2pSession: Olm.Session;
|
||||
|
||||
beforeEach(async () => {
|
||||
// set up the aliceTestClient so that it is a room with no known members
|
||||
aliceTestClient.expectKeyQuery({ device_keys: { "@alice:localhost": {} }, failures: {} });
|
||||
await aliceTestClient.start({ lazyLoadMembers: true });
|
||||
aliceTestClient.client.setGlobalErrorOnUnknownDevices(false);
|
||||
|
||||
aliceTestClient.httpBackend.when("GET", "/sync").respond(200, getSyncResponse([]));
|
||||
await aliceTestClient.flushSync();
|
||||
|
||||
p2pSession = await establishOlmSession(aliceTestClient, testOlmAccount);
|
||||
});
|
||||
|
||||
async function expectMembershipRequest(roomId: string, members: string[]): Promise<void> {
|
||||
const membersPath = `/rooms/${encodeURIComponent(roomId)}/members?not_membership=leave`;
|
||||
aliceTestClient.httpBackend.when("GET", membersPath).respond(200, {
|
||||
chunk: [
|
||||
testUtils.mkMembershipCustom({
|
||||
membership: "join",
|
||||
sender: "@bob:xyz",
|
||||
}),
|
||||
],
|
||||
});
|
||||
await aliceTestClient.httpBackend.flush(membersPath, 1);
|
||||
}
|
||||
|
||||
it("Sending an event initiates a member list sync", async () => {
|
||||
// we expect a call to the /members list...
|
||||
const memberListPromise = expectMembershipRequest(ROOM_ID, ["@bob:xyz"]);
|
||||
|
||||
// then a request for bob's devices...
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// then a to-device with the room_key
|
||||
const inboundGroupSessionPromise = expectSendRoomKey(
|
||||
aliceTestClient.httpBackend,
|
||||
"@bob:xyz",
|
||||
testOlmAccount,
|
||||
p2pSession,
|
||||
);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(
|
||||
aliceTestClient.httpBackend,
|
||||
inboundGroupSessionPromise,
|
||||
);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
|
||||
|
||||
await Promise.all([
|
||||
sendPromise,
|
||||
megolmMessagePromise,
|
||||
memberListPromise,
|
||||
aliceTestClient.httpBackend.flush("/keys/query", 1),
|
||||
]);
|
||||
});
|
||||
|
||||
it("loading the membership list inhibits a later load", async () => {
|
||||
const room = aliceTestClient.client.getRoom(ROOM_ID)!;
|
||||
await Promise.all([room.loadMembersIfNeeded(), expectMembershipRequest(ROOM_ID, ["@bob:xyz"])]);
|
||||
|
||||
// expect a request for bob's devices...
|
||||
aliceTestClient.httpBackend.when("POST", "/keys/query").respond(200, getTestKeysQueryResponse("@bob:xyz"));
|
||||
|
||||
// then a to-device with the room_key
|
||||
const inboundGroupSessionPromise = expectSendRoomKey(
|
||||
aliceTestClient.httpBackend,
|
||||
"@bob:xyz",
|
||||
testOlmAccount,
|
||||
p2pSession,
|
||||
);
|
||||
|
||||
// and finally the megolm message
|
||||
const megolmMessagePromise = expectSendMegolmMessage(
|
||||
aliceTestClient.httpBackend,
|
||||
inboundGroupSessionPromise,
|
||||
);
|
||||
|
||||
// kick it off
|
||||
const sendPromise = aliceTestClient.client.sendTextMessage(ROOM_ID, "test");
|
||||
|
||||
await Promise.all([sendPromise, megolmMessagePromise, aliceTestClient.httpBackend.flush("/keys/query", 1)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -38,7 +38,7 @@ import {
|
||||
IRoomTimelineData,
|
||||
} from "../../src";
|
||||
import { SlidingSyncSdk } from "../../src/sliding-sync-sdk";
|
||||
import { SyncState } from "../../src/sync";
|
||||
import { SyncApiOptions, SyncState } from "../../src/sync";
|
||||
import { IStoredClientOpts } from "../../src/client";
|
||||
import { logger } from "../../src/logger";
|
||||
import { emitPromise } from "../test-utils/test-utils";
|
||||
@@ -111,6 +111,7 @@ describe("SlidingSyncSdk", () => {
|
||||
// assign client/httpBackend globals
|
||||
const setupClient = async (testOpts?: Partial<IStoredClientOpts & { withCrypto: boolean }>) => {
|
||||
testOpts = testOpts || {};
|
||||
const syncOpts: SyncApiOptions = {};
|
||||
const testClient = new TestClient(selfUserId, "DEVICE", selfAccessToken);
|
||||
httpBackend = testClient.httpBackend;
|
||||
client = testClient.client;
|
||||
@@ -118,10 +119,10 @@ describe("SlidingSyncSdk", () => {
|
||||
if (testOpts.withCrypto) {
|
||||
httpBackend!.when("GET", "/room_keys/version").respond(404, {});
|
||||
await client!.initCrypto();
|
||||
testOpts.crypto = client!.crypto;
|
||||
syncOpts.cryptoCallbacks = syncOpts.crypto = client!.crypto;
|
||||
}
|
||||
httpBackend!.when("GET", "/_matrix/client/r0/pushrules").respond(200, {});
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts);
|
||||
sdk = new SlidingSyncSdk(mockSlidingSync, client, testOpts, syncOpts);
|
||||
};
|
||||
|
||||
// tear down client/httpBackend globals
|
||||
|
@@ -1418,6 +1418,102 @@ describe("SlidingSync", () => {
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should not be possible to add/modify an already added custom subscription", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
|
||||
it("should change the custom subscription if they are different", async () => {
|
||||
const slidingSync = new SlidingSync(proxyBaseUrl, [], defaultSub, client!, 1);
|
||||
slidingSync.addCustomSubscription(customSubName1, customSub1);
|
||||
slidingSync.addCustomSubscription(customSubName2, customSub2);
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub1);
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// using the same subscription doesn't unsub nor changes subscriptions
|
||||
slidingSync.useCustomSubscription(roomA, customSubName1);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeUndefined();
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
|
||||
// Changing the subscription works
|
||||
slidingSync.useCustomSubscription(roomA, customSubName2);
|
||||
slidingSync.modifyRoomSubscriptions(new Set<string>([roomA]));
|
||||
|
||||
httpBackend!
|
||||
.when("POST", syncUrl)
|
||||
.check(function (req) {
|
||||
const body = req.data;
|
||||
logger.log("custom subs", body);
|
||||
expect(body.room_subscriptions).toBeTruthy();
|
||||
expect(body.room_subscriptions[roomA]).toEqual(customSub2);
|
||||
expect(body.unsubscribe_rooms).toBeUndefined();
|
||||
})
|
||||
.respond(200, {
|
||||
pos: "b",
|
||||
lists: [],
|
||||
extensions: {},
|
||||
rooms: {},
|
||||
});
|
||||
slidingSync.start();
|
||||
await httpBackend!.flushAllExpected();
|
||||
slidingSync.stop();
|
||||
});
|
||||
});
|
||||
|
||||
describe("extensions", () => {
|
||||
|
@@ -513,9 +513,6 @@ export class MockMatrixCall extends TypedEventEmitter<CallEvent, CallEventHandle
|
||||
|
||||
public sendMetadataUpdate = jest.fn<void, []>();
|
||||
|
||||
public on = jest.fn();
|
||||
public removeListener = jest.fn();
|
||||
|
||||
public getOpponentMember(): Partial<RoomMember> {
|
||||
return this.opponentMember;
|
||||
}
|
||||
|
@@ -544,7 +544,7 @@ describe("AutoDiscovery", function () {
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(404, {});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(404, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -591,7 +591,7 @@ describe("AutoDiscovery", function () {
|
||||
.respond(200, {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend.when("GET", "/_matrix/identity/api/v1").respond(500, {});
|
||||
httpBackend.when("GET", "/_matrix/identity/v2").respond(500, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
"m.homeserver": {
|
||||
// Note: we also expect this test to trim the trailing slash
|
||||
@@ -636,9 +636,9 @@ describe("AutoDiscovery", function () {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/api/v1")
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
@@ -682,9 +682,9 @@ describe("AutoDiscovery", function () {
|
||||
versions: ["r0.0.1"],
|
||||
});
|
||||
httpBackend
|
||||
.when("GET", "/_matrix/identity/api/v1")
|
||||
.when("GET", "/_matrix/identity/v2")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/api/v1");
|
||||
expect(req.path).toEqual("https://identity.example.org/_matrix/identity/v2");
|
||||
})
|
||||
.respond(200, {});
|
||||
httpBackend.when("GET", "/.well-known/matrix/client").respond(200, {
|
||||
|
@@ -167,6 +167,38 @@ describe("Crypto", function () {
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
|
||||
it("doesn't throw an error when attempting to decrypt a redacted event", async () => {
|
||||
const client = new TestClient("@alice:example.com", "deviceid").client;
|
||||
await client.initCrypto();
|
||||
|
||||
const event = new MatrixEvent({
|
||||
content: {},
|
||||
event_id: "$event_id",
|
||||
room_id: "!room_id",
|
||||
sender: "@bob:example.com",
|
||||
type: "m.room.encrypted",
|
||||
unsigned: {
|
||||
redacted_because: {
|
||||
content: {},
|
||||
event_id: "$redaction_event_id",
|
||||
redacts: "$event_id",
|
||||
room_id: "!room_id",
|
||||
origin_server_ts: 1234567890,
|
||||
sender: "@bob:example.com",
|
||||
type: "m.room.redaction",
|
||||
unsigned: {},
|
||||
},
|
||||
},
|
||||
});
|
||||
await event.attemptDecryption(client.crypto!);
|
||||
expect(event.isDecryptionFailure()).toBeFalsy();
|
||||
// since the redaction event isn't encrypted, the redacted_because
|
||||
// should be the same as in the original event
|
||||
expect(event.getRedactionEvent()).toEqual(event.getUnsigned().redacted_because);
|
||||
|
||||
client.stopClient();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Session management", function () {
|
||||
|
@@ -1,56 +0,0 @@
|
||||
/*
|
||||
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 "../../../olm-loader";
|
||||
|
||||
import { CRYPTO_ENABLED, MatrixClient } from "../../../../src/client";
|
||||
import { TestClient } from "../../../TestClient";
|
||||
|
||||
const Olm = global.Olm;
|
||||
|
||||
describe("crypto.setDeviceVerification", () => {
|
||||
const userId = "@alice:example.com";
|
||||
const deviceId1 = "device1";
|
||||
let client: MatrixClient;
|
||||
|
||||
if (!CRYPTO_ENABLED) {
|
||||
return;
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
await Olm.init();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
client = new TestClient(userId, deviceId1).client;
|
||||
await client.initCrypto();
|
||||
});
|
||||
|
||||
it("client should provide crypto", () => {
|
||||
expect(client.crypto).not.toBeUndefined();
|
||||
});
|
||||
|
||||
describe("when setting an own device as verified", () => {
|
||||
beforeEach(async () => {
|
||||
jest.spyOn(client.crypto!, "cancelAndResendAllOutgoingKeyRequests");
|
||||
await client.crypto!.setDeviceVerification(userId, deviceId1, true);
|
||||
});
|
||||
|
||||
it("cancelAndResendAllOutgoingKeyRequests should be called", () => {
|
||||
expect(client.crypto!.cancelAndResendAllOutgoingKeyRequests).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
@@ -24,10 +24,13 @@ import {
|
||||
MatrixClient,
|
||||
MatrixEvent,
|
||||
MatrixEventEvent,
|
||||
RelationType,
|
||||
Room,
|
||||
RoomEvent,
|
||||
} from "../../src";
|
||||
import { Thread } from "../../src/models/thread";
|
||||
import { FeatureSupport, Thread } from "../../src/models/thread";
|
||||
import { ReEmitter } from "../../src/ReEmitter";
|
||||
import { eventMapperFor } from "../../src/event-mapper";
|
||||
|
||||
describe("EventTimelineSet", () => {
|
||||
const roomId = "!foo:bar";
|
||||
@@ -202,6 +205,88 @@ describe("EventTimelineSet", () => {
|
||||
expect(liveTimeline.getEvents().length).toStrictEqual(0);
|
||||
});
|
||||
|
||||
it("should allow edits to be added to thread timeline", async () => {
|
||||
jest.spyOn(client, "supportsExperimentalThreads").mockReturnValue(true);
|
||||
jest.spyOn(client, "getEventMapper").mockReturnValue(eventMapperFor(client, {}));
|
||||
Thread.hasServerSideSupport = FeatureSupport.Stable;
|
||||
|
||||
const sender = "@alice:matrix.org";
|
||||
|
||||
const root = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
body: "Thread root",
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
room.addLiveEvents([root]);
|
||||
|
||||
const threadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": "Thread reply",
|
||||
"m.relates_to": {
|
||||
event_id: root.getId()!,
|
||||
rel_type: RelationType.Thread,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
root.setUnsigned({
|
||||
"m.relations": {
|
||||
[RelationType.Thread]: {
|
||||
count: 1,
|
||||
latest_event: {
|
||||
content: threadReply.getContent(),
|
||||
origin_server_ts: 5,
|
||||
room_id: room.roomId,
|
||||
sender,
|
||||
type: EventType.RoomMessage,
|
||||
event_id: threadReply.getId()!,
|
||||
user_id: sender,
|
||||
age: 1,
|
||||
},
|
||||
current_user_participated: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const editToThreadReply = utils.mkEvent({
|
||||
event: true,
|
||||
content: {
|
||||
"body": " * edit",
|
||||
"m.new_content": {
|
||||
"body": "edit",
|
||||
"msgtype": "m.text",
|
||||
"org.matrix.msc1767.text": "edit",
|
||||
},
|
||||
"m.relates_to": {
|
||||
event_id: threadReply.getId()!,
|
||||
rel_type: RelationType.Replace,
|
||||
},
|
||||
},
|
||||
type: EventType.RoomMessage,
|
||||
sender,
|
||||
});
|
||||
|
||||
jest.spyOn(client, "paginateEventTimeline").mockImplementation(async () => {
|
||||
thread.timelineSet.getLiveTimeline().addEvent(threadReply, { toStartOfTimeline: true });
|
||||
return true;
|
||||
});
|
||||
jest.spyOn(client, "relations").mockResolvedValue({
|
||||
events: [],
|
||||
});
|
||||
|
||||
const thread = room.createThread(root.getId()!, root, [threadReply, editToThreadReply], false);
|
||||
thread.once(RoomEvent.TimelineReset, () => {
|
||||
const lastEvent = thread.timeline.at(-1)!;
|
||||
expect(lastEvent.getContent().body).toBe(" * edit");
|
||||
});
|
||||
});
|
||||
|
||||
describe("non-room timeline", () => {
|
||||
it("Adds event to timeline", () => {
|
||||
const nonRoomEventTimelineSet = new EventTimelineSet(
|
||||
|
@@ -22,11 +22,13 @@ import { Filter } from "../../src/filter";
|
||||
import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace";
|
||||
import {
|
||||
EventType,
|
||||
RelationType,
|
||||
RoomCreateTypeField,
|
||||
RoomType,
|
||||
UNSTABLE_MSC3088_ENABLED,
|
||||
UNSTABLE_MSC3088_PURPOSE,
|
||||
UNSTABLE_MSC3089_TREE_SUBTYPE,
|
||||
MSC3912_RELATION_BASED_REDACTIONS_PROP,
|
||||
} from "../../src/@types/event";
|
||||
import { MEGOLM_ALGORITHM } from "../../src/crypto/olmlib";
|
||||
import { Crypto } from "../../src/crypto";
|
||||
@@ -170,6 +172,10 @@ describe("MatrixClient", function () {
|
||||
data: SYNC_DATA,
|
||||
};
|
||||
|
||||
const unstableFeatures: Record<string, boolean> = {
|
||||
"org.matrix.msc3440.stable": true,
|
||||
};
|
||||
|
||||
// items are popped off when processed and block if no items left.
|
||||
let httpLookups: HttpLookup[] = [];
|
||||
let acceptKeepalives: boolean;
|
||||
@@ -188,9 +194,7 @@ describe("MatrixClient", function () {
|
||||
const { prefix } = requestOpts;
|
||||
if (path === KEEP_ALIVE_PATH && acceptKeepalives) {
|
||||
return Promise.resolve({
|
||||
unstable_features: {
|
||||
"org.matrix.msc3440.stable": true,
|
||||
},
|
||||
unstable_features: unstableFeatures,
|
||||
versions: ["r0.6.0", "r0.6.1"],
|
||||
});
|
||||
}
|
||||
@@ -1314,6 +1318,59 @@ describe("MatrixClient", function () {
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId, { reason });
|
||||
});
|
||||
|
||||
describe("when calling with with_relations", () => {
|
||||
const eventId = "$event42:example.org";
|
||||
|
||||
it("should raise an error if server has no support for relation based redactions", async () => {
|
||||
// load supported features
|
||||
await client.getVersions();
|
||||
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
expect(() => {
|
||||
client.redactEvent(roomId, eventId, txnId, {
|
||||
with_relations: [RelationType.Reference],
|
||||
});
|
||||
}).toThrowError(
|
||||
new Error(
|
||||
"Server does not support relation based redactions " +
|
||||
`roomId ${roomId} eventId ${eventId} txnId: ${txnId} threadId null`,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
describe("and the server supports relation based redactions (unstable)", () => {
|
||||
beforeEach(async () => {
|
||||
unstableFeatures["org.matrix.msc3912"] = true;
|
||||
// load supported features
|
||||
await client.getVersions();
|
||||
});
|
||||
|
||||
it("should send with_relations in the request body", async () => {
|
||||
const txnId = client.makeTxnId();
|
||||
|
||||
httpLookups = [
|
||||
{
|
||||
method: "PUT",
|
||||
path:
|
||||
`/rooms/${encodeURIComponent(roomId)}/redact/${encodeURIComponent(eventId)}` +
|
||||
`/${encodeURIComponent(txnId)}`,
|
||||
expectBody: {
|
||||
reason: "redaction test",
|
||||
[MSC3912_RELATION_BASED_REDACTIONS_PROP.unstable!]: [RelationType.Reference],
|
||||
},
|
||||
data: { event_id: eventId },
|
||||
},
|
||||
];
|
||||
|
||||
await client.redactEvent(roomId, eventId, txnId, {
|
||||
reason: "redaction test",
|
||||
with_relations: [RelationType.Reference],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("cancelPendingEvent", () => {
|
||||
|
@@ -16,7 +16,6 @@ limitations under the License.
|
||||
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../../src/models/event";
|
||||
import { emitPromise } from "../../test-utils/test-utils";
|
||||
import { EventType } from "../../../src";
|
||||
import { Crypto } from "../../../src/crypto";
|
||||
|
||||
describe("MatrixEvent", () => {
|
||||
@@ -88,22 +87,6 @@ describe("MatrixEvent", () => {
|
||||
expect(ev.getWireContent().ciphertext).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should abort decryption if fails with an error other than a DecryptionError", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
type: EventType.RoomMessageEncrypted,
|
||||
content: {
|
||||
body: "Test",
|
||||
},
|
||||
event_id: "$event1:server",
|
||||
});
|
||||
await ev.attemptDecryption({
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("Not a DecryptionError")),
|
||||
} as unknown as Crypto);
|
||||
expect(ev.isEncrypted()).toBeTruthy();
|
||||
expect(ev.isBeingDecrypted()).toBeFalsy();
|
||||
expect(ev.isDecryptionFailure()).toBeFalsy();
|
||||
});
|
||||
|
||||
describe("applyVisibilityEvent", () => {
|
||||
it("should emit VisibilityChange if a change was made", async () => {
|
||||
const ev = new MatrixEvent({
|
||||
@@ -134,6 +117,21 @@ describe("MatrixEvent", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should report decryption errors", async () => {
|
||||
const crypto = {
|
||||
decryptEvent: jest.fn().mockRejectedValue(new Error("test error")),
|
||||
} as unknown as Crypto;
|
||||
|
||||
await encryptedEvent.attemptDecryption(crypto);
|
||||
expect(encryptedEvent.isEncrypted()).toBeTruthy();
|
||||
expect(encryptedEvent.isBeingDecrypted()).toBeFalsy();
|
||||
expect(encryptedEvent.isDecryptionFailure()).toBeTruthy();
|
||||
expect(encryptedEvent.getContent()).toEqual({
|
||||
msgtype: "m.bad.encrypted",
|
||||
body: "** Unable to decrypt: Error: test error **",
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry decryption if a retry is queued", async () => {
|
||||
const eventAttemptDecryptionSpy = jest.spyOn(encryptedEvent, "attemptDecryption");
|
||||
|
||||
|
@@ -14,13 +14,21 @@ See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { M_POLL_START } from "matrix-events-sdk";
|
||||
|
||||
import { EventTimelineSet } from "../../src/models/event-timeline-set";
|
||||
import { MatrixEvent, MatrixEventEvent } from "../../src/models/event";
|
||||
import { Room } from "../../src/models/room";
|
||||
import { Relations } from "../../src/models/relations";
|
||||
import { Relations, RelationsEvent } from "../../src/models/relations";
|
||||
import { TestClient } from "../TestClient";
|
||||
import { RelationType } from "../../src";
|
||||
import { logger } from "../../src/logger";
|
||||
|
||||
describe("Relations", function () {
|
||||
afterEach(() => {
|
||||
jest.spyOn(logger, "error").mockRestore();
|
||||
});
|
||||
|
||||
it("should deduplicate annotations", function () {
|
||||
const room = new Room("room123", null!, null!);
|
||||
const relations = new Relations("m.annotation", "m.reaction", room);
|
||||
@@ -75,6 +83,92 @@ describe("Relations", function () {
|
||||
}
|
||||
});
|
||||
|
||||
describe("addEvent()", () => {
|
||||
const relationType = RelationType.Reference;
|
||||
const eventType = M_POLL_START.stable!;
|
||||
const altEventTypes = [M_POLL_START.unstable!];
|
||||
const room = new Room("room123", null!, null!);
|
||||
|
||||
it("should not add events without a relation", async () => {
|
||||
// dont pollute console
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const relations = new Relations(relationType, eventType, room);
|
||||
const emitSpy = jest.spyOn(relations, "emit");
|
||||
const event = new MatrixEvent({ type: eventType });
|
||||
|
||||
await relations.addEvent(event);
|
||||
expect(logSpy).toHaveBeenCalledWith("Event must have relation info");
|
||||
// event not added
|
||||
expect(relations.getRelations().length).toBe(0);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not add events of incorrect event type", async () => {
|
||||
// dont pollute console
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const relations = new Relations(relationType, eventType, room);
|
||||
const emitSpy = jest.spyOn(relations, "emit");
|
||||
const event = new MatrixEvent({
|
||||
type: "different-event-type",
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
rel_type: relationType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await relations.addEvent(event);
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
|
||||
// event not added
|
||||
expect(relations.getRelations().length).toBe(0);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("adds events that match alt event types", async () => {
|
||||
const relations = new Relations(relationType, eventType, room, altEventTypes);
|
||||
const emitSpy = jest.spyOn(relations, "emit");
|
||||
const event = new MatrixEvent({
|
||||
type: M_POLL_START.unstable!,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
rel_type: relationType,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await relations.addEvent(event);
|
||||
|
||||
// event added
|
||||
expect(relations.getRelations()).toEqual([event]);
|
||||
expect(emitSpy).toHaveBeenCalledWith(RelationsEvent.Add, event);
|
||||
});
|
||||
|
||||
it("should not add events of incorrect relation type", async () => {
|
||||
const logSpy = jest.spyOn(logger, "error").mockImplementation(() => {});
|
||||
const relations = new Relations(relationType, eventType, room);
|
||||
const event = new MatrixEvent({
|
||||
type: eventType,
|
||||
content: {
|
||||
"m.relates_to": {
|
||||
event_id: "$2s4yYpEkVQrPglSCSqB_m6E8vDhWsg0yFNyOJdVIb_o",
|
||||
rel_type: "m.annotation",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await relations.addEvent(event);
|
||||
const emitSpy = jest.spyOn(relations, "emit");
|
||||
|
||||
expect(logSpy).toHaveBeenCalledWith(`Event relation info doesn't match this container`);
|
||||
// event not added
|
||||
expect(relations.getRelations().length).toBe(0);
|
||||
expect(emitSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it("should emit created regardless of ordering", async function () {
|
||||
const targetEvent = new MatrixEvent({
|
||||
sender: "@bob:example.com",
|
||||
|
248
spec/unit/rust-crypto.spec.ts
Normal file
248
spec/unit/rust-crypto.spec.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
/*
|
||||
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 "fake-indexeddb/auto";
|
||||
import { IDBFactory } from "fake-indexeddb";
|
||||
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import {
|
||||
KeysBackupRequest,
|
||||
KeysClaimRequest,
|
||||
KeysQueryRequest,
|
||||
KeysUploadRequest,
|
||||
OlmMachine,
|
||||
SignatureUploadRequest,
|
||||
} from "@matrix-org/matrix-sdk-crypto-js";
|
||||
import { Mocked } from "jest-mock";
|
||||
import MockHttpBackend from "matrix-mock-request";
|
||||
|
||||
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
|
||||
import { initRustCrypto } from "../../src/rust-crypto";
|
||||
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
|
||||
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
|
||||
|
||||
afterEach(() => {
|
||||
// reset fake-indexeddb after each test, to make sure we don't leak connections
|
||||
// cf https://github.com/dumbmatter/fakeIndexedDB#wipingresetting-the-indexeddb-for-a-fresh-state
|
||||
// eslint-disable-next-line no-global-assign
|
||||
indexedDB = new IDBFactory();
|
||||
});
|
||||
|
||||
describe("RustCrypto", () => {
|
||||
const TEST_USER = "@alice:example.com";
|
||||
const TEST_DEVICE_ID = "TEST_DEVICE";
|
||||
|
||||
describe(".exportRoomKeys", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
});
|
||||
|
||||
it("should return a list", async () => {
|
||||
const keys = await rustCrypto.exportRoomKeys();
|
||||
expect(Array.isArray(keys)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("to-device messages", () => {
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockHttpApi = {} as MatrixClient["http"];
|
||||
rustCrypto = (await initRustCrypto(mockHttpApi, TEST_USER, TEST_DEVICE_ID)) as RustCrypto;
|
||||
});
|
||||
|
||||
it("should pass through unencrypted to-device messages", async () => {
|
||||
const inputs: IToDeviceEvent[] = [
|
||||
{ content: { key: "value" }, type: "org.matrix.test", sender: "@alice:example.com" },
|
||||
];
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
|
||||
it("should pass through bad encrypted messages", async () => {
|
||||
const olmMachine: OlmMachine = rustCrypto["olmMachine"];
|
||||
const keys = olmMachine.identityKeys;
|
||||
const inputs: IToDeviceEvent[] = [
|
||||
{
|
||||
type: "m.room.encrypted",
|
||||
content: {
|
||||
algorithm: "m.olm.v1.curve25519-aes-sha2",
|
||||
sender_key: "IlRMeOPX2e0MurIyfWEucYBRVOEEUMrOHqn/8mLqMjA",
|
||||
ciphertext: {
|
||||
[keys.curve25519.toBase64()]: {
|
||||
type: 0,
|
||||
body: "ajyjlghi",
|
||||
},
|
||||
},
|
||||
},
|
||||
sender: "@alice:example.com",
|
||||
},
|
||||
];
|
||||
|
||||
const res = await rustCrypto.preprocessToDeviceMessages(inputs);
|
||||
expect(res).toEqual(inputs);
|
||||
});
|
||||
});
|
||||
|
||||
describe("outgoing requests", () => {
|
||||
/** the RustCrypto implementation under test */
|
||||
let rustCrypto: RustCrypto;
|
||||
|
||||
/** A mock http backend which rustCrypto is connected to */
|
||||
let httpBackend: MockHttpBackend;
|
||||
|
||||
/** a mocked-up OlmMachine which rustCrypto is connected to */
|
||||
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
/** A list of results to be returned from olmMachine.outgoingRequest. Each call will shift a result off
|
||||
* the front of the queue, until it is empty. */
|
||||
let outgoingRequestQueue: Array<Array<any>>;
|
||||
|
||||
/** wait for a call to olmMachine.markRequestAsSent */
|
||||
function awaitCallToMarkAsSent(): Promise<void> {
|
||||
return new Promise((resolve, _reject) => {
|
||||
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
httpBackend = new MockHttpBackend();
|
||||
|
||||
await RustSdkCryptoJs.initAsync();
|
||||
|
||||
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
|
||||
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
|
||||
baseUrl: "https://example.com",
|
||||
prefix: "/_matrix",
|
||||
fetchFn: httpBackend.fetchFn as typeof global.fetch,
|
||||
onlyData: true,
|
||||
});
|
||||
|
||||
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
|
||||
// returns objects from outgoingRequestQueue
|
||||
outgoingRequestQueue = [];
|
||||
olmMachine = {
|
||||
outgoingRequests: jest.fn().mockImplementation(() => {
|
||||
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
|
||||
}),
|
||||
markRequestAsSent: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
|
||||
|
||||
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
|
||||
});
|
||||
|
||||
it("should poll for outgoing messages", () => {
|
||||
rustCrypto.onSyncCompleted({});
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/* simple requests that map directly to the request body */
|
||||
const tests: Array<[any, "POST" | "PUT", string]> = [
|
||||
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
|
||||
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
|
||||
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
|
||||
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
|
||||
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
|
||||
];
|
||||
|
||||
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
|
||||
it(`should handle ${RequestClass.name}s`, async () => {
|
||||
const testBody = '{ "foo": "bar" }';
|
||||
const outgoingRequest = new RequestClass("1234", testBody);
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
const testResponse = '{ "result": 1 }';
|
||||
httpBackend
|
||||
.when(expectedMethod, "/_matrix")
|
||||
.check((req) => {
|
||||
expect(req.path).toEqual(expectedPath);
|
||||
expect(req.rawData).toEqual(testBody);
|
||||
expect(req.headers["Accept"]).toEqual("application/json");
|
||||
expect(req.headers["Content-Type"]).toEqual("application/json");
|
||||
})
|
||||
.respond(200, testResponse, true);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
|
||||
|
||||
const markSentCallPromise = awaitCallToMarkAsSent();
|
||||
await httpBackend.flushAllExpected();
|
||||
|
||||
await markSentCallPromise;
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
});
|
||||
}
|
||||
|
||||
it("does not explode with unknown requests", async () => {
|
||||
const outgoingRequest = { id: "5678", type: 987 };
|
||||
outgoingRequestQueue.push([outgoingRequest]);
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await awaitCallToMarkAsSent();
|
||||
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
|
||||
});
|
||||
|
||||
it("stops looping when stop() is called", async () => {
|
||||
const testResponse = '{ "result": 1 }';
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
|
||||
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
|
||||
}
|
||||
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
|
||||
|
||||
// go a couple of times round the loop
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
|
||||
// a second sync while this is going on shouldn't make any difference
|
||||
rustCrypto.onSyncCompleted({});
|
||||
|
||||
await httpBackend.flush("/_matrix", 1);
|
||||
await awaitCallToMarkAsSent();
|
||||
|
||||
// now stop...
|
||||
rustCrypto.stop();
|
||||
|
||||
// which should (eventually) cause the loop to stop with no further calls to outgoingRequests
|
||||
olmMachine.outgoingRequests.mockReset();
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 100);
|
||||
});
|
||||
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
|
||||
httpBackend.verifyNoOutstandingRequests();
|
||||
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
|
||||
|
||||
// we sent three, so there should be 2 left
|
||||
expect(outgoingRequestQueue.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
@@ -142,6 +142,15 @@ describe("Group Call", function () {
|
||||
} as unknown as RoomMember;
|
||||
});
|
||||
|
||||
it.each(Object.values(GroupCallState).filter((v) => v !== GroupCallState.LocalCallFeedUninitialized))(
|
||||
"throws when initializing local call feed in %s state",
|
||||
async (state: GroupCallState) => {
|
||||
// @ts-ignore
|
||||
groupCall.state = state;
|
||||
await expect(groupCall.initLocalCallFeed()).rejects.toThrowError();
|
||||
},
|
||||
);
|
||||
|
||||
it("does not initialize local call feed, if it already is", async () => {
|
||||
await groupCall.initLocalCallFeed();
|
||||
jest.spyOn(groupCall, "initLocalCallFeed");
|
||||
@@ -308,6 +317,17 @@ describe("Group Call", function () {
|
||||
}
|
||||
});
|
||||
|
||||
describe("hasLocalParticipant()", () => {
|
||||
it("should return false, if we don't have a local participant", () => {
|
||||
expect(groupCall.hasLocalParticipant()).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should return true, if we do have local participant", async () => {
|
||||
await groupCall.enter();
|
||||
expect(groupCall.hasLocalParticipant()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("call feeds changing", () => {
|
||||
let call: MockMatrixCall;
|
||||
const currentFeed = new MockCallFeed(FAKE_USER_ID_1, FAKE_DEVICE_ID_1, new MockMediaStream("current"));
|
||||
@@ -475,7 +495,7 @@ describe("Group Call", function () {
|
||||
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
||||
// @ts-ignore
|
||||
groupCall.calls.set(
|
||||
mockCall.getOpponentMember() as RoomMember,
|
||||
mockCall.getOpponentMember().userId!,
|
||||
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
||||
);
|
||||
|
||||
@@ -501,7 +521,7 @@ describe("Group Call", function () {
|
||||
const mockCall = new MockMatrixCall(FAKE_ROOM_ID, groupCall.groupCallId);
|
||||
// @ts-ignore
|
||||
groupCall.calls.set(
|
||||
mockCall.getOpponentMember() as RoomMember,
|
||||
mockCall.getOpponentMember().userId!,
|
||||
new Map([[mockCall.getOpponentDeviceId()!, mockCall.typed()]]),
|
||||
);
|
||||
|
||||
@@ -663,9 +683,7 @@ describe("Group Call", function () {
|
||||
expect(client1.sendToDevice).toHaveBeenCalled();
|
||||
|
||||
// @ts-ignore
|
||||
const oldCall = groupCall1.calls
|
||||
.get(groupCall1.room.getMember(client2.userId)!)!
|
||||
.get(client2.deviceId)!;
|
||||
const oldCall = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!;
|
||||
oldCall.emit(CallEvent.Hangup, oldCall!);
|
||||
|
||||
client1.sendToDevice.mockClear();
|
||||
@@ -685,9 +703,7 @@ describe("Group Call", function () {
|
||||
let newCall: MatrixCall | undefined;
|
||||
while (
|
||||
// @ts-ignore
|
||||
(newCall = groupCall1.calls
|
||||
.get(groupCall1.room.getMember(client2.userId)!)
|
||||
?.get(client2.deviceId)) === undefined ||
|
||||
(newCall = groupCall1.calls.get(client2.userId)?.get(client2.deviceId)) === undefined ||
|
||||
newCall.peerConn === undefined ||
|
||||
newCall.callId == oldCall.callId
|
||||
) {
|
||||
@@ -730,7 +746,7 @@ describe("Group Call", function () {
|
||||
groupCall1.setLocalVideoMuted(false);
|
||||
|
||||
// @ts-ignore
|
||||
const call = groupCall1.calls.get(groupCall1.room.getMember(client2.userId)!)!.get(client2.deviceId)!;
|
||||
const call = groupCall1.calls.get(client2.userId)!.get(client2.deviceId)!;
|
||||
call.isMicrophoneMuted = jest.fn().mockReturnValue(true);
|
||||
call.setMicrophoneMuted = jest.fn();
|
||||
call.isLocalVideoMuted = jest.fn().mockReturnValue(true);
|
||||
@@ -839,7 +855,7 @@ describe("Group Call", function () {
|
||||
await sleep(10);
|
||||
|
||||
// @ts-ignore
|
||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
||||
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
|
||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||
// @ts-ignore Mock
|
||||
call.pushRemoteFeed(
|
||||
@@ -866,7 +882,7 @@ describe("Group Call", function () {
|
||||
await sleep(10);
|
||||
|
||||
// @ts-ignore
|
||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
||||
const call = groupCall.calls.get(FAKE_USER_ID_2).get(FAKE_DEVICE_ID_2)!;
|
||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||
// @ts-ignore Mock
|
||||
call.pushRemoteFeed(
|
||||
@@ -943,9 +959,7 @@ describe("Group Call", function () {
|
||||
expect(mockCall.reject).not.toHaveBeenCalled();
|
||||
expect(mockCall.answerWithCallFeeds).toHaveBeenCalled();
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(
|
||||
new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, mockCall]])]]),
|
||||
);
|
||||
expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, mockCall]])]]));
|
||||
});
|
||||
|
||||
it("replaces calls if it already has one with the same user", async () => {
|
||||
@@ -960,9 +974,7 @@ describe("Group Call", function () {
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(newMockCall.answerWithCallFeeds).toHaveBeenCalled();
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(
|
||||
new Map([[groupCall.room.getMember(FAKE_USER_ID_1)!, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]]),
|
||||
);
|
||||
expect(groupCall.calls).toEqual(new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall]])]]));
|
||||
});
|
||||
|
||||
it("starts to process incoming calls when we've entered", async () => {
|
||||
@@ -975,6 +987,83 @@ describe("Group Call", function () {
|
||||
|
||||
expect(call.answerWithCallFeeds).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe("handles call being replaced", () => {
|
||||
let callChangedListener: jest.Mock;
|
||||
let oldMockCall: MockMatrixCall;
|
||||
let newMockCall: MockMatrixCall;
|
||||
let newCallsMap: Map<string, Map<string, MatrixCall>>;
|
||||
|
||||
beforeEach(() => {
|
||||
callChangedListener = jest.fn();
|
||||
groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener);
|
||||
|
||||
oldMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
newMockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
newCallsMap = new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_1, newMockCall.typed()]])]]);
|
||||
|
||||
newMockCall.opponentMember = oldMockCall.opponentMember; // Ensure referential equality
|
||||
newMockCall.callId = "not " + oldMockCall.callId;
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, oldMockCall.typed());
|
||||
});
|
||||
|
||||
it("handles regular case", () => {
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(newCallsMap);
|
||||
});
|
||||
|
||||
it("handles case where call is missing from the calls map", () => {
|
||||
// @ts-ignore
|
||||
groupCall.calls = new Map();
|
||||
oldMockCall.emit(CallEvent.Replaced, newMockCall.typed());
|
||||
|
||||
expect(oldMockCall.hangup).toHaveBeenCalled();
|
||||
expect(callChangedListener).toHaveBeenCalledWith(newCallsMap);
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(newCallsMap);
|
||||
});
|
||||
});
|
||||
|
||||
describe("handles call being hangup", () => {
|
||||
let callChangedListener: jest.Mock;
|
||||
let mockCall: MockMatrixCall;
|
||||
|
||||
beforeEach(() => {
|
||||
callChangedListener = jest.fn();
|
||||
groupCall.addListener(GroupCallEvent.CallsChanged, callChangedListener);
|
||||
mockCall = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
});
|
||||
|
||||
it("doesn't throw when calls map is empty", () => {
|
||||
// @ts-ignore
|
||||
expect(() => groupCall.onCallHangup(mockCall)).not.toThrow();
|
||||
});
|
||||
|
||||
it("clears map completely when we're the last users device left", () => {
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed());
|
||||
mockCall.emit(CallEvent.Hangup, mockCall.typed());
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(new Map());
|
||||
});
|
||||
|
||||
it("doesn't remove another call of the same user", () => {
|
||||
const anotherCallOfTheSameUser = new MockMatrixCall(room.roomId, groupCall.groupCallId);
|
||||
anotherCallOfTheSameUser.callId = "another call id";
|
||||
anotherCallOfTheSameUser.getOpponentDeviceId = () => FAKE_DEVICE_ID_2;
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, anotherCallOfTheSameUser.typed());
|
||||
|
||||
mockClient.emit(CallEventHandlerEvent.Incoming, mockCall.typed());
|
||||
mockCall.emit(CallEvent.Hangup, mockCall.typed());
|
||||
// @ts-ignore
|
||||
expect(groupCall.calls).toEqual(
|
||||
new Map([[FAKE_USER_ID_1, new Map([[FAKE_DEVICE_ID_2, anotherCallOfTheSameUser.typed()]])]]),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("screensharing", () => {
|
||||
@@ -1039,7 +1128,7 @@ describe("Group Call", function () {
|
||||
await sleep(10);
|
||||
|
||||
// @ts-ignore
|
||||
const call = groupCall.calls.get(groupCall.room.getMember(FAKE_USER_ID_2)!)!.get(FAKE_DEVICE_ID_2)!;
|
||||
const call = groupCall.calls.get(FAKE_USER_ID_2)!.get(FAKE_DEVICE_ID_2)!;
|
||||
call.getOpponentMember = () => ({ userId: call.invitee } as RoomMember);
|
||||
call.onNegotiateReceived({
|
||||
getContent: () => ({
|
||||
|
Reference in New Issue
Block a user