@@ -32,25 +32,38 @@ import { makeMockClient, makeMockRoom, membershipTemplate, mockCallMembership, t
32
32
import { MembershipManager } from "../../../src/matrixrtc/NewMembershipManager" ;
33
33
import { logger } from "../../../src/logger.ts" ;
34
34
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 ( ) ;
41
46
} ) ;
47
+ return promise ;
42
48
}
49
+
50
+ /** See waitForMockCall */
43
51
function 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 ( ) ;
49
56
} ) ;
57
+ return promise ;
50
58
}
51
59
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 > ( ) ;
54
67
method . mockImplementation ( ( ) => promise ) ;
55
68
return { reject, resolve } ;
56
69
}
@@ -110,13 +123,13 @@ describe.each([
110
123
it ( "sends a membership event and schedules delayed leave when joining a call" , async ( ) => {
111
124
// Spys/Mocks
112
125
113
- const updateDelayedEventHandle = createAsyncHandle ( client . _unstable_updateDelayedEvent as Mock ) ;
126
+ const updateDelayedEventHandle = createAsyncHandle < void > ( client . _unstable_updateDelayedEvent as Mock ) ;
114
127
115
128
// Test
116
129
const memberManager = new TestMembershipManager ( undefined , room , client , ( ) => undefined ) ;
117
130
memberManager . join ( [ focus ] , focusActive ) ;
118
131
// expects
119
- await waitForMockCall ( client . sendStateEvent ) ;
132
+ await waitForMockCall ( client . sendStateEvent , Promise . resolve ( { event_id : "id" } ) ) ;
120
133
expect ( client . sendStateEvent ) . toHaveBeenCalledWith (
121
134
room . roomId ,
122
135
"org.matrix.msc3401.call.member" ,
@@ -311,6 +324,44 @@ describe.each([
311
324
} ) ;
312
325
} ) ;
313
326
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
+
314
365
it ( "uses membershipEventExpiryMs from config" , async ( ) => {
315
366
const manager = new TestMembershipManager (
316
367
{ membershipEventExpiryMs : 1234567 } ,
@@ -542,8 +593,8 @@ describe.each([
542
593
expect ( manager . status ) . toBe ( Status . Disconnected ) ;
543
594
} ) ;
544
595
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 ) ;
547
598
548
599
const manager = new TestMembershipManager ( { } , room , client , ( ) => undefined ) ;
549
600
expect ( manager . status ) . toBe ( Status . Disconnected ) ;
@@ -594,7 +645,7 @@ describe.each([
594
645
} ) ;
595
646
// FailsForLegacy as implementation does not re-check membership before retrying.
596
647
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 (
598
649
new MatrixError (
599
650
{ errcode : "M_LIMIT_EXCEEDED" } ,
600
651
429 ,
0 commit comments