diff --git a/spec/unit/matrixrtc/CallMembership.spec.ts b/spec/unit/matrixrtc/CallMembership.spec.ts index 0b458b3be49..24600c1a170 100644 --- a/spec/unit/matrixrtc/CallMembership.spec.ts +++ b/spec/unit/matrixrtc/CallMembership.spec.ts @@ -15,17 +15,7 @@ limitations under the License. */ import { MatrixEvent } from "../../../src"; -import { CallMembership, CallMembershipData } from "../../../src/matrixrtc/CallMembership"; - -const membershipTemplate: CallMembershipData = { - call_id: "", - scope: "m.room", - application: "m.call", - device_id: "AAAAAAA", - expires: 5000, - membershipID: "bloop", - foci_active: [{ type: "livekit" }], -}; +import { CallMembership, CallMembershipDataLegacy, SessionMembershipData } from "../../../src/matrixrtc/CallMembership"; function makeMockEvent(originTs = 0): MatrixEvent { return { @@ -35,96 +25,175 @@ function makeMockEvent(originTs = 0): MatrixEvent { } describe("CallMembership", () => { - it("rejects membership with no expiry and no expires_ts", () => { - expect(() => { - new CallMembership( - makeMockEvent(), - Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }), + describe("CallMembershipDataLegacy", () => { + const membershipTemplate: CallMembershipDataLegacy = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + expires: 5000, + membershipID: "bloop", + foci_active: [{ type: "livekit" }], + }; + it("rejects membership with no expiry and no expires_ts", () => { + expect(() => { + new CallMembership( + makeMockEvent(), + Object.assign({}, membershipTemplate, { expires: undefined, expires_ts: undefined }), + ); + }).toThrow(); + }); + + it("rejects membership with no device_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); + }).toThrow(); + }); + + it("rejects membership with no call_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); + }).toThrow(); + }); + + it("allow membership with no scope", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); + }).not.toThrow(); + }); + it("rejects with malformatted expires_ts", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" })); + }).toThrow(); + }); + it("rejects with malformatted expires", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" })); + }).toThrow(); + }); + + it("uses event timestamp if no created_ts", () => { + const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); + expect(membership.createdTs()).toEqual(12345); + }); + + it("uses created_ts if present", () => { + const membership = new CallMembership( + makeMockEvent(12345), + Object.assign({}, membershipTemplate, { created_ts: 67890 }), ); - }).toThrow(); - }); + expect(membership.createdTs()).toEqual(67890); + }); - it("rejects membership with no device_id", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); - }).toThrow(); - }); + it("computes absolute expiry time based on expires", () => { + const membership = new CallMembership(makeMockEvent(1000), membershipTemplate); + expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); + }); - it("rejects membership with no call_id", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); - }).toThrow(); - }); + it("computes absolute expiry time based on expires_ts", () => { + const membership = new CallMembership( + makeMockEvent(1000), + Object.assign({}, membershipTemplate, { expires_ts: 6000 }), + ); + expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); + }); - it("allow membership with no scope", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); - }).not.toThrow(); - }); - it("rejects with malformatted expires_ts", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires_ts: "string" })); - }).toThrow(); - }); - it("rejects with malformatted expires", () => { - expect(() => { - new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { expires: "string" })); - }).toThrow(); - }); + it("considers memberships unexpired if local age low enough", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000); + const membership = new CallMembership(fakeEvent, membershipTemplate); + expect(membership.isExpired()).toEqual(false); + }); - it("uses event timestamp if no created_ts", () => { - const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); - expect(membership.createdTs()).toEqual(12345); - }); + it("considers memberships expired when local age large", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.localTimestamp = Date.now() - 6000; + const membership = new CallMembership(fakeEvent, membershipTemplate); + expect(membership.isExpired()).toEqual(true); + }); - it("uses created_ts if present", () => { - const membership = new CallMembership( - makeMockEvent(12345), - Object.assign({}, membershipTemplate, { created_ts: 67890 }), - ); - expect(membership.createdTs()).toEqual(67890); + it("returns preferred foci", () => { + const fakeEvent = makeMockEvent(); + const mockFocus = { type: "this_is_a_mock_focus" }; + const membership = new CallMembership( + fakeEvent, + Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }), + ); + expect(membership.getPreferredFoci()).toEqual([mockFocus]); + }); }); - it("computes absolute expiry time based on expires", () => { - const membership = new CallMembership(makeMockEvent(1000), membershipTemplate); - expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); - }); + describe("SessionMembershipData", () => { + const membershipTemplate: SessionMembershipData = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + foci_active: { type: "livekit" }, + foci_preferred: [{ type: "livekit" }], + }; + + it("rejects membership with no device_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { device_id: undefined })); + }).toThrow(); + }); - it("computes absolute expiry time based on expires_ts", () => { - const membership = new CallMembership( - makeMockEvent(1000), - Object.assign({}, membershipTemplate, { expires_ts: 6000 }), - ); - expect(membership.getAbsoluteExpiry()).toEqual(5000 + 1000); - }); + it("rejects membership with no call_id", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { call_id: undefined })); + }).toThrow(); + }); - it("considers memberships unexpired if local age low enough", () => { - const fakeEvent = makeMockEvent(1000); - fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000); - const membership = new CallMembership(fakeEvent, membershipTemplate); - expect(membership.isExpired()).toEqual(false); - }); + it("allow membership with no scope", () => { + expect(() => { + new CallMembership(makeMockEvent(), Object.assign({}, membershipTemplate, { scope: undefined })); + }).not.toThrow(); + }); - it("considers memberships expired when local age large", () => { - const fakeEvent = makeMockEvent(1000); - fakeEvent.localTimestamp = Date.now() - 6000; - const membership = new CallMembership(fakeEvent, membershipTemplate); - expect(membership.isExpired()).toEqual(true); - }); + it("uses event timestamp if no created_ts", () => { + const membership = new CallMembership(makeMockEvent(12345), membershipTemplate); + expect(membership.createdTs()).toEqual(12345); + }); + + it("uses created_ts if present", () => { + const membership = new CallMembership( + makeMockEvent(12345), + Object.assign({}, membershipTemplate, { created_ts: 67890 }), + ); + expect(membership.createdTs()).toEqual(67890); + }); + + it("considers memberships unexpired if local age low enough", () => { + const fakeEvent = makeMockEvent(1000); + fakeEvent.getLocalAge = jest.fn().mockReturnValue(3000); + const membership = new CallMembership(fakeEvent, membershipTemplate); + expect(membership.isExpired()).toEqual(false); + }); - it("returns preferred foci", () => { - const fakeEvent = makeMockEvent(); - const mockFocus = { type: "this_is_a_mock_focus" }; - const membership = new CallMembership( - fakeEvent, - Object.assign({}, membershipTemplate, { foci_active: [mockFocus] }), - ); - expect(membership.getPreferredFoci()).toEqual([mockFocus]); + it("returns preferred foci", () => { + const fakeEvent = makeMockEvent(); + const mockFocus = { type: "this_is_a_mock_focus" }; + const membership = new CallMembership( + fakeEvent, + Object.assign({}, membershipTemplate, { foci_preferred: [mockFocus] }), + ); + expect(membership.getPreferredFoci()).toEqual([mockFocus]); + }); }); describe("expiry calculation", () => { let fakeEvent: MatrixEvent; let membership: CallMembership; + const membershipTemplate: CallMembershipDataLegacy = { + call_id: "", + scope: "m.room", + application: "m.call", + device_id: "AAAAAAA", + expires: 5000, + membershipID: "bloop", + foci_active: [{ type: "livekit" }], + }; beforeEach(() => { // server origin timestamp for this event is 1000 diff --git a/spec/unit/matrixrtc/LivekitFocus.spec.ts b/spec/unit/matrixrtc/LivekitFocus.spec.ts new file mode 100644 index 00000000000..728d6a68de6 --- /dev/null +++ b/spec/unit/matrixrtc/LivekitFocus.spec.ts @@ -0,0 +1,60 @@ +/* +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 { isLivekitFocus, isLivekitFocusActive, isLivekitFocusConfig } from "../../../src/matrixrtc/LivekitFocus"; + +describe("LivekitFocus", () => { + it("isLivekitFocus", () => { + expect( + isLivekitFocus({ + type: "livekit", + livekit_service_url: "http://test.com", + livekit_alias: "test", + }), + ).toBeTruthy(); + expect(isLivekitFocus({ type: "livekit" })).toBeFalsy(); + expect( + isLivekitFocus({ type: "not-livekit", livekit_service_url: "http://test.com", livekit_alias: "test" }), + ).toBeFalsy(); + expect( + isLivekitFocus({ type: "livekit", other_service_url: "http://test.com", livekit_alias: "test" }), + ).toBeFalsy(); + expect( + isLivekitFocus({ type: "livekit", livekit_service_url: "http://test.com", other_alias: "test" }), + ).toBeFalsy(); + }); + it("isLivekitFocusActive", () => { + expect( + isLivekitFocusActive({ + type: "livekit", + focus_selection: "oldest_membership", + }), + ).toBeTruthy(); + expect(isLivekitFocusActive({ type: "livekit" })).toBeFalsy(); + expect(isLivekitFocusActive({ type: "not-livekit", focus_selection: "oldest_membership" })).toBeFalsy(); + }); + it("isLivekitFocusConfig", () => { + expect( + isLivekitFocusConfig({ + type: "livekit", + livekit_service_url: "http://test.com", + }), + ).toBeTruthy(); + expect(isLivekitFocusConfig({ type: "livekit" })).toBeFalsy(); + expect(isLivekitFocusConfig({ type: "not-livekit", livekit_service_url: "http://test.com" })).toBeFalsy(); + expect(isLivekitFocusConfig({ type: "livekit", other_service_url: "oldest_membership" })).toBeFalsy(); + }); +}); diff --git a/src/matrixrtc/CallMembership.ts b/src/matrixrtc/CallMembership.ts index 55fd07b5fe2..8930c0ab054 100644 --- a/src/matrixrtc/CallMembership.ts +++ b/src/matrixrtc/CallMembership.ts @@ -43,7 +43,6 @@ export interface SessionMembershipData { export const isSessionMembershipData = (data: any): data is SessionMembershipData => "foci_active" in data && "foci_preferred" in data && - "membership_id" in data && !Array.isArray(data.foci_active) && Array.isArray(data.foci_preferred); @@ -53,7 +52,7 @@ const checkSessionsMembershipData = (data: SessionMembershipData): void => { if (typeof data.call_id !== "string") throw new Error(prefix + "call_id must be string"); if (typeof data.application !== "string") throw new Error(prefix + "application must be a string"); if (typeof data.foci_active?.type !== "string") throw new Error(prefix + "foci_active.type must be a string"); - if (Array.isArray(data.foci_preferred)) throw new Error(prefix + "foci_preferred must be an array"); + if (!Array.isArray(data.foci_preferred)) throw new Error(prefix + "foci_preferred must be an array"); // optional elements if (data.created_ts && typeof data.created_ts !== "number") throw new Error(prefix + "created_ts must be number"); diff --git a/src/matrixrtc/LivekitFocus.ts b/src/matrixrtc/LivekitFocus.ts index c859883611e..0a42dda5fd5 100644 --- a/src/matrixrtc/LivekitFocus.ts +++ b/src/matrixrtc/LivekitFocus.ts @@ -21,19 +21,19 @@ export interface LivekitFocusConfig extends Focus { livekit_service_url: string; } -export const isLivekitFocusConfig = (object: Focus): object is LivekitFocusConfig => +export const isLivekitFocusConfig = (object: any): object is LivekitFocusConfig => object.type === "livekit" && "livekit_service_url" in object; export interface LivekitFocus extends LivekitFocusConfig { livekit_alias: string; } -export const isLivekitFocus = (object: Focus): object is LivekitFocus => +export const isLivekitFocus = (object: any): object is LivekitFocus => isLivekitFocusConfig(object) && "livekit_alias" in object; export interface LivekitFocusActive extends Focus { type: "livekit"; focus_selection: "oldest_membership"; } -export const isLivekitFocusActive = (object: Focus): object is LivekitFocusActive => +export const isLivekitFocusActive = (object: any): object is LivekitFocusActive => object.type === "livekit" && "focus_selection" in object;