From c629d2f60eeb8c99adf41041cbe5a8c7ca83463a Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 4 Aug 2022 11:44:10 -0400 Subject: [PATCH] Emit an event when the client receives TURN servers (#2529) * Emit an event when the client receives TURN servers * Add tests * Fix lints --- spec/unit/matrix-client.spec.ts | 131 ++++++++++++++++++++++++++++---- src/client.ts | 24 ++++-- 2 files changed, 134 insertions(+), 21 deletions(-) diff --git a/spec/unit/matrix-client.spec.ts b/spec/unit/matrix-client.spec.ts index db3377501..702c22c05 100644 --- a/spec/unit/matrix-client.spec.ts +++ b/spec/unit/matrix-client.spec.ts @@ -14,8 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +import { mocked } from "jest-mock"; + import { logger } from "../../src/logger"; -import { MatrixClient } from "../../src/client"; +import { MatrixClient, ClientEvent } from "../../src/client"; import { Filter } from "../../src/filter"; import { DEFAULT_TREE_POWER_LEVELS_TEMPLATE } from "../../src/models/MSC3089TreeSpace"; import { @@ -35,10 +37,16 @@ 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 { supportsMatrixCall } from "../../src/webrtc/call"; import { makeBeaconEvent } from "../test-utils/beacon"; jest.useFakeTimers(); +jest.mock("../../src/webrtc/call", () => ({ + ...jest.requireActual("../../src/webrtc/call"), + supportsMatrixCall: jest.fn(() => false), +})); + describe("MatrixClient", function() { const userId = "@alice:bar"; const identityServerUrl = "https://identity.server"; @@ -160,6 +168,24 @@ describe("MatrixClient", function() { return new Promise(() => {}); } + function makeClient() { + client = new MatrixClient({ + baseUrl: "https://my.home.server", + idBaseUrl: identityServerUrl, + accessToken: "my.access.token", + request: function() {} as any, // NOP + store: store, + scheduler: scheduler, + userId: userId, + }); + // FIXME: We shouldn't be yanking http like this. + client.http = [ + "authedRequest", "getContentUri", "request", "uploadContent", + ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); + client.http.authedRequest.mockImplementation(httpReq); + client.http.request.mockImplementation(httpReq); + } + beforeEach(function() { scheduler = [ "getQueueForEvent", "queueEvent", "removeEventFromQueue", @@ -177,21 +203,7 @@ describe("MatrixClient", function() { store.getClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.storeClientOptions = jest.fn().mockReturnValue(Promise.resolve(null)); store.isNewlyCreated = jest.fn().mockReturnValue(Promise.resolve(true)); - client = new MatrixClient({ - baseUrl: "https://my.home.server", - idBaseUrl: identityServerUrl, - accessToken: "my.access.token", - request: function() {} as any, // NOP - store: store, - scheduler: scheduler, - userId: userId, - }); - // FIXME: We shouldn't be yanking http like this. - client.http = [ - "authedRequest", "getContentUri", "request", "uploadContent", - ].reduce((r, k) => { r[k] = jest.fn(); return r; }, {}); - client.http.authedRequest.mockImplementation(httpReq); - client.http.request.mockImplementation(httpReq); + makeClient(); // set reasonable working defaults acceptKeepalives = true; @@ -1299,6 +1311,93 @@ describe("MatrixClient", function() { }); }); + describe("pollingTurnServers", () => { + afterEach(() => { + mocked(supportsMatrixCall).mockReset(); + }); + + it("is false if the client isn't started", () => { + expect(client.clientRunning).toBe(false); + expect(client.pollingTurnServers).toBe(false); + }); + + it("is false if VoIP is not supported", async () => { + mocked(supportsMatrixCall).mockReturnValue(false); + makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock + await client.startClient(); + expect(client.pollingTurnServers).toBe(false); + }); + + it("is true if VoIP is supported", async () => { + mocked(supportsMatrixCall).mockReturnValue(true); + makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock + await client.startClient(); + expect(client.pollingTurnServers).toBe(true); + }); + }); + + describe("checkTurnServers", () => { + beforeAll(() => { + mocked(supportsMatrixCall).mockReturnValue(true); + }); + + beforeEach(() => { + makeClient(); // create the client a second time so it picks up the supportsMatrixCall mock + }); + + afterAll(() => { + mocked(supportsMatrixCall).mockReset(); + }); + + it("emits an event when new TURN creds are found", async () => { + const turnServer = { + uris: [ + "turn:turn.example.com:3478?transport=udp", + "turn:10.20.30.40:3478?transport=tcp", + "turns:10.20.30.40:443?transport=tcp", + ], + username: "1443779631:@user:example.com", + password: "JlKfBy1QwLrO20385QyAtEyIv0=", + }; + jest.spyOn(client, "turnServer").mockResolvedValue(turnServer); + + const events: any[][] = []; + const onTurnServers = (...args) => events.push(args); + client.on(ClientEvent.TurnServers, onTurnServers); + expect(await client.checkTurnServers()).toBe(true); + client.off(ClientEvent.TurnServers, onTurnServers); + expect(events).toEqual([[[{ + urls: turnServer.uris, + username: turnServer.username, + credential: turnServer.password, + }]]]); + }); + + it("emits an event when an error occurs", async () => { + const error = new Error(":("); + jest.spyOn(client, "turnServer").mockRejectedValue(error); + + const events: any[][] = []; + const onTurnServersError = (...args) => events.push(args); + client.on(ClientEvent.TurnServersError, onTurnServersError); + expect(await client.checkTurnServers()).toBe(false); + client.off(ClientEvent.TurnServersError, onTurnServersError); + expect(events).toEqual([[error, false]]); // non-fatal + }); + + it("considers 403 errors fatal", async () => { + const error = { httpStatus: 403 }; + jest.spyOn(client, "turnServer").mockRejectedValue(error); + + const events: any[][] = []; + const onTurnServersError = (...args) => events.push(args); + client.on(ClientEvent.TurnServersError, onTurnServersError); + expect(await client.checkTurnServers()).toBe(false); + client.off(ClientEvent.TurnServersError, onTurnServersError); + expect(events).toEqual([[error, true]]); // fatal + }); + }); + describe("encryptAndSendToDevices", () => { it("throws an error if crypto is unavailable", () => { client.crypto = undefined; diff --git a/src/client.ts b/src/client.ts index 69daf9028..e9cf8fff1 100644 --- a/src/client.ts +++ b/src/client.ts @@ -1,5 +1,5 @@ /* -Copyright 2015-2021 The Matrix.org Foundation C.I.C. +Copyright 2015-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. @@ -506,7 +506,7 @@ interface ITurnServerResponse { ttl: number; } -interface ITurnServer { +export interface ITurnServer { urls: string[]; username: string; credential: string; @@ -791,6 +791,8 @@ export enum ClientEvent { DeleteRoom = "deleteRoom", SyncUnexpectedError = "sync.unexpectedError", ClientWellKnown = "WellKnown.client", + TurnServers = "turnServers", + TurnServersError = "turnServers.error", } type RoomEvents = RoomEvent.Name @@ -861,6 +863,8 @@ export type ClientEventHandlerMap = { [ClientEvent.DeleteRoom]: (roomId: string) => void; [ClientEvent.SyncUnexpectedError]: (error: Error) => void; [ClientEvent.ClientWellKnown]: (data: IClientWellKnown) => void; + [ClientEvent.TurnServers]: (servers: ITurnServer[]) => void; + [ClientEvent.TurnServersError]: (error: Error, fatal: boolean) => void; } & RoomEventHandlerMap & RoomStateEventHandlerMap & CryptoEventHandlerMap @@ -937,7 +941,7 @@ export class MatrixClient extends TypedEventEmitter; protected turnServers: ITurnServer[] = []; protected turnServersExpiry = 0; - protected checkTurnServersIntervalID: ReturnType; + protected checkTurnServersIntervalID: ReturnType | null = null; protected exportedOlmDeviceToImport: IExportedOlmDevice; protected txnCtr = 0; protected mediaHandler = new MediaHandler(this); @@ -1230,6 +1234,8 @@ export class MatrixClient extends TypedEventEmitter { if (!this.canSupportVoip) { @@ -6370,17 +6380,21 @@ export class MatrixClient extends TypedEventEmitter