You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-23 17:02:25 +03:00
442 lines
19 KiB
TypeScript
442 lines
19 KiB
TypeScript
/*
|
|
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 { type MatrixEvent } from "../../../src";
|
|
import { rtcMembershipTemplate, sessionMembershipTemplate } from "./mocks";
|
|
import { CallMembership, DEFAULT_EXPIRE_DURATION } from "../../../src/matrixrtc/CallMembership";
|
|
|
|
function makeMockEvent(originTs = 0, content = {}): MatrixEvent {
|
|
return {
|
|
getTs: jest.fn().mockReturnValue(originTs),
|
|
getSender: jest.fn().mockReturnValue("@alice:example.org"),
|
|
getId: jest.fn().mockReturnValue("$eventid"),
|
|
getContent: jest.fn().mockReturnValue(content),
|
|
} as unknown as MatrixEvent;
|
|
}
|
|
|
|
describe("CallMembership", () => {
|
|
describe("SessionMembershipData", () => {
|
|
const membershipTemplate = sessionMembershipTemplate;
|
|
beforeEach(() => {
|
|
jest.useFakeTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it("rejects membership with no device_id", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { device_id: undefined })));
|
|
}).toThrow();
|
|
});
|
|
|
|
it("rejects membership with no call_id", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { call_id: undefined })));
|
|
}).toThrow();
|
|
});
|
|
|
|
it("allow membership with no scope", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, Object.assign({}, membershipTemplate, { scope: undefined })));
|
|
}).not.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 })),
|
|
);
|
|
expect(membership.createdTs()).toEqual(67890);
|
|
});
|
|
|
|
it("considers memberships unexpired if local age low enough", () => {
|
|
const fakeEvent = makeMockEvent(1000, membershipTemplate);
|
|
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION - 1));
|
|
expect(new CallMembership(fakeEvent).isExpired()).toEqual(false);
|
|
});
|
|
|
|
it("considers memberships expired if local age large enough", () => {
|
|
const fakeEvent = makeMockEvent(1000, membershipTemplate);
|
|
fakeEvent.getTs = jest.fn().mockReturnValue(Date.now() - (DEFAULT_EXPIRE_DURATION + 1));
|
|
expect(new CallMembership(fakeEvent).isExpired()).toEqual(true);
|
|
});
|
|
|
|
it("returns preferred foci", () => {
|
|
const mockFocus = { type: "livekit", livekit_service_url: "https://example.org" };
|
|
const fakeEvent = makeMockEvent(0, { ...membershipTemplate, foci_preferred: [mockFocus] });
|
|
const membership = new CallMembership(fakeEvent);
|
|
expect(membership.transports.length).toEqual(1);
|
|
expect(membership.transports[0]).toEqual(expect.objectContaining({ type: "livekit" }));
|
|
});
|
|
|
|
describe("getTransport", () => {
|
|
const mockFocus = { type: "livekit", livekit_service_url: "https://example.org" };
|
|
const oldestMembership = new CallMembership(makeMockEvent(0, membershipTemplate));
|
|
it("gets the correct active transport with oldest_membership", () => {
|
|
const membership = new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
// The list of foci_preferred provided by the homeserver. (in the test example we just provide one)
|
|
// The oldest member logic will use the first item in this list.
|
|
// The multi-sfu logic will (theoretically) also use all the items in the list at once
|
|
// (currently the js-sdk sets it to only one item in multi-sfu mode).
|
|
foci_preferred: [mockFocus],
|
|
focus_active: { type: "livekit", focus_selection: "oldest_membership" },
|
|
}),
|
|
);
|
|
|
|
// if we are the oldest member we use our focus.
|
|
expect(membership.getTransport(membership)).toEqual(expect.objectContaining({ type: "livekit" }));
|
|
expect(membership.transports[0]).toEqual(expect.objectContaining({ type: "livekit" }));
|
|
|
|
// If there is an older member we use its focus.
|
|
expect(membership.getTransport(oldestMembership)).toEqual(expect.objectContaining({ type: "livekit" }));
|
|
});
|
|
|
|
it("gets the correct active transport with multi_sfu", () => {
|
|
const membership = new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
foci_preferred: [mockFocus],
|
|
focus_active: { type: "livekit", focus_selection: "multi_sfu" },
|
|
}),
|
|
);
|
|
|
|
// We use our focus.
|
|
expect(membership.getTransport(membership)).toStrictEqual(mockFocus);
|
|
|
|
// If there is an older member we still use our own focus in multi sfu.
|
|
expect(membership.getTransport(oldestMembership)).toBe(mockFocus);
|
|
});
|
|
it("does not provide focus if the selection method is unknown", () => {
|
|
const membership = new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
foci_preferred: [mockFocus],
|
|
focus_active: { type: "livekit", focus_selection: "unknown" },
|
|
}),
|
|
);
|
|
|
|
expect(membership.getTransport(membership)).toBeUndefined();
|
|
});
|
|
});
|
|
describe("correct values from computed fields", () => {
|
|
const membership = new CallMembership(makeMockEvent(0, membershipTemplate));
|
|
it("returns correct sender", () => {
|
|
expect(membership.sender).toBe("@alice:example.org");
|
|
});
|
|
it("returns correct eventId", () => {
|
|
expect(membership.eventId).toBe("$eventid");
|
|
});
|
|
it("returns correct slot_id", () => {
|
|
expect(membership.slotId).toBe("m.call#");
|
|
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
|
});
|
|
it("returns correct deviceId", () => {
|
|
expect(membership.deviceId).toBe("AAAAAAA");
|
|
});
|
|
it("returns correct call intent", () => {
|
|
expect(membership.callIntent).toBe("voice");
|
|
});
|
|
it("returns correct application", () => {
|
|
expect(membership.application).toStrictEqual("m.call");
|
|
});
|
|
it("returns correct applicationData", () => {
|
|
expect(membership.applicationData).toStrictEqual({ "type": "m.call", "m.call.intent": "voice" });
|
|
});
|
|
it("returns correct scope", () => {
|
|
expect(membership.scope).toBe("m.room");
|
|
});
|
|
it("returns correct membershipID", () => {
|
|
expect(membership.membershipID).toBe("0");
|
|
});
|
|
it("returns correct unused fields", () => {
|
|
expect(membership.getAbsoluteExpiry()).toBe(DEFAULT_EXPIRE_DURATION);
|
|
expect(membership.getMsUntilExpiry()).toBe(DEFAULT_EXPIRE_DURATION - Date.now());
|
|
expect(membership.isExpired()).toBe(true);
|
|
});
|
|
});
|
|
describe("expiry calculation", () => {
|
|
beforeEach(() => jest.useFakeTimers());
|
|
afterEach(() => jest.useRealTimers());
|
|
|
|
it("calculates time until expiry", () => {
|
|
// server origin timestamp for this event is 1000
|
|
const fakeEvent = makeMockEvent(1000, membershipTemplate);
|
|
const membership = new CallMembership(fakeEvent);
|
|
jest.setSystemTime(2000);
|
|
// should be using absolute expiry time
|
|
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("RtcMembershipData", () => {
|
|
const membershipTemplate = rtcMembershipTemplate;
|
|
|
|
it("rejects membership with no slot_id", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, slot_id: undefined }));
|
|
}).toThrow();
|
|
});
|
|
|
|
it("rejects membership with invalid slot_id", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, slot_id: "invalid_slot_id" }));
|
|
}).toThrow();
|
|
});
|
|
it("accepts membership with valid slot_id", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, slot_id: "m.call#" }));
|
|
}).not.toThrow();
|
|
});
|
|
|
|
it("rejects membership with no application", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, application: undefined }));
|
|
}).toThrow();
|
|
});
|
|
|
|
it("rejects membership with incorrect application", () => {
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
application: { wrong_type_key: "unknown" },
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
});
|
|
|
|
it("rejects membership with no member", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, member: undefined }));
|
|
}).toThrow();
|
|
});
|
|
|
|
it("rejects membership with incorrect member", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, member: { i: "test" } }));
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
member: { id: "test", device_id: "test", user_id_wrong: "test" },
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
member: { id: "test", device_id_wrong: "test", user_id_wrong: "test" },
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
member: { id: "test", device_id: "test", user_id: "@@test" },
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
member: { id: "test", device_id: "test", user_id: "@test-wrong-user:user.id" },
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
});
|
|
it("rejects membership with incorrect sticky_key", () => {
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, membershipTemplate));
|
|
}).not.toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
sticky_key: 1,
|
|
msc4354_sticky_key: undefined,
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
sticky_key: "1",
|
|
msc4354_sticky_key: undefined,
|
|
}),
|
|
);
|
|
}).not.toThrow();
|
|
expect(() => {
|
|
new CallMembership(makeMockEvent(0, { ...membershipTemplate, msc4354_sticky_key: undefined }));
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
msc4354_sticky_key: 1,
|
|
sticky_key: "valid",
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
msc4354_sticky_key: "valid",
|
|
sticky_key: "valid",
|
|
}),
|
|
);
|
|
}).not.toThrow();
|
|
expect(() => {
|
|
new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
msc4354_sticky_key: "valid_but_different",
|
|
sticky_key: "valid",
|
|
}),
|
|
);
|
|
}).toThrow();
|
|
});
|
|
|
|
it("considers memberships unexpired if local age low enough", () => {
|
|
const now = Date.now();
|
|
const startEv = makeMockEvent(now - DEFAULT_EXPIRE_DURATION + 100, membershipTemplate);
|
|
const membershipWithRel = new CallMembership(
|
|
//update 900 ms later
|
|
makeMockEvent(now - DEFAULT_EXPIRE_DURATION + 1000, membershipTemplate),
|
|
startEv,
|
|
);
|
|
const membershipWithoutRel = new CallMembership(startEv);
|
|
expect(membershipWithRel.isExpired()).toEqual(false);
|
|
expect(membershipWithoutRel.isExpired()).toEqual(false);
|
|
expect(membershipWithoutRel.createdTs()).toEqual(membershipWithRel.createdTs());
|
|
});
|
|
|
|
it("considers memberships expired if local age large enough", () => {
|
|
const now = Date.now();
|
|
const startEv = makeMockEvent(now - DEFAULT_EXPIRE_DURATION - 100, membershipTemplate);
|
|
const membershipWithRel = new CallMembership(
|
|
//update 50 ms later (so the update is still expired)
|
|
makeMockEvent(now - DEFAULT_EXPIRE_DURATION - 50, membershipTemplate),
|
|
startEv,
|
|
);
|
|
const membershipWithRelUnexpired = new CallMembership(
|
|
//update 200 ms later (due to the update the member is NOT expired)
|
|
makeMockEvent(now - DEFAULT_EXPIRE_DURATION + 100, membershipTemplate),
|
|
startEv,
|
|
);
|
|
const membershipWithoutRel = new CallMembership(startEv);
|
|
expect(membershipWithRel.isExpired()).toEqual(true);
|
|
expect(membershipWithRelUnexpired.isExpired()).toEqual(false);
|
|
expect(membershipWithoutRel.isExpired()).toEqual(true);
|
|
expect(membershipWithoutRel.createdTs()).toEqual(membershipWithRel.createdTs());
|
|
});
|
|
|
|
describe("getTransport", () => {
|
|
it("gets the correct active transport with oldest_membership", () => {
|
|
const oldestMembership = new CallMembership(
|
|
makeMockEvent(0, {
|
|
...membershipTemplate,
|
|
rtc_transports: [{ type: "oldest_transport" }],
|
|
}),
|
|
);
|
|
const membership = new CallMembership(makeMockEvent(0, membershipTemplate));
|
|
|
|
// if we are the oldest member we use our focus.
|
|
expect(membership.getTransport(membership)).toStrictEqual({ type: "livekit" });
|
|
|
|
// If there is an older member we use our own focus focus. (RtcMembershipData always uses multi sfu)
|
|
expect(membership.getTransport(oldestMembership)).toStrictEqual({ type: "livekit" });
|
|
});
|
|
});
|
|
|
|
describe("correct values from computed fields", () => {
|
|
const now = Date.now();
|
|
const membership = new CallMembership(makeMockEvent(now, membershipTemplate));
|
|
it("returns correct sender", () => {
|
|
expect(membership.sender).toBe("@alice:example.org");
|
|
});
|
|
it("returns correct eventId", () => {
|
|
expect(membership.eventId).toBe("$eventid");
|
|
});
|
|
it("returns correct slot_id", () => {
|
|
expect(membership.slotId).toBe("m.call#");
|
|
expect(membership.slotDescription).toStrictEqual({ id: "", application: "m.call" });
|
|
});
|
|
it("returns correct deviceId", () => {
|
|
expect(membership.deviceId).toBe("AAAAAAA");
|
|
});
|
|
it("returns correct call intent", () => {
|
|
expect(membership.callIntent).toBe("voice");
|
|
});
|
|
it("returns correct application", () => {
|
|
expect(membership.application).toStrictEqual("m.call");
|
|
});
|
|
it("returns correct applicationData", () => {
|
|
expect(membership.applicationData).toStrictEqual({
|
|
"type": "m.call",
|
|
"m.call.id": "",
|
|
"m.call.intent": "voice",
|
|
});
|
|
});
|
|
it("returns correct scope", () => {
|
|
expect(membership.scope).toBe(undefined);
|
|
});
|
|
it("returns correct membershipID", () => {
|
|
expect(membership.membershipID).toBe("xyzHASHxyz");
|
|
});
|
|
it("returns correct expiration fields", () => {
|
|
expect(membership.getAbsoluteExpiry()).toBe(now + DEFAULT_EXPIRE_DURATION);
|
|
expect(membership.getMsUntilExpiry()).toBe(now + DEFAULT_EXPIRE_DURATION - Date.now());
|
|
expect(membership.isExpired()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("expiry calculation", () => {
|
|
beforeEach(() => jest.useFakeTimers());
|
|
afterEach(() => jest.useRealTimers());
|
|
|
|
afterEach(() => {
|
|
jest.useRealTimers();
|
|
});
|
|
|
|
it("calculates time until expiry", () => {
|
|
// server origin timestamp for this event is 1000
|
|
// The related event used for created_ts is at 500
|
|
const fakeEvent = makeMockEvent(1000, membershipTemplate);
|
|
const initialEvent = makeMockEvent(500, membershipTemplate);
|
|
const membership = new CallMembership(fakeEvent, initialEvent);
|
|
jest.setSystemTime(2000);
|
|
// should be using absolute expiry time from the new event (ts = 1000)
|
|
expect(membership.getMsUntilExpiry()).toEqual(DEFAULT_EXPIRE_DURATION - 1000);
|
|
});
|
|
});
|
|
});
|
|
});
|