Skip to content

Commit 0c1d5f6

Browse files
authored
Element-R: implement remaining OutgoingMessage request types (#3083)
This is a follow-up to #3019: it implements the remaining two types of message types, now that rust SDK has sensibly-shaped types for them.
1 parent 1c26dc0 commit 0c1d5f6

File tree

6 files changed

+347
-162
lines changed

6 files changed

+347
-162
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@
5555
],
5656
"dependencies": {
5757
"@babel/runtime": "^7.12.5",
58-
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
58+
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
5959
"another-json": "^0.2.0",
6060
"bs58": "^5.0.0",
6161
"content-type": "^1.0.4",
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import MockHttpBackend from "matrix-mock-request";
18+
import { Mocked } from "jest-mock";
19+
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
20+
import {
21+
KeysBackupRequest,
22+
KeysClaimRequest,
23+
KeysQueryRequest,
24+
KeysUploadRequest,
25+
RoomMessageRequest,
26+
SignatureUploadRequest,
27+
ToDeviceRequest,
28+
} from "@matrix-org/matrix-sdk-crypto-js";
29+
30+
import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
31+
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
32+
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
33+
34+
describe("OutgoingRequestProcessor", () => {
35+
/** the OutgoingRequestProcessor implementation under test */
36+
let processor: OutgoingRequestProcessor;
37+
38+
/** A mock http backend which processor is connected to */
39+
let httpBackend: MockHttpBackend;
40+
41+
/** a mocked-up OlmMachine which processor is connected to */
42+
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
43+
44+
/** wait for a call to olmMachine.markRequestAsSent */
45+
function awaitCallToMarkAsSent(): Promise<void> {
46+
return new Promise((resolve, _reject) => {
47+
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
48+
resolve(undefined);
49+
});
50+
});
51+
}
52+
53+
beforeEach(async () => {
54+
httpBackend = new MockHttpBackend();
55+
56+
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
57+
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
58+
baseUrl: "https://example.com",
59+
prefix: "/_matrix",
60+
fetchFn: httpBackend.fetchFn as typeof global.fetch,
61+
onlyData: true,
62+
});
63+
64+
olmMachine = {
65+
markRequestAsSent: jest.fn(),
66+
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
67+
68+
processor = new OutgoingRequestProcessor(olmMachine, httpApi);
69+
});
70+
71+
/* simple requests that map directly to the request body */
72+
const tests: Array<[string, any, "POST" | "PUT", string]> = [
73+
["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
74+
["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
75+
["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
76+
[
77+
"SignatureUploadRequest",
78+
SignatureUploadRequest,
79+
"POST",
80+
"https://example.com/_matrix/client/v3/keys/signatures/upload",
81+
],
82+
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
83+
];
84+
85+
test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
86+
// first, mock up a request as we might expect to receive it from the Rust layer ...
87+
const testBody = '{ "foo": "bar" }';
88+
const outgoingRequest = new RequestClass("1234", testBody);
89+
90+
// ... then poke it into the OutgoingRequestProcessor under test.
91+
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
92+
93+
// Now: check that it makes a matching HTTP request ...
94+
const testResponse = '{ "result": 1 }';
95+
httpBackend
96+
.when(expectedMethod, "/_matrix")
97+
.check((req) => {
98+
expect(req.path).toEqual(expectedPath);
99+
expect(req.rawData).toEqual(testBody);
100+
expect(req.headers["Accept"]).toEqual("application/json");
101+
expect(req.headers["Content-Type"]).toEqual("application/json");
102+
})
103+
.respond(200, testResponse, true);
104+
105+
// ... and that it calls OlmMachine.markAsSent.
106+
const markSentCallPromise = awaitCallToMarkAsSent();
107+
await httpBackend.flushAllExpected();
108+
109+
await Promise.all([reqProm, markSentCallPromise]);
110+
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
111+
httpBackend.verifyNoOutstandingRequests();
112+
});
113+
114+
it("should handle ToDeviceRequests", async () => {
115+
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
116+
const testBody = '{ "foo": "bar" }';
117+
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);
118+
119+
// ... then poke it into the OutgoingRequestProcessor under test.
120+
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
121+
122+
// Now: check that it makes a matching HTTP request ...
123+
const testResponse = '{ "result": 1 }';
124+
httpBackend
125+
.when("PUT", "/_matrix")
126+
.check((req) => {
127+
expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid");
128+
expect(req.rawData).toEqual(testBody);
129+
expect(req.headers["Accept"]).toEqual("application/json");
130+
expect(req.headers["Content-Type"]).toEqual("application/json");
131+
})
132+
.respond(200, testResponse, true);
133+
134+
// ... and that it calls OlmMachine.markAsSent.
135+
const markSentCallPromise = awaitCallToMarkAsSent();
136+
await httpBackend.flushAllExpected();
137+
138+
await Promise.all([reqProm, markSentCallPromise]);
139+
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
140+
httpBackend.verifyNoOutstandingRequests();
141+
});
142+
143+
it("should handle RoomMessageRequests", async () => {
144+
// first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ...
145+
const testBody = '{ "foo": "bar" }';
146+
const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody);
147+
148+
// ... then poke it into the OutgoingRequestProcessor under test.
149+
const reqProm = processor.makeOutgoingRequest(outgoingRequest);
150+
151+
// Now: check that it makes a matching HTTP request ...
152+
const testResponse = '{ "result": 1 }';
153+
httpBackend
154+
.when("PUT", "/_matrix")
155+
.check((req) => {
156+
expect(req.path).toEqual(
157+
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
158+
);
159+
expect(req.rawData).toEqual(testBody);
160+
expect(req.headers["Accept"]).toEqual("application/json");
161+
expect(req.headers["Content-Type"]).toEqual("application/json");
162+
})
163+
.respond(200, testResponse, true);
164+
165+
// ... and that it calls OlmMachine.markAsSent.
166+
const markSentCallPromise = awaitCallToMarkAsSent();
167+
await httpBackend.flushAllExpected();
168+
169+
await Promise.all([reqProm, markSentCallPromise]);
170+
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
171+
httpBackend.verifyNoOutstandingRequests();
172+
});
173+
174+
it("does not explode with unknown requests", async () => {
175+
const outgoingRequest = { id: "5678", type: 987 };
176+
const markSentCallPromise = awaitCallToMarkAsSent();
177+
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
178+
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
179+
});
180+
});

spec/unit/rust-crypto.spec.ts renamed to spec/unit/rust-crypto/rust-crypto.spec.ts

Lines changed: 46 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -17,24 +17,16 @@ limitations under the License.
1717
import "fake-indexeddb/auto";
1818
import { IDBFactory } from "fake-indexeddb";
1919
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
20-
import {
21-
KeysBackupRequest,
22-
KeysClaimRequest,
23-
KeysQueryRequest,
24-
KeysUploadRequest,
25-
OlmMachine,
26-
SignatureUploadRequest,
27-
} from "@matrix-org/matrix-sdk-crypto-js";
20+
import { KeysQueryRequest, OlmMachine } from "@matrix-org/matrix-sdk-crypto-js";
2821
import { Mocked } from "jest-mock";
29-
import MockHttpBackend from "matrix-mock-request";
3022

31-
import { RustCrypto } from "../../src/rust-crypto/rust-crypto";
32-
import { initRustCrypto } from "../../src/rust-crypto";
33-
import { HttpApiEvent, HttpApiEventHandlerMap, IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../src";
34-
import { TypedEventEmitter } from "../../src/models/typed-event-emitter";
35-
import { mkEvent } from "../test-utils/test-utils";
36-
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
37-
import { IEventDecryptionResult } from "../../src/@types/crypto";
23+
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
24+
import { initRustCrypto } from "../../../src/rust-crypto";
25+
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
26+
import { mkEvent } from "../../test-utils/test-utils";
27+
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
28+
import { IEventDecryptionResult } from "../../../src/@types/crypto";
29+
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";
3830

3931
afterEach(() => {
4032
// reset fake-indexeddb after each test, to make sure we don't leak connections
@@ -106,8 +98,8 @@ describe("RustCrypto", () => {
10698
/** the RustCrypto implementation under test */
10799
let rustCrypto: RustCrypto;
108100

109-
/** A mock http backend which rustCrypto is connected to */
110-
let httpBackend: MockHttpBackend;
101+
/** A mock OutgoingRequestProcessor which rustCrypto is connected to */
102+
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;
111103

112104
/** a mocked-up OlmMachine which rustCrypto is connected to */
113105
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
@@ -116,120 +108,81 @@ describe("RustCrypto", () => {
116108
* the front of the queue, until it is empty. */
117109
let outgoingRequestQueue: Array<Array<any>>;
118110

119-
/** wait for a call to olmMachine.markRequestAsSent */
120-
function awaitCallToMarkAsSent(): Promise<void> {
121-
return new Promise((resolve, _reject) => {
122-
olmMachine.markRequestAsSent.mockImplementationOnce(async () => {
123-
resolve(undefined);
111+
/** wait for a call to outgoingRequestProcessor.makeOutgoingRequest.
112+
*
113+
* The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned
114+
* callback is called.
115+
*/
116+
function awaitCallToMakeOutgoingRequest(): Promise<() => void> {
117+
return new Promise<() => void>((resolveCalledPromise, _reject) => {
118+
outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => {
119+
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
120+
resolveCalledPromise(resolveCompletePromise);
121+
});
122+
return completePromise;
124123
});
125124
});
126125
}
127126

128127
beforeEach(async () => {
129-
httpBackend = new MockHttpBackend();
130-
131128
await RustSdkCryptoJs.initAsync();
132129

133-
const dummyEventEmitter = new TypedEventEmitter<HttpApiEvent, HttpApiEventHandlerMap>();
134-
const httpApi = new MatrixHttpApi(dummyEventEmitter, {
135-
baseUrl: "https://example.com",
136-
prefix: "/_matrix",
137-
fetchFn: httpBackend.fetchFn as typeof global.fetch,
138-
onlyData: true,
139-
});
140-
141130
// for these tests we use a mock OlmMachine, with an implementation of outgoingRequests that
142131
// returns objects from outgoingRequestQueue
143132
outgoingRequestQueue = [];
144133
olmMachine = {
145134
outgoingRequests: jest.fn().mockImplementation(() => {
146135
return Promise.resolve(outgoingRequestQueue.shift() ?? []);
147136
}),
148-
markRequestAsSent: jest.fn(),
149137
close: jest.fn(),
150138
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;
151139

152-
rustCrypto = new RustCrypto(olmMachine, httpApi, TEST_USER, TEST_DEVICE_ID);
153-
});
140+
outgoingRequestProcessor = {
141+
makeOutgoingRequest: jest.fn(),
142+
} as unknown as Mocked<OutgoingRequestProcessor>;
154143

155-
it("should poll for outgoing messages", () => {
156-
rustCrypto.onSyncCompleted({});
157-
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
144+
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
145+
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
158146
});
159147

160-
/* simple requests that map directly to the request body */
161-
const tests: Array<[any, "POST" | "PUT", string]> = [
162-
[KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
163-
[KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
164-
[KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
165-
[SignatureUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/signatures/upload"],
166-
[KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
167-
];
168-
169-
for (const [RequestClass, expectedMethod, expectedPath] of tests) {
170-
it(`should handle ${RequestClass.name}s`, async () => {
171-
const testBody = '{ "foo": "bar" }';
172-
const outgoingRequest = new RequestClass("1234", testBody);
173-
outgoingRequestQueue.push([outgoingRequest]);
174-
175-
const testResponse = '{ "result": 1 }';
176-
httpBackend
177-
.when(expectedMethod, "/_matrix")
178-
.check((req) => {
179-
expect(req.path).toEqual(expectedPath);
180-
expect(req.rawData).toEqual(testBody);
181-
expect(req.headers["Accept"]).toEqual("application/json");
182-
expect(req.headers["Content-Type"]).toEqual("application/json");
183-
})
184-
.respond(200, testResponse, true);
185-
186-
rustCrypto.onSyncCompleted({});
187-
188-
expect(olmMachine.outgoingRequests).toHaveBeenCalledTimes(1);
189-
190-
const markSentCallPromise = awaitCallToMarkAsSent();
191-
await httpBackend.flushAllExpected();
192-
193-
await markSentCallPromise;
194-
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
195-
httpBackend.verifyNoOutstandingRequests();
196-
});
197-
}
198-
199-
it("does not explode with unknown requests", async () => {
200-
const outgoingRequest = { id: "5678", type: 987 };
201-
outgoingRequestQueue.push([outgoingRequest]);
148+
it("should poll for outgoing messages and send them", async () => {
149+
const testReq = new KeysQueryRequest("1234", "{}");
150+
outgoingRequestQueue.push([testReq]);
202151

152+
const makeRequestPromise = awaitCallToMakeOutgoingRequest();
203153
rustCrypto.onSyncCompleted({});
204154

205-
await awaitCallToMarkAsSent();
206-
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
155+
await makeRequestPromise;
156+
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
157+
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
207158
});
208159

209160
it("stops looping when stop() is called", async () => {
210-
const testResponse = '{ "result": 1 }';
211-
212161
for (let i = 0; i < 5; i++) {
213162
outgoingRequestQueue.push([new KeysQueryRequest("1234", "{}")]);
214-
httpBackend.when("POST", "/_matrix").respond(200, testResponse, true);
215163
}
216164

165+
let makeRequestPromise = awaitCallToMakeOutgoingRequest();
166+
217167
rustCrypto.onSyncCompleted({});
218168

219169
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeTruthy();
220170

221171
// go a couple of times round the loop
222-
await httpBackend.flush("/_matrix", 1);
223-
await awaitCallToMarkAsSent();
172+
let resolveMakeRequest = await makeRequestPromise;
173+
makeRequestPromise = awaitCallToMakeOutgoingRequest();
174+
resolveMakeRequest();
224175

225-
await httpBackend.flush("/_matrix", 1);
226-
await awaitCallToMarkAsSent();
176+
resolveMakeRequest = await makeRequestPromise;
177+
makeRequestPromise = awaitCallToMakeOutgoingRequest();
178+
resolveMakeRequest();
227179

228180
// a second sync while this is going on shouldn't make any difference
229181
rustCrypto.onSyncCompleted({});
230182

231-
await httpBackend.flush("/_matrix", 1);
232-
await awaitCallToMarkAsSent();
183+
resolveMakeRequest = await makeRequestPromise;
184+
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
185+
resolveMakeRequest();
233186

234187
// now stop...
235188
rustCrypto.stop();
@@ -241,7 +194,7 @@ describe("RustCrypto", () => {
241194
setTimeout(resolve, 100);
242195
});
243196
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
244-
httpBackend.verifyNoOutstandingRequests();
197+
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
245198
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();
246199

247200
// we sent three, so there should be 2 left

0 commit comments

Comments
 (0)