test: fixup MatrixLivekitMembers tests

This commit is contained in:
Valere
2025-11-10 10:43:53 +01:00
parent 92ddc4c797
commit 5c83e0dce1

View File

@@ -12,19 +12,23 @@ import {
} from "matrix-js-sdk/lib/matrixrtc"; } from "matrix-js-sdk/lib/matrixrtc";
import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk"; import { type Room as MatrixRoom, type RoomMember } from "matrix-js-sdk";
import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils"; import { getParticipantId } from "matrix-js-sdk/lib/matrixrtc/utils";
import { combineLatest, map, type Observable } from "rxjs";
import { type IConnectionManager } from "./ConnectionManager.ts"; import { type IConnectionManager } from "./ConnectionManager.ts";
import { import {
type MatrixLivekitMember, type MatrixLivekitMember,
createMatrixLivekitMembers$, createMatrixLivekitMembers$,
areLivekitTransportsEqual,
} from "./MatrixLivekitMembers.ts"; } from "./MatrixLivekitMembers.ts";
import { ObservableScope } from "../../ObservableScope.ts"; import {
Epoch,
mapEpoch,
ObservableScope,
trackEpoch,
} from "../../ObservableScope.ts";
import { ConnectionManagerData } from "./ConnectionManager.ts"; import { ConnectionManagerData } from "./ConnectionManager.ts";
import { import {
mockCallMembership, mockCallMembership,
mockRemoteParticipant, mockRemoteParticipant,
type OurRunHelpers,
withTestScheduler, withTestScheduler,
} from "../../../utils/test.ts"; } from "../../../utils/test.ts";
import { type Connection } from "./Connection.ts"; import { type Connection } from "./Connection.ts";
@@ -32,7 +36,28 @@ import { type Connection } from "./Connection.ts";
let testScope: ObservableScope; let testScope: ObservableScope;
let mockMatrixRoom: MatrixRoom; let mockMatrixRoom: MatrixRoom;
// The merger beeing tested const transportA: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
};
const transportB: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.sample.com",
livekit_alias: "!alias:sample.com",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transportA,
);
const carlMembership = mockCallMembership(
"@carl:sample.com",
"DEV111",
transportB,
);
beforeEach(() => { beforeEach(() => {
testScope = new ObservableScope(); testScope = new ObservableScope();
@@ -53,113 +78,138 @@ afterEach(() => {
testScope.end(); testScope.end();
}); });
function epochMeWith$<T, U>(
source$: Observable<Epoch<U>>,
me$: Observable<T>,
): Observable<Epoch<T>> {
return combineLatest([source$, me$]).pipe(
map(([ep, cd]) => {
return new Epoch(cd, ep.epoch);
}),
);
}
test("should signal participant not yet connected to livekit", () => { test("should signal participant not yet connected to livekit", () => {
withTestScheduler(({ behavior, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const bobMembership = { const { memberships$, membershipsWithTransport$ } = fromMemberships$(
userId: "@bob:example.org", behavior("a", {
deviceId: "DEV000", a: [bobMembership],
transports: [ }),
{ );
type: "livekit",
livekit_service_url: "https://lk.example.org", const connectionManagerData$ = epochMeWith$(
livekit_alias: "!alias:example.org", memberships$,
}, behavior("a", {
], a: new ConnectionManagerData(),
} as unknown as CallMembership; }),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: behavior("a", { membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
a: [
{
membership: bobMembership,
},
],
}),
connectionManager: { connectionManager: {
connectionManagerData$: behavior("a", { connectionManagerData$: connectionManagerData$,
a: new ConnectionManagerData(), } as unknown as IConnectionManager,
}),
transports$: behavior("a", { a: [] }),
connections$: behavior("a", { a: [] }),
},
matrixRoom: mockMatrixRoom, matrixRoom: mockMatrixRoom,
}); });
expectObservable(matrixLivekitMember$).toBe("a", { expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => { a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
return ( expect(data.length).toEqual(1);
data.length == 1 && expectObservable(data[0].membership$).toBe("a", {
data[0].membership === bobMembership && a: bobMembership,
data[0].participant === undefined && });
data[0].connection === undefined expectObservable(data[0].participant$).toBe("a", {
); a: null,
});
expectObservable(data[0].connection$).toBe("a", {
a: null,
});
return true;
}), }),
}); });
}); });
}); });
function aConnectionManager( // Helper to create epoch'ed memberships$ and membershipsWithTransport$ from memberships observable.
data: ConnectionManagerData, function fromMemberships$(m$: Observable<CallMembership[]>): {
behavior: OurRunHelpers["behavior"], memberships$: Observable<Epoch<CallMembership[]>>;
): IConnectionManager { membershipsWithTransport$: Observable<
return { Epoch<{ membership: CallMembership; transport?: LivekitTransport }[]>
connectionManagerData$: behavior("a", { a: data }), >;
transports$: behavior("a", { } {
a: data.getConnections().map((connection) => connection.transport), const memberships$ = m$.pipe(trackEpoch());
const membershipsWithTransport$ = memberships$.pipe(
mapEpoch((members) => {
return members.map((m) => {
const tr = m.getTransport(m);
return {
membership: m,
transport:
tr?.type === "livekit" ? (tr as LivekitTransport) : undefined,
};
});
}), }),
connections$: behavior("a", { a: data.getConnections() }), );
return {
memberships$,
membershipsWithTransport$,
}; };
} }
test("should signal participant on a connection that is publishing", () => { test("should signal participant on a connection that is publishing", () => {
withTestScheduler(({ behavior, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const transport: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.example.org",
livekit_alias: "!alias:example.org",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transport,
);
const connectionWithPublisher = new ConnectionManagerData();
const bobParticipantId = getParticipantId( const bobParticipantId = getParticipantId(
bobMembership.userId, bobMembership.userId,
bobMembership.deviceId, bobMembership.deviceId,
); );
const { memberships$, membershipsWithTransport$ } = fromMemberships$(
behavior("a", {
a: [bobMembership],
}),
);
const connection = { const connection = {
transport: transport, transport: bobMembership.getTransport(bobMembership),
} as unknown as Connection; } as unknown as Connection;
connectionWithPublisher.add(connection, [ const dataWithPublisher = new ConnectionManagerData();
dataWithPublisher.add(connection, [
mockRemoteParticipant({ identity: bobParticipantId }), mockRemoteParticipant({ identity: bobParticipantId }),
]); ]);
const connectionManagerData$ = epochMeWith$(
memberships$,
behavior("a", {
a: dataWithPublisher,
}),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: behavior("a", { membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
a: [ connectionManager: {
{ connectionManagerData$: connectionManagerData$,
membership: bobMembership, } as unknown as IConnectionManager,
transport,
},
],
}),
connectionManager: aConnectionManager(connectionWithPublisher, behavior),
matrixRoom: mockMatrixRoom, matrixRoom: mockMatrixRoom,
}); });
expectObservable(matrixLivekitMember$).toBe("a", { expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => { a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1); expect(data.length).toEqual(1);
expect(data[0].participant).toBeDefined(); expectObservable(data[0].membership$).toBe("a", {
expect(data[0].connection).toBeDefined(); a: bobMembership,
expect(data[0].membership).toEqual(bobMembership); });
expect( expectObservable(data[0].participant$).toBe("a", {
areLivekitTransportsEqual(data[0].connection!.transport, transport), a: expect.toSatisfy((participant) => {
).toBe(true); expect(participant).toBeDefined();
expect(participant!.identity).toEqual(bobParticipantId);
return true;
}),
});
expectObservable(data[0].connection$).toBe("a", {
a: connection,
});
return true; return true;
}), }),
}); });
@@ -168,47 +218,46 @@ test("should signal participant on a connection that is publishing", () => {
test("should signal participant on a connection that is not publishing", () => { test("should signal participant on a connection that is not publishing", () => {
withTestScheduler(({ behavior, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const transport: LivekitTransport = { const { memberships$, membershipsWithTransport$ } = fromMemberships$(
type: "livekit", behavior("a", {
livekit_service_url: "https://lk.example.org", a: [bobMembership],
livekit_alias: "!alias:example.org", }),
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transport,
); );
const connectionWithPublisher = new ConnectionManagerData();
// const bobParticipantId = getParticipantId(bobMembership.userId, bobMembership.deviceId);
const connection = { const connection = {
transport: transport, transport: bobMembership.getTransport(bobMembership),
} as unknown as Connection; } as unknown as Connection;
connectionWithPublisher.add(connection, []); const dataWithPublisher = new ConnectionManagerData();
dataWithPublisher.add(connection, []);
const connectionManagerData$ = epochMeWith$(
memberships$,
behavior("a", {
a: dataWithPublisher,
}),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: behavior("a", { membershipsWithTransport$: testScope.behavior(membershipsWithTransport$),
a: [ connectionManager: {
{ connectionManagerData$: connectionManagerData$,
membership: bobMembership, } as unknown as IConnectionManager,
transport,
},
],
}),
connectionManager: aConnectionManager(connectionWithPublisher, behavior),
matrixRoom: mockMatrixRoom, matrixRoom: mockMatrixRoom,
}); });
expectObservable(matrixLivekitMember$).toBe("a", { expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe("a", {
a: expect.toSatisfy((data: MatrixLivekitMember[]) => { a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data.length).toEqual(1); expect(data.length).toEqual(1);
expect(data[0].participant).not.toBeDefined(); expectObservable(data[0].membership$).toBe("a", {
expect(data[0].connection).toBeDefined(); a: bobMembership,
expect(data[0].membership).toEqual(bobMembership); });
expect( expectObservable(data[0].participant$).toBe("a", {
areLivekitTransportsEqual(data[0].connection!.transport, transport), a: null,
).toBe(true); });
expectObservable(data[0].connection$).toBe("a", {
a: connection,
});
return true; return true;
}), }),
}); });
@@ -218,22 +267,10 @@ test("should signal participant on a connection that is not publishing", () => {
describe("Publication edge case", () => { describe("Publication edge case", () => {
test("bob is publishing in several connections", () => { test("bob is publishing in several connections", () => {
withTestScheduler(({ behavior, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const transportA: LivekitTransport = { const { memberships$, membershipsWithTransport$ } = fromMemberships$(
type: "livekit", behavior("a", {
livekit_service_url: "https://lk.example.org", a: [bobMembership, carlMembership],
livekit_alias: "!alias:example.org", }),
};
const transportB: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.sample.com",
livekit_alias: "!alias:sample.com",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transportA,
); );
const connectionWithPublisher = new ConnectionManagerData(); const connectionWithPublisher = new ConnectionManagerData();
@@ -254,60 +291,57 @@ describe("Publication edge case", () => {
connectionWithPublisher.add(connectionB, [ connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }), mockRemoteParticipant({ identity: bobParticipantId }),
]); ]);
const connectionManagerData$ = epochMeWith$(
memberships$,
behavior("a", {
a: connectionWithPublisher,
}),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: behavior("a", { membershipsWithTransport$: testScope.behavior(
a: [ membershipsWithTransport$,
{
membership: bobMembership,
transport: transportA,
},
],
}),
connectionManager: aConnectionManager(
connectionWithPublisher,
behavior,
), ),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
matrixRoom: mockMatrixRoom, matrixRoom: mockMatrixRoom,
}); });
expectObservable(matrixLivekitMember$).toBe("a", { expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe(
a: expect.toSatisfy((data: MatrixLivekitMember[]) => { "a",
expect(data.length).toEqual(1); {
expect(data[0].participant).toBeDefined(); a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data[0].participant!.identity).toEqual(bobParticipantId); expect(data.length).toEqual(2);
expect(data[0].connection).toBeDefined(); expectObservable(data[0].membership$).toBe("a", {
expect(data[0].membership).toEqual(bobMembership); a: bobMembership,
expect( });
areLivekitTransportsEqual( expectObservable(data[0].connection$).toBe("a", {
data[0].connection!.transport, // The real connection should be from transportA as per the membership
transportA, a: connectionA,
), });
).toBe(true); expectObservable(data[0].participant$).toBe("a", {
return true; a: expect.toSatisfy((participant) => {
}), expect(participant).toBeDefined();
}); expect(participant!.identity).toEqual(bobParticipantId);
return true;
}),
});
return true;
}),
},
);
}); });
}); });
test("bob is publishing in the wrong connection", () => { test("bob is publishing in the wrong connection", () => {
withTestScheduler(({ behavior, expectObservable }) => { withTestScheduler(({ behavior, expectObservable }) => {
const transportA: LivekitTransport = { const { memberships$, membershipsWithTransport$ } = fromMemberships$(
type: "livekit", behavior("a", {
livekit_service_url: "https://lk.example.org", a: [bobMembership, carlMembership],
livekit_alias: "!alias:example.org", }),
};
const transportB: LivekitTransport = {
type: "livekit",
livekit_service_url: "https://lk.sample.com",
livekit_alias: "!alias:sample.com",
};
const bobMembership = mockCallMembership(
"@bob:example.org",
"DEV000",
transportA,
); );
const connectionWithPublisher = new ConnectionManagerData(); const connectionWithPublisher = new ConnectionManagerData();
@@ -315,86 +349,54 @@ describe("Publication edge case", () => {
bobMembership.userId, bobMembership.userId,
bobMembership.deviceId, bobMembership.deviceId,
); );
const connectionA = { const connectionA = { transport: transportA } as unknown as Connection;
transport: transportA, const connectionB = { transport: transportB } as unknown as Connection;
} as unknown as Connection;
const connectionB = {
transport: transportB,
} as unknown as Connection;
// Bob is not publishing on A
connectionWithPublisher.add(connectionA, []); connectionWithPublisher.add(connectionA, []);
// Bob is publishing on B but his membership says A
connectionWithPublisher.add(connectionB, [ connectionWithPublisher.add(connectionB, [
mockRemoteParticipant({ identity: bobParticipantId }), mockRemoteParticipant({ identity: bobParticipantId }),
]); ]);
const connectionManagerData$ = epochMeWith$(
memberships$,
behavior("a", {
a: connectionWithPublisher,
}),
);
const matrixLivekitMember$ = createMatrixLivekitMembers$({ const matrixLivekitMember$ = createMatrixLivekitMembers$({
scope: testScope, scope: testScope,
membershipsWithTransport$: behavior("a", { membershipsWithTransport$: testScope.behavior(
a: [ membershipsWithTransport$,
{
membership: bobMembership,
transport: transportA,
},
],
}),
connectionManager: aConnectionManager(
connectionWithPublisher,
behavior,
), ),
connectionManager: {
connectionManagerData$: connectionManagerData$,
} as unknown as IConnectionManager,
matrixRoom: mockMatrixRoom, matrixRoom: mockMatrixRoom,
}); });
expectObservable(matrixLivekitMember$).toBe("a", { expectObservable(matrixLivekitMember$.pipe(map((e) => e.value))).toBe(
a: expect.toSatisfy((data: MatrixLivekitMember[]) => { "a",
expect(data.length).toEqual(1); {
expect(data[0].participant).not.toBeDefined(); a: expect.toSatisfy((data: MatrixLivekitMember[]) => {
expect(data[0].connection).toBeDefined(); expect(data.length).toEqual(2);
expect(data[0].membership).toEqual(bobMembership); expectObservable(data[0].membership$).toBe("a", {
expect( a: bobMembership,
areLivekitTransportsEqual( });
data[0].connection!.transport, expectObservable(data[0].connection$).toBe("a", {
transportA, // The real connection should be from transportA as per the membership
), a: connectionA,
).toBe(true); });
return true; expectObservable(data[0].participant$).toBe("a", {
}), // No participant as Bob is not publishing on his membership transport
}); a: null,
});
return true;
}),
},
);
}); });
// let lastMatrixLkItems: MatrixLivekitMember[] = [];
// matrixLivekitMerger.matrixLivekitMember$.subscribe((items) => {
// lastMatrixLkItems = items;
// });
// vi.mocked(bobMembership).getTransport = vi
// .fn()
// .mockReturnValue(connectionA.transport);
// fakeMemberships$.next([bobMembership]);
// const lkMap = new ConnectionManagerData();
// lkMap.add(connectionA, []);
// lkMap.add(connectionB, [
// mockRemoteParticipant({ identity: bobParticipantId })
// ]);
// fakeManagerData$.next(lkMap);
// const items = lastMatrixLkItems;
// expect(items).toHaveLength(1);
// const item = items[0];
// // Assert the expected membership
// expect(item.membership.userId).toEqual(bobMembership.userId);
// expect(item.membership.deviceId).toEqual(bobMembership.deviceId);
// expect(item.participant).not.toBeDefined();
// // The transport info should come from the membership transports and not only from the publishing connection
// expect(item.connection?.transport?.livekit_service_url).toEqual(
// bobMembership.transports[0]?.livekit_service_url
// );
// expect(item.connection?.transport?.livekit_alias).toEqual(
// bobMembership.transports[0]?.livekit_alias
// );
}); });
}); });