small refactor to make it testable.

This commit is contained in:
Timo K
2025-12-15 20:17:57 +01:00
parent 909d980dff
commit 5bc6ed5885
8 changed files with 134 additions and 60 deletions

View File

@@ -23,6 +23,7 @@ import {
import {
exampleTransport,
mockComputeLivekitParticipantIdentity$,
mockMatrixRoomMember,
mockRtcMembership,
} from "../utils/test";
@@ -47,6 +48,13 @@ vitest.mock("../rtcSessionHelpers", async (importOriginal) => ({
...(await importOriginal()),
makeTransport: (): [LivekitTransport] => [exampleTransport],
}));
vitest.mock(
import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"),
async (importOriginal) => ({
...(await importOriginal()),
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
}),
);
afterEach(() => {
vitest.clearAllMocks();

View File

@@ -22,8 +22,8 @@ import { BrowserRouter } from "react-router-dom";
import { TooltipProvider } from "@vector-im/compound-web";
import { RoomContext, useLocalParticipant } from "@livekit/components-react";
import { InCallView } from "./InCallView";
import {
mockComputeLivekitParticipantIdentity$,
mockLivekitRoom,
mockLocalParticipant,
mockMatrixRoom,
@@ -34,6 +34,7 @@ import {
mockRtcMembership,
type MockRTCSession,
} from "../utils/test";
import { InCallView } from "./InCallView";
import { E2eeType } from "../e2ee/e2eeType";
import { getBasicCallViewModelEnvironment } from "../utils/test-viewmodel";
import { alice, local } from "../utils/test-fixtures";
@@ -61,6 +62,13 @@ vi.mock("../livekit/MatrixAudioRenderer");
vi.mock("react-use-measure", () => ({
default: (): [() => void, object] => [(): void => {}, {}],
}));
vi.mock(
import("../state/CallViewModel/remoteMembers/LivekitParticipantIdentity.ts"),
async (importOriginal) => ({
...(await importOriginal()),
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
}),
);
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
const localParticipant = mockLocalParticipant({

View File

@@ -44,6 +44,7 @@ import {
mockRtcMembership,
testScope,
exampleTransport,
mockComputeLivekitParticipantIdentity$,
} from "../../utils/test.ts";
import { E2eeType } from "../../e2ee/e2eeType.ts";
import {
@@ -77,11 +78,22 @@ vi.mock("../e2ee/matrixKeyProvider");
const getUrlParams = vi.hoisted(() => vi.fn(() => ({})));
vi.mock("../UrlParams", () => ({ getUrlParams }));
vi.mock("../rtcSessionHelpers", async (importOriginal) => ({
...(await importOriginal()),
makeTransport: async (): Promise<LivekitTransport> =>
Promise.resolve(exampleTransport),
}));
vi.mock(
"../state/CallViewModel/localMember/localTransport",
async (importOriginal) => ({
...(await importOriginal()),
makeTransport: async (): Promise<LivekitTransport> =>
Promise.resolve(exampleTransport),
}),
);
vi.mock(
import("./remoteMembers/LivekitParticipantIdentity.ts"),
async (importOriginal) => ({
...(await importOriginal()),
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
}),
);
const yesNo = {
y: true,

View File

@@ -0,0 +1,53 @@
/*
Copyright 2025 New Vector Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE in the repository root for full details.
*/
import { encodeUnpaddedBase64Url } from "matrix-js-sdk";
import { sha256 } from "matrix-js-sdk/lib/digest";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { from, type Observable } from "rxjs";
const livekitParticipantIdentityCache = new Map<string, string>();
/**
* The string that is computed based on the membership and used for the computing the hash.
* `${userId}:${deviceId}:${membershipID}`
* as the direct imput for: await sha256(input)
*/
export const livekitIdentityInput = ({
userId,
deviceId,
memberId,
}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`;
export function computeLivekitParticipantIdentity$(
membership: CallMembershipIdentityParts,
kind: "rtc" | "session",
): Observable<string> {
const compute = async (): Promise<string> => {
switch (kind) {
case "rtc": {
const input = livekitIdentityInput(membership);
if (livekitParticipantIdentityCache.size > 400)
// prevent memory leaks in a stupid/simple way
livekitParticipantIdentityCache.clear();
// TODO use non deprecated memberId
if (livekitParticipantIdentityCache.has(input))
return livekitParticipantIdentityCache.get(input)!;
else {
const hashBuffer = await sha256(input);
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
livekitParticipantIdentityCache.set(input, hashedString);
return hashedString;
}
}
case "session":
default:
return `${membership.userId}:${membership.deviceId}`;
}
};
return from(compute());
}

View File

@@ -28,7 +28,6 @@ import {
flushPromises,
mockCallMembership,
mockRemoteParticipant,
withTestScheduler,
} from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts";
import { constant } from "../../Behavior.ts";

View File

@@ -15,15 +15,13 @@ import {
} from "matrix-js-sdk/lib/matrixrtc";
import { combineLatest, filter, map, switchMap } from "rxjs";
import { logger as rootLogger } from "matrix-js-sdk/lib/logger";
import { sha256 } from "matrix-js-sdk/lib/digest";
import { encodeUnpaddedBase64Url } from "matrix-js-sdk";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import { type Behavior } from "../../Behavior";
import { type IConnectionManager } from "./ConnectionManager";
import { Epoch, type ObservableScope } from "../../ObservableScope";
import { type Connection } from "./Connection";
import { generateItemsWithEpoch } from "../../../utils/observable";
import { computeLivekitParticipantIdentity$ } from "./LivekitParticipantIdentity";
const logger = rootLogger.getChild("[MatrixLivekitMembers]");
@@ -73,25 +71,28 @@ export function createMatrixLivekitMembers$({
*/
const membershipsWithTransportAndLivekitIdentity$ =
membershipsWithTransport$.pipe(
switchMap(async (membershipsWithTransport) => {
switchMap((membershipsWithTransport) => {
const { value, epoch } = membershipsWithTransport;
const membershipsWithTransportAndLkIdentityPromises = value.map(
async (obj) => {
return computeLivekitParticipantIdentity(
(obj) => {
return computeLivekitParticipantIdentity$(
obj.membership,
obj.membership.kind,
);
},
);
const identities = await Promise.all(
return combineLatest(
membershipsWithTransportAndLkIdentityPromises,
).pipe(
map((identities) => {
const membershipsWithTransportAndLkIdentity = value.map(
({ transport, membership }, index) => {
return { transport, membership, identity: identities[index] };
},
);
return new Epoch(membershipsWithTransportAndLkIdentity, epoch);
}),
);
const membershipsWithTransportAndLkIdentity = value.map(
({ transport, membership }, index) => {
return { transport, membership, identity: identities[index] };
},
);
return new Epoch(membershipsWithTransportAndLkIdentity, epoch);
}),
);
@@ -164,42 +165,3 @@ export function areLivekitTransportsEqual(
if (!t1 && !t2) return true;
return false;
}
const livekitParticipantIdentityCache = new Map<string, string>();
/**
* The string that is computed based on the membership and used for the computing the hash.
* `${userId}:${deviceId}:${membershipID}`
* as the direct imput for: await sha256(input)
*/
export const livekitIdentityInput = ({
userId,
deviceId,
memberId,
}: CallMembershipIdentityParts): string => `${userId}|${deviceId}|${memberId}`;
export async function computeLivekitParticipantIdentity(
membership: CallMembershipIdentityParts,
kind: "rtc" | "session",
): Promise<string> {
switch (kind) {
case "rtc": {
const input = livekitIdentityInput(membership);
if (livekitParticipantIdentityCache.size > 400)
// prevent memory leaks in a stupid/simple way
livekitParticipantIdentityCache.clear();
// TODO use non deprecated memberId
if (livekitParticipantIdentityCache.has(input))
return livekitParticipantIdentityCache.get(input)!;
else {
const hashBuffer = await sha256(input);
const hashedString = encodeUnpaddedBase64Url(hashBuffer);
livekitParticipantIdentityCache.set(input, hashedString);
return hashedString;
}
}
case "session":
default:
return `${membership.userId}:${membership.deviceId}`;
}
}

View File

@@ -22,6 +22,7 @@ import { ECConnectionFactory } from "./ConnectionFactory.ts";
import { type OpenIDClientParts } from "../../../livekit/openIDSFU.ts";
import {
mockCallMembership,
mockComputeLivekitParticipantIdentity$,
mockMediaDevices,
withTestScheduler,
} from "../../../utils/test.ts";
@@ -43,6 +44,11 @@ let lkRoomFactory: () => LivekitRoom;
const createdMockLivekitRooms: Map<string, LivekitRoom> = new Map();
vi.mock(import("./LivekitParticipantIdentity.ts"), async (importOriginal) => ({
...(await importOriginal()),
computeLivekitParticipantIdentity$: mockComputeLivekitParticipantIdentity$,
}));
beforeEach(() => {
testScope = new ObservableScope();
mockClient = {

View File

@@ -15,6 +15,7 @@ import {
vitest,
} from "vitest";
import {
encodeUnpaddedBase64,
MatrixEvent,
type Room as MatrixRoom,
type Room,
@@ -43,13 +44,14 @@ import {
type Room as LivekitRoom,
Track,
} from "livekit-client";
import { randomUUID } from "crypto";
import { createHash, randomUUID } from "crypto";
import { type TrackReference } from "@livekit/components-core";
import EventEmitter from "events";
import {
type KeyTransportEvents,
type KeyTransportEventsHandlerMap,
} from "matrix-js-sdk/lib/matrixrtc/IKeyTransport";
import { type CallMembershipIdentityParts } from "matrix-js-sdk/lib/matrixrtc/EncryptionManager";
import {
LocalUserMediaViewModel,
@@ -522,3 +524,27 @@ export function mockMuteStates(
const observableScope = new ObservableScope();
return new MuteStates(observableScope, mockMediaDevices({}), joined$);
}
export const mockComputeLivekitParticipantIdentity$ = (
membership: CallMembershipIdentityParts,
kind: "rtc" | "session",
): Observable<string> => {
function sha256(commitmentStr: string): string {
return encodeUnpaddedBase64(
createHash("sha256").update(commitmentStr, "utf8").digest(),
);
}
let hash;
switch (kind) {
case "rtc": {
hash = sha256(
`${membership.userId}|${membership.deviceId}|${membership.memberId}`,
);
break;
}
case "session":
default:
hash = `${membership.userId}:${membership.deviceId}`;
}
return of(hash);
};