@@ -32,25 +32,38 @@ import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, t
3232import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager" ;
3333import { logger } from "../../../src/logger.ts" ;
3434
35- function waitForMockCall ( method : MockedFunction < any > , returnVal ?: Promise < any > ) {
36- return new Promise < void > ( ( resolve ) => {
37- method . mockImplementation ( ( ) => {
38- resolve ( ) ;
39- return returnVal ?? Promise . resolve ( ) ;
40- } ) ;
35+ /**
36+ * Create a promise that will resolve once a mocked method is called.
37+ * @param method The method to wait for.
38+ * @param returnVal Provide an optional value that the mocked method should return. (use Promise.resolve(val) or Promise.reject(err))
39+ * @returns The promise that resolves once the method is called.
40+ */
41+ function waitForMockCall ( method : MockedFunction < any > , returnVal ?: Promise < any > ) : Promise < void > {
42+ const { promise, resolve } = Promise . withResolvers < void > ( ) ;
43+ method . mockImplementation ( ( ) => {
44+ resolve ( ) ;
45+ return returnVal ?? Promise . resolve ( ) ;
4146 } ) ;
47+ return promise ;
4248}
49+
50+ /** See waitForMockCall */
4351function waitForMockCallOnce ( method : MockedFunction < any > , returnVal ?: Promise < any > ) {
44- return new Promise < void > ( ( resolve ) => {
45- method . mockImplementationOnce ( ( ) => {
46- resolve ( ) ;
47- return returnVal ?? Promise . resolve ( ) ;
48- } ) ;
52+ const { promise, resolve } = Promise . withResolvers < void > ( ) ;
53+ method . mockImplementationOnce ( ( ) => {
54+ resolve ( ) ;
55+ return returnVal ?? Promise . resolve ( ) ;
4956 } ) ;
57+ return promise ;
5058}
5159
52- function createAsyncHandle ( method : MockedFunction < any > ) {
53- const { reject, resolve, promise } = Promise . withResolvers < void > ( ) ;
60+ /**
61+ * A handle to control when in the test flow the provided method resolves (or gets rejected).
62+ * @param method The method to control the resolve timing.
63+ * @returns
64+ */
65+ function createAsyncHandle < T > ( method : MockedFunction < any > ) {
66+ const { reject, resolve, promise } = Promise . withResolvers < T > ( ) ;
5467 method . mockImplementation ( ( ) => promise ) ;
5568 return { reject, resolve } ;
5669}
@@ -110,13 +123,13 @@ describe.each([
110123 it ( "sends a membership event and schedules delayed leave when joining a call" , async ( ) => {
111124 // Spys/Mocks
112125
113- const updateDelayedEventHandle = createAsyncHandle ( client . _unstable_updateDelayedEvent as Mock ) ;
126+ const updateDelayedEventHandle = createAsyncHandle < void > ( client . _unstable_updateDelayedEvent as Mock ) ;
114127
115128 // Test
116129 const memberManager = new TestMembershipManager ( undefined , room , client , ( ) => undefined ) ;
117130 memberManager . join ( [ focus ] , focusActive ) ;
118131 // expects
119- await waitForMockCall ( client . sendStateEvent ) ;
132+ await waitForMockCall ( client . sendStateEvent , Promise . resolve ( { event_id : "id" } ) ) ;
120133 expect ( client . sendStateEvent ) . toHaveBeenCalledWith (
121134 room . roomId ,
122135 "org.matrix.msc3401.call.member" ,
@@ -311,6 +324,44 @@ describe.each([
311324 } ) ;
312325 } ) ;
313326
327+ it ( "rejoins if delayed event is not found (404) !FailsForLegacy" , async ( ) => {
328+ const RESTART_DELAY = 15000 ;
329+ const manager = new TestMembershipManager (
330+ { delayedLeaveEventRestartMs : RESTART_DELAY } ,
331+ room ,
332+ client ,
333+ ( ) => undefined ,
334+ ) ;
335+ // Join with the membership manager
336+ manager . join ( [ focus ] , focusActive ) ;
337+ expect ( manager . status ) . toBe ( Status . Connecting ) ;
338+ // Let the scheduler run one iteration so that we can send the join state event
339+ await jest . runOnlyPendingTimersAsync ( ) ;
340+ expect ( client . sendStateEvent ) . toHaveBeenCalledTimes ( 1 ) ;
341+ expect ( manager . status ) . toBe ( Status . Connected ) ;
342+ // Now that we are connected, we set up the mocks.
343+ // We enforce the following scenario where we simulate that the delayed event activated and caused the user to leave:
344+ // - We wait until the delayed event gets sent and then mock its response to be "not found."
345+ // - We enforce a race condition between the sync that informs us that our call membership state event was set to "left"
346+ // and the "not found" response from the delayed event: we receive the sync while we are waiting for the delayed event to be sent.
347+ // - While the delayed leave event is being sent, we inform the manager that our membership state event was set to "left."
348+ // (onRTCSessionMemberUpdate)
349+ // - Only then do we resolve the sending of the delayed event.
350+ // - We test that the manager acknowledges the leave and sends a new membership state event.
351+ ( client . _unstable_updateDelayedEvent as Mock < any > ) . mockRejectedValueOnce (
352+ new MatrixError ( { errcode : "M_NOT_FOUND" } ) ,
353+ ) ;
354+
355+ const { resolve } = createAsyncHandle ( client . _unstable_sendDelayedStateEvent ) ;
356+ await jest . advanceTimersByTimeAsync ( RESTART_DELAY ) ;
357+ // first simulate the sync, then resolve sending the delayed event.
358+ await manager . onRTCSessionMemberUpdate ( [ mockCallMembership ( membershipTemplate , room . roomId ) ] ) ;
359+ resolve ( { delay_id : "id" } ) ;
360+ // Let the scheduler run one iteration so that the new join gets sent
361+ await jest . runOnlyPendingTimersAsync ( ) ;
362+ expect ( client . sendStateEvent ) . toHaveBeenCalledTimes ( 2 ) ;
363+ } ) ;
364+
314365 it ( "uses membershipEventExpiryMs from config" , async ( ) => {
315366 const manager = new TestMembershipManager (
316367 { membershipEventExpiryMs : 1234567 } ,
@@ -542,8 +593,8 @@ describe.each([
542593 expect ( manager . status ) . toBe ( Status . Disconnected ) ;
543594 } ) ;
544595 it ( "emits 'Connection' and 'Connected' after join !FailsForLegacy" , async ( ) => {
545- const handleDelayedEvent = createAsyncHandle ( client . _unstable_sendDelayedStateEvent ) ;
546- const handleStateEvent = createAsyncHandle ( client . sendStateEvent ) ;
596+ const handleDelayedEvent = createAsyncHandle < void > ( client . _unstable_sendDelayedStateEvent ) ;
597+ const handleStateEvent = createAsyncHandle < void > ( client . sendStateEvent ) ;
547598
548599 const manager = new TestMembershipManager ( { } , room , client , ( ) => undefined ) ;
549600 expect ( manager . status ) . toBe ( Status . Disconnected ) ;
@@ -594,7 +645,7 @@ describe.each([
594645 } ) ;
595646 // FailsForLegacy as implementation does not re-check membership before retrying.
596647 it ( "abandons retry loop and sends new own membership if not present anymore !FailsForLegacy" , async ( ) => {
597- ( client . _unstable_sendDelayedStateEvent as any ) . mockRejectedValue (
648+ ( client . _unstable_sendDelayedStateEvent as Mock < any > ) . mockRejectedValue (
598649 new MatrixError (
599650 { errcode : "M_LIMIT_EXCEEDED" } ,
600651 429 ,
0 commit comments