Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Element-R: implement remaining OutgoingMessage request types #3083

Merged
merged 4 commits into from
Jan 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
],
"dependencies": {
"@babel/runtime": "^7.12.5",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.2",
"@matrix-org/matrix-sdk-crypto-js": "^0.1.0-alpha.3",
"another-json": "^0.2.0",
"bs58": "^5.0.0",
"content-type": "^1.0.4",
Expand Down
180 changes: 180 additions & 0 deletions spec/unit/rust-crypto/OutgoingRequestProcessor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
/*
Copyright 2023 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 MockHttpBackend from "matrix-mock-request";
import { Mocked } from "jest-mock";
import * as RustSdkCryptoJs from "@matrix-org/matrix-sdk-crypto-js";
import {
KeysBackupRequest,
KeysClaimRequest,
KeysQueryRequest,
KeysUploadRequest,
RoomMessageRequest,
SignatureUploadRequest,
ToDeviceRequest,
} from "@matrix-org/matrix-sdk-crypto-js";

import { TypedEventEmitter } from "../../../src/models/typed-event-emitter";
import { HttpApiEvent, HttpApiEventHandlerMap, MatrixHttpApi } from "../../../src";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";

describe("OutgoingRequestProcessor", () => {
andybalaam marked this conversation as resolved.
Show resolved Hide resolved
/** the OutgoingRequestProcessor implementation under test */
let processor: OutgoingRequestProcessor;

/** A mock http backend which processor is connected to */
let httpBackend: MockHttpBackend;

/** a mocked-up OlmMachine which processor is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;

/** 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();

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,
});

olmMachine = {
markRequestAsSent: jest.fn(),
} as unknown as Mocked<RustSdkCryptoJs.OlmMachine>;

processor = new OutgoingRequestProcessor(olmMachine, httpApi);
});

/* simple requests that map directly to the request body */
const tests: Array<[string, any, "POST" | "PUT", string]> = [
["KeysUploadRequest", KeysUploadRequest, "POST", "https://example.com/_matrix/client/v3/keys/upload"],
["KeysQueryRequest", KeysQueryRequest, "POST", "https://example.com/_matrix/client/v3/keys/query"],
["KeysClaimRequest", KeysClaimRequest, "POST", "https://example.com/_matrix/client/v3/keys/claim"],
[
"SignatureUploadRequest",
SignatureUploadRequest,
"POST",
"https://example.com/_matrix/client/v3/keys/signatures/upload",
],
["KeysBackupRequest", KeysBackupRequest, "PUT", "https://example.com/_matrix/client/v3/room_keys/keys"],
];

test.each(tests)(`should handle %ss`, async (_, RequestClass, expectedMethod, expectedPath) => {
// first, mock up a request as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RequestClass("1234", testBody);

// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);

// Now: check that it makes a matching HTTP request ...
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);

// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();

await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});

it("should handle ToDeviceRequests", async () => {
// first, mock up the ToDeviceRequest as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new ToDeviceRequest("1234", "test/type", "test/txnid", testBody);

// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);

// Now: check that it makes a matching HTTP request ...
const testResponse = '{ "result": 1 }';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual("https://example.com/_matrix/client/v3/sendToDevice/test%2Ftype/test%2Ftxnid");
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);

// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();

await Promise.all([reqProm, markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("1234", outgoingRequest.type, testResponse);
httpBackend.verifyNoOutstandingRequests();
});

it("should handle RoomMessageRequests", async () => {
// first, mock up the RoomMessageRequest as we might expect to receive it from the Rust layer ...
const testBody = '{ "foo": "bar" }';
const outgoingRequest = new RoomMessageRequest("1234", "test/room", "test/txnid", "test/type", testBody);

// ... then poke it into the OutgoingRequestProcessor under test.
const reqProm = processor.makeOutgoingRequest(outgoingRequest);

// Now: check that it makes a matching HTTP request ...
const testResponse = '{ "result": 1 }';
httpBackend
.when("PUT", "/_matrix")
.check((req) => {
expect(req.path).toEqual(
"https://example.com/_matrix/client/v3/room/test%2Froom/send/test%2Ftype/test%2Ftxnid",
);
expect(req.rawData).toEqual(testBody);
expect(req.headers["Accept"]).toEqual("application/json");
expect(req.headers["Content-Type"]).toEqual("application/json");
})
.respond(200, testResponse, true);

// ... and that it calls OlmMachine.markAsSent.
const markSentCallPromise = awaitCallToMarkAsSent();
await httpBackend.flushAllExpected();

await Promise.all([reqProm, 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 };
const markSentCallPromise = awaitCallToMarkAsSent();
await Promise.all([processor.makeOutgoingRequest(outgoingRequest), markSentCallPromise]);
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,16 @@ 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 { KeysQueryRequest, OlmMachine } 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";
import { mkEvent } from "../test-utils/test-utils";
import { CryptoBackend } from "../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../src/@types/crypto";
import { RustCrypto } from "../../../src/rust-crypto/rust-crypto";
import { initRustCrypto } from "../../../src/rust-crypto";
import { IToDeviceEvent, MatrixClient, MatrixHttpApi } from "../../../src";
import { mkEvent } from "../../test-utils/test-utils";
import { CryptoBackend } from "../../../src/common-crypto/CryptoBackend";
import { IEventDecryptionResult } from "../../../src/@types/crypto";
import { OutgoingRequestProcessor } from "../../../src/rust-crypto/OutgoingRequestProcessor";

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

/** A mock http backend which rustCrypto is connected to */
let httpBackend: MockHttpBackend;
/** A mock OutgoingRequestProcessor which rustCrypto is connected to */
let outgoingRequestProcessor: Mocked<OutgoingRequestProcessor>;

/** a mocked-up OlmMachine which rustCrypto is connected to */
let olmMachine: Mocked<RustSdkCryptoJs.OlmMachine>;
Expand All @@ -116,120 +108,81 @@ describe("RustCrypto", () => {
* 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);
/** wait for a call to outgoingRequestProcessor.makeOutgoingRequest.
*
* The promise resolves to a callback: the makeOutgoingRequest call will not complete until the returned
* callback is called.
*/
function awaitCallToMakeOutgoingRequest(): Promise<() => void> {
return new Promise<() => void>((resolveCalledPromise, _reject) => {
outgoingRequestProcessor.makeOutgoingRequest.mockImplementationOnce(async () => {
const completePromise = new Promise<void>((resolveCompletePromise, _reject) => {
resolveCalledPromise(resolveCompletePromise);
});
return completePromise;
});
});
}

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);
});
outgoingRequestProcessor = {
makeOutgoingRequest: jest.fn(),
} as unknown as Mocked<OutgoingRequestProcessor>;

it("should poll for outgoing messages", () => {
rustCrypto.onSyncCompleted({});
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
rustCrypto = new RustCrypto(olmMachine, {} as MatrixHttpApi<any>, TEST_USER, TEST_DEVICE_ID);
rustCrypto["outgoingRequestProcessor"] = outgoingRequestProcessor;
});

/* 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]);
it("should poll for outgoing messages and send them", async () => {
const testReq = new KeysQueryRequest("1234", "{}");
outgoingRequestQueue.push([testReq]);

const makeRequestPromise = awaitCallToMakeOutgoingRequest();
rustCrypto.onSyncCompleted({});

await awaitCallToMarkAsSent();
expect(olmMachine.markRequestAsSent).toHaveBeenCalledWith("5678", 987, "");
await makeRequestPromise;
expect(olmMachine.outgoingRequests).toHaveBeenCalled();
expect(outgoingRequestProcessor.makeOutgoingRequest).toHaveBeenCalledWith(testReq);
});

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);
}

let makeRequestPromise = awaitCallToMakeOutgoingRequest();

rustCrypto.onSyncCompleted({});

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

// go a couple of times round the loop
await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
let resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();

await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
resolveMakeRequest = await makeRequestPromise;
makeRequestPromise = awaitCallToMakeOutgoingRequest();
resolveMakeRequest();

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

await httpBackend.flush("/_matrix", 1);
await awaitCallToMarkAsSent();
resolveMakeRequest = await makeRequestPromise;
outgoingRequestProcessor.makeOutgoingRequest.mockReset();
resolveMakeRequest();

// now stop...
rustCrypto.stop();
Expand All @@ -241,7 +194,7 @@ describe("RustCrypto", () => {
setTimeout(resolve, 100);
});
expect(rustCrypto["outgoingRequestLoopRunning"]).toBeFalsy();
httpBackend.verifyNoOutstandingRequests();
expect(outgoingRequestProcessor.makeOutgoingRequest).not.toHaveBeenCalled();
expect(olmMachine.outgoingRequests).not.toHaveBeenCalled();

// we sent three, so there should be 2 left
Expand Down
Loading