@@ -20,7 +20,7 @@ import { SessionMembershipData } from "../../../src/matrixrtc/CallMembership";
2020import { MatrixRTCSession , MatrixRTCSessionEvent } from "../../../src/matrixrtc/MatrixRTCSession" ;
2121import { EncryptionKeysEventContent } from "../../../src/matrixrtc/types" ;
2222import { randomString } from "../../../src/randomstring" ;
23- import { makeMockRoom , makeMockRoomState , membershipTemplate } from "./mocks" ;
23+ import { makeMockRoom , makeMockRoomState , membershipTemplate , mockRTCEvent } from "./mocks" ;
2424
2525const mockFocus = { type : "mock" } ;
2626
@@ -269,6 +269,55 @@ describe("MatrixRTCSession", () => {
269269 } ) ;
270270 } ) ;
271271
272+ describe ( "getsActiveFocus" , ( ) => {
273+ const firstPreferredFocus = {
274+ type : "livekit" ,
275+ livekit_service_url : "https://active.url" ,
276+ livekit_alias : "!active:active.url" ,
277+ } ;
278+ it ( "gets the correct active focus with oldest_membership" , ( ) => {
279+ jest . useFakeTimers ( ) ;
280+ jest . setSystemTime ( 3000 ) ;
281+ const mockRoom = makeMockRoom ( [
282+ Object . assign ( { } , membershipTemplate , {
283+ device_id : "foo" ,
284+ created_ts : 500 ,
285+ foci_preferred : [ firstPreferredFocus ] ,
286+ } ) ,
287+ Object . assign ( { } , membershipTemplate , { device_id : "old" , created_ts : 1000 } ) ,
288+ Object . assign ( { } , membershipTemplate , { device_id : "bar" , created_ts : 2000 } ) ,
289+ ] ) ;
290+
291+ sess = MatrixRTCSession . roomSessionForRoom ( client , mockRoom ) ;
292+
293+ sess . joinRoomSession ( [ { type : "livekit" , livekit_service_url : "htts://test.org" } ] , {
294+ type : "livekit" ,
295+ focus_selection : "oldest_membership" ,
296+ } ) ;
297+ expect ( sess . getActiveFocus ( ) ) . toBe ( firstPreferredFocus ) ;
298+ jest . useRealTimers ( ) ;
299+ } ) ;
300+ it ( "does not provide focus if the selection method is unknown" , ( ) => {
301+ const mockRoom = makeMockRoom ( [
302+ Object . assign ( { } , membershipTemplate , {
303+ device_id : "foo" ,
304+ created_ts : 500 ,
305+ foci_preferred : [ firstPreferredFocus ] ,
306+ } ) ,
307+ Object . assign ( { } , membershipTemplate , { device_id : "old" , created_ts : 1000 } ) ,
308+ Object . assign ( { } , membershipTemplate , { device_id : "bar" , created_ts : 2000 } ) ,
309+ ] ) ;
310+
311+ sess = MatrixRTCSession . roomSessionForRoom ( client , mockRoom ) ;
312+
313+ sess . joinRoomSession ( [ { type : "livekit" , livekit_service_url : "htts://test.org" } ] , {
314+ type : "livekit" ,
315+ focus_selection : "unknown" ,
316+ } ) ;
317+ expect ( sess . getActiveFocus ( ) ) . toBe ( undefined ) ;
318+ } ) ;
319+ } ) ;
320+
272321 describe ( "joining" , ( ) => {
273322 let mockRoom : Room ;
274323 let sendStateEventMock : jest . Mock ;
@@ -319,6 +368,68 @@ describe("MatrixRTCSession", () => {
319368 expect ( sess ! . isJoined ( ) ) . toEqual ( true ) ;
320369 } ) ;
321370
371+ it ( "sends a membership event when joining a call" , async ( ) => {
372+ const realSetTimeout = setTimeout ;
373+ jest . useFakeTimers ( ) ;
374+ sess ! . joinRoomSession ( [ mockFocus ] , mockFocus ) ;
375+ await Promise . race ( [ sentStateEvent , new Promise ( ( resolve ) => realSetTimeout ( resolve , 500 ) ) ] ) ;
376+ expect ( client . sendStateEvent ) . toHaveBeenCalledWith (
377+ mockRoom ! . roomId ,
378+ EventType . GroupCallMemberPrefix ,
379+ {
380+ application : "m.call" ,
381+ scope : "m.room" ,
382+ call_id : "" ,
383+ device_id : "AAAAAAA" ,
384+ expires : DEFAULT_EXPIRE_DURATION ,
385+ foci_preferred : [ mockFocus ] ,
386+ focus_active : {
387+ focus_selection : "oldest_membership" ,
388+ type : "livekit" ,
389+ } ,
390+ } ,
391+ "_@alice:example.org_AAAAAAA" ,
392+ ) ;
393+ await Promise . race ( [ sentDelayedState , new Promise ( ( resolve ) => realSetTimeout ( resolve , 500 ) ) ] ) ;
394+ // Because we actually want to send the state
395+ expect ( client . sendStateEvent ) . toHaveBeenCalledTimes ( 1 ) ;
396+ // For checking if the delayed event is still there or got removed while sending the state.
397+ expect ( client . _unstable_updateDelayedEvent ) . toHaveBeenCalledTimes ( 1 ) ;
398+ // For scheduling the delayed event
399+ expect ( client . _unstable_sendDelayedStateEvent ) . toHaveBeenCalledTimes ( 1 ) ;
400+ // This returns no error so we do not check if we reschedule the event again. this is done in another test.
401+
402+ jest . useRealTimers ( ) ;
403+ } ) ;
404+
405+ it ( "uses membershipExpiryTimeout from join config" , async ( ) => {
406+ const realSetTimeout = setTimeout ;
407+ jest . useFakeTimers ( ) ;
408+ sess ! . joinRoomSession ( [ mockFocus ] , mockFocus , { membershipExpiryTimeout : 60000 } ) ;
409+ await Promise . race ( [ sentStateEvent , new Promise ( ( resolve ) => realSetTimeout ( resolve , 500 ) ) ] ) ;
410+ expect ( client . sendStateEvent ) . toHaveBeenCalledWith (
411+ mockRoom ! . roomId ,
412+ EventType . GroupCallMemberPrefix ,
413+ {
414+ application : "m.call" ,
415+ scope : "m.room" ,
416+ call_id : "" ,
417+ device_id : "AAAAAAA" ,
418+ expires : 60000 ,
419+ foci_preferred : [ mockFocus ] ,
420+ focus_active : {
421+ focus_selection : "oldest_membership" ,
422+ type : "livekit" ,
423+ } ,
424+ } ,
425+
426+ "_@alice:example.org_AAAAAAA" ,
427+ ) ;
428+ await Promise . race ( [ sentDelayedState , new Promise ( ( resolve ) => realSetTimeout ( resolve , 500 ) ) ] ) ;
429+ expect ( client . _unstable_sendDelayedStateEvent ) . toHaveBeenCalledTimes ( 1 ) ;
430+ jest . useRealTimers ( ) ;
431+ } ) ;
432+
322433 describe ( "calls" , ( ) => {
323434 const activeFocusConfig = { type : "livekit" , livekit_service_url : "https://active.url" } ;
324435 const activeFocus = { type : "livekit" , focus_selection : "oldest_membership" } ;
@@ -437,6 +548,78 @@ describe("MatrixRTCSession", () => {
437548 } ) ;
438549 } ) ;
439550
551+ it ( "renews membership event before expiry time" , async ( ) => {
552+ return "TODO add back the renew method since we also want this for non-legacy events." ;
553+ const activeFocus = { type : "livekit" , focus_selection : "oldest_membership" } ;
554+
555+ jest . useFakeTimers ( ) ;
556+ let resolveFn : ( ( _roomId : string , _type : string , val : Record < string , any > ) => void ) | undefined ;
557+
558+ const eventSentPromise = new Promise < Record < string , any > > ( ( r ) => {
559+ resolveFn = ( _roomId : string , _type : string , val : Record < string , any > ) => {
560+ r ( val ) ;
561+ } ;
562+ } ) ;
563+ try {
564+ const sendStateEventMock = jest . fn ( ) . mockImplementation ( resolveFn ) ;
565+ client . sendStateEvent = sendStateEventMock ;
566+
567+ sess ! . joinRoomSession ( [ mockFocus ] , mockFocus , { membershipExpiryTimeout : 60 * 60 * 1000 } ) ;
568+
569+ const eventContent = await eventSentPromise ;
570+
571+ jest . setSystemTime ( 1000 ) ;
572+ const event = mockRTCEvent ( eventContent . memberships , mockRoom . roomId ) ;
573+ const getState = mockRoom . getLiveTimeline ( ) . getState ( EventTimeline . FORWARDS ) ! ;
574+ getState . getStateEvents = jest . fn ( ) . mockReturnValue ( event ) ;
575+ getState . events = new Map ( [
576+ [
577+ event . getType ( ) ,
578+ {
579+ size : ( ) => true ,
580+ has : ( _stateKey : string ) => true ,
581+ get : ( _stateKey : string ) => event ,
582+ values : ( ) => [ event ] ,
583+ } as unknown as Map < string , MatrixEvent > ,
584+ ] ,
585+ ] ) ;
586+
587+ const eventReSentPromise = new Promise < Record < string , any > > ( ( r ) => {
588+ resolveFn = ( _roomId : string , _type : string , val : Record < string , any > ) => {
589+ r ( val ) ;
590+ } ;
591+ } ) ;
592+
593+ sendStateEventMock . mockReset ( ) . mockImplementation ( resolveFn ) ;
594+
595+ // definitely should have renewed by 1 second before the expiry!
596+ const timeElapsed = 60 * 60 * 1000 - 1000 ;
597+ jest . setSystemTime ( Date . now ( ) + timeElapsed ) ;
598+ jest . advanceTimersByTime ( timeElapsed ) ;
599+ await eventReSentPromise ;
600+
601+ expect ( sendStateEventMock ) . toHaveBeenCalledWith (
602+ mockRoom . roomId ,
603+ EventType . GroupCallMemberPrefix ,
604+ {
605+ application : "m.call" ,
606+ scope : "m.room" ,
607+ call_id : "" ,
608+ device_id : "AAAAAAA" ,
609+ expires : 3600000 * 2 ,
610+ foci_preferred : [ mockFocus ] ,
611+ focus_active : activeFocus ,
612+ created_ts : 1000 ,
613+ membershipID : expect . stringMatching ( ".*" ) ,
614+ } ,
615+ "_@alice:example.org_AAAAAAA" ,
616+ ) ;
617+ } finally {
618+ jest . useRealTimers ( ) ;
619+ }
620+ } ) ;
621+ } ) ;
622+
440623 describe ( "onMembershipsChanged" , ( ) => {
441624 it ( "does not emit if no membership changes" , ( ) => {
442625 const mockRoom = makeMockRoom ( membershipTemplate ) ;
@@ -768,6 +951,92 @@ describe("MatrixRTCSession", () => {
768951 }
769952 } ) ;
770953
954+ it ( "re-sends key if a member changes membership ID" , async ( ) => {
955+ return "membershipID is not a thing anymore" ;
956+ /*
957+ jest.useFakeTimers();
958+ try {
959+ const keysSentPromise1 = new Promise((resolve) => {
960+ sendEventMock.mockImplementation(resolve);
961+ });
962+
963+ const member1 = membershipTemplate;
964+ const member2 = {
965+ ...membershipTemplate,
966+ device_id: "BBBBBBB",
967+ };
968+
969+ const mockRoom = makeMockRoom([member1, member2]);
970+ mockRoom.getLiveTimeline().getState = jest
971+ .fn()
972+ .mockReturnValue(makeMockRoomState([member1, member2], mockRoom.roomId));
973+
974+ sess = MatrixRTCSession.roomSessionForRoom(client, mockRoom);
975+ sess.joinRoomSession([mockFocus], mockFocus, { manageMediaKeys: true });
976+
977+ await keysSentPromise1;
978+
979+ // make sure an encryption key was sent
980+ expect(sendEventMock).toHaveBeenCalledWith(
981+ expect.stringMatching(".*"),
982+ "io.element.call.encryption_keys",
983+ {
984+ call_id: "",
985+ device_id: "AAAAAAA",
986+ keys: [
987+ {
988+ index: 0,
989+ key: expect.stringMatching(".*"),
990+ },
991+ ],
992+ sent_ts: Date.now(),
993+ },
994+ );
995+ expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(1);
996+
997+ sendEventMock.mockClear();
998+
999+ // this should be a no-op:
1000+ sess.onMembershipUpdate();
1001+ expect(sendEventMock).toHaveBeenCalledTimes(0);
1002+
1003+ // advance time to avoid key throttling
1004+ jest.advanceTimersByTime(10000);
1005+
1006+ // update membership ID
1007+ member2.membershipID = "newID";
1008+
1009+ const keysSentPromise2 = new Promise((resolve) => {
1010+ sendEventMock.mockImplementation(resolve);
1011+ });
1012+
1013+ // this should re-send the key
1014+ sess.onMembershipUpdate();
1015+
1016+ await keysSentPromise2;
1017+
1018+ expect(sendEventMock).toHaveBeenCalledWith(
1019+ expect.stringMatching(".*"),
1020+ "io.element.call.encryption_keys",
1021+ {
1022+ call_id: "",
1023+ device_id: "AAAAAAA",
1024+ keys: [
1025+ {
1026+ index: 0,
1027+ key: expect.stringMatching(".*"),
1028+ },
1029+ ],
1030+ sent_ts: Date.now(),
1031+ },
1032+ );
1033+ expect(sess!.statistics.counters.roomEventEncryptionKeysSent).toEqual(2);
1034+ } finally {
1035+ jest.useRealTimers();
1036+ }
1037+ */
1038+ } ) ;
1039+
7711040 it ( "re-sends key if a member changes created_ts" , async ( ) => {
7721041 jest . useFakeTimers ( ) ;
7731042 jest . setSystemTime ( 1000 ) ;
0 commit comments