@@ -12,7 +12,7 @@ void main() {
1212 late VoIP voip;
1313 late LiveKitBackend backend;
1414
15- group ('LiveKitBackend encryption key request retry tests ' , () {
15+ group ('LiveKitBackend encryption key retry' , () {
1616 Logs ().level = Level .info;
1717
1818 setUp (() async {
@@ -21,19 +21,16 @@ void main() {
2121
2222 voip = VoIP (matrix, MockWebRTCDelegate ());
2323 VoIP .customTxid = '1234' ;
24- final id = '!calls:example.com' ;
25- room = matrix.getRoomById (id)! ;
24+ room = matrix.getRoomById ('!calls:example.com' )! ;
2625
2726 backend = LiveKitBackend (
2827 livekitServiceUrl: 'https://livekit.example.com' ,
2928 livekitAlias: 'test_alias' ,
3029 );
3130
32- // Clear logs before each test to avoid interference
3331 Logs ().outputEvents.clear ();
3432 });
3533
36- /// Helper to create a group call session for testing
3734 Future <GroupCallSession > createGroupCall (String callId) async {
3835 final membership = CallMembership (
3936 userId: matrix.userID! ,
@@ -63,131 +60,118 @@ void main() {
6360 );
6461
6562 await voip.createGroupCallFromRoomStateEvent (membership);
66- final groupCall = voip.getGroupCallById (room.id, callId);
67- await groupCall! .enter ();
63+ final groupCall = voip.getGroupCallById (room.id, callId)! ;
64+ await groupCall.enter ();
6865 return groupCall;
6966 }
7067
71- /// Helper to count key request log messages for a specific participant
72- int countKeyRequestsFor (String participantId) {
73- return Logs ()
74- .outputEvents
75- .where (
76- (event) =>
77- event.title
78- .contains ('requesting stream encryption keys from' ) &&
79- event.title.contains (participantId),
80- )
81- .length;
82- }
68+ int countKeyRequests (String userId) => Logs ()
69+ .outputEvents
70+ .where (
71+ (e) =>
72+ e.title.contains ('requesting stream encryption keys' ) &&
73+ e.title.contains (userId),
74+ )
75+ .length;
8376
8477 test (
85- 'retry mechanism automatically re-requests keys when initial request fails' ,
86- () async {
87- // This test verifies: without retry, a failed key request leaves the call
88- // in an unrecoverable state. With retry, requests are automatically retried.
78+ 'retries keys periodically until received and receiving keys cancels retry for that participant only' ,
79+ () async {
80+ final groupCall = await createGroupCall ('test1' );
81+ final p1 = CallParticipant (voip, userId: '@alice:x.com' , deviceId: 'D1' );
82+ final p2 = CallParticipant (voip, userId: '@bob:x.com' , deviceId: 'D2' );
8983
90- final groupCall = await createGroupCall ('test_retry_mechanism' );
84+ Logs ().outputEvents.clear ();
85+ Logs ().level = Level .verbose;
9186
92- const remoteUserId = '@retry_test_user:example.com' ;
93- const remoteDeviceId = 'RETRY_TEST_DEVICE' ;
94- final remoteParticipant = CallParticipant (
95- voip,
96- userId: remoteUserId,
97- deviceId: remoteDeviceId,
98- );
87+ await backend.requestEncrytionKey (groupCall, [p1]);
88+ await backend.requestEncrytionKey (groupCall, [p2]);
9989
100- Logs ().outputEvents.clear ();
101- Logs ().level = Level .verbose;
90+ // Receive keys for p1 only
91+ await backend.onCallEncryption (groupCall, '@alice:x.com' , 'D1' , {
92+ 'keys' : [
93+ {
94+ 'key' : base64Encode ([1 , 2 , 3 , 4 ]),
95+ 'index' : 0 ,
96+ }
97+ ],
98+ 'call_id' : 'test1' ,
99+ });
102100
103- // Step 1: Initial key request (simulates framecryptor detecting missingKey)
104- await backend.requestEncrytionKey (groupCall, [remoteParticipant]);
105- expect (countKeyRequestsFor (remoteUserId), 1 );
101+ final countP1 = countKeyRequests ('@alice:x.com' );
102+ final countP2 = countKeyRequests ('@bob:x.com' );
106103
107- // Step 2: Wait for retry timer (2 second interval)
108- // WITHOUT retry: count stays at 1 (STUCK!)
109- // WITH retry: count increases (RECOVERY!)
110- await Future .delayed (Duration (milliseconds: 2100 ));
104+ await Future .delayed (Duration (milliseconds: 2100 ));
111105
112- expect (
113- countKeyRequestsFor (remoteUserId),
114- greaterThan (1 ),
115- reason: 'Retry mechanism should automatically re-request keys. '
116- 'Without retry, the call would be stuck in an unrecoverable state.' ,
117- );
106+ // p1 stopped, p2 continues
107+ expect (countKeyRequests ('@alice:x.com' ), countP1);
108+ expect (countKeyRequests ('@bob:x.com' ), greaterThan (countP2));
118109
119- await backend.dispose (groupCall);
120- },
121- );
110+ await backend.dispose (groupCall);
111+ });
122112
123- test (
124- 'each participant has independent retry - receiving keys for one does not affect another' ,
125- () async {
126- final groupCall = await createGroupCall ('test_independent_retries' );
113+ test ('can start fresh retry cycle after receiving keys' , () async {
114+ final groupCall = await createGroupCall ('test2' );
115+ final p = CallParticipant (voip, userId: '@bob:x.com' , deviceId: 'D1' );
127116
128- const user1 = '@independent_user1:example.com' ;
129- const device1 = 'DEVICE_1' ;
130- const user2 = '@independent_user2:example.com' ;
131- const device2 = 'DEVICE_2' ;
117+ Logs ().outputEvents.clear ();
118+ Logs ().level = Level .verbose;
132119
133- final participant1 =
134- CallParticipant (voip, userId: user1, deviceId: device1);
135- final participant2 =
136- CallParticipant (voip, userId: user2, deviceId: device2);
120+ // Request -> receive keys -> timer cancelled
121+ await backend.requestEncrytionKey (groupCall, [p]);
122+ await backend.onCallEncryption (groupCall, '@bob:x.com' , 'D1' , {
123+ 'keys' : [
124+ {
125+ 'key' : base64Encode ([1 , 2 , 3 , 4 ]),
126+ 'index' : 0 ,
127+ }
128+ ],
129+ 'call_id' : 'test2' ,
130+ });
131+
132+ final countAfterReceive = countKeyRequests ('@bob:x.com' );
133+
134+ // New request starts fresh cycle
135+ await backend.requestEncrytionKey (groupCall, [p]);
136+ expect (countKeyRequests ('@bob:x.com' ), countAfterReceive + 1 );
137+
138+ // New timer works
139+ await Future .delayed (Duration (milliseconds: 2100 ));
140+ expect (
141+ countKeyRequests ('@bob:x.com' ),
142+ greaterThan (countAfterReceive + 1 ),
143+ );
144+
145+ await backend.dispose (groupCall);
146+ });
147+
148+ test (
149+ 'stops after 5 retries' ,
150+ () async {
151+ final groupCall = await createGroupCall ('test3' );
152+ final p = CallParticipant (voip, userId: '@bob:x.com' , deviceId: 'D1' );
137153
138154 Logs ().outputEvents.clear ();
139155 Logs ().level = Level .verbose;
140156
141- // Request keys from both participants
142- await backend.requestEncrytionKey (groupCall, [participant1]);
143- await backend.requestEncrytionKey (groupCall, [participant2]);
157+ await backend.requestEncrytionKey (groupCall, [p]);
144158
145- expect ( countKeyRequestsFor (user1), 1 );
146- expect ( countKeyRequestsFor (user2), 1 );
159+ // Wait for 5 retries (5 * 2s = 10s)
160+ await Future . delayed ( Duration (milliseconds : 10500 ) );
147161
148- // Wait for retry
149- await Future .delayed (Duration (milliseconds: 2100 ));
150- expect (countKeyRequestsFor (user1), greaterThan (1 ));
151- expect (countKeyRequestsFor (user2), greaterThan (1 ));
152-
153- // Receive keys ONLY from participant 1
154- await backend.onCallEncryption (
155- groupCall,
156- user1,
157- device1,
158- {
159- 'keys' : [
160- {
161- 'key' : base64Encode ([1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ]),
162- 'index' : 0 ,
163- },
164- ],
165- 'call_id' : 'test_independent_retries' ,
166- },
167- );
162+ final hasMaxRetryLog = Logs ()
163+ .outputEvents
164+ .any ((e) => e.title.contains ('Max retries (5) reached' ));
165+ expect (hasMaxRetryLog, true );
168166
169- final countUser1AfterKeys = countKeyRequestsFor (user1);
170- final countUser2AfterKeys = countKeyRequestsFor (user2);
171-
172- // Wait another retry interval
167+ // No more retries after max
168+ final countAtMax = countKeyRequests ('@bob:x.com' );
173169 await Future .delayed (Duration (milliseconds: 2100 ));
174-
175- // User 1's retry should have stopped (received keys)
176- expect (
177- countKeyRequestsFor (user1),
178- countUser1AfterKeys,
179- reason: 'User 1 retry should stop after receiving keys.' ,
180- );
181-
182- // User 2's retry should continue (no keys received)
183- expect (
184- countKeyRequestsFor (user2),
185- greaterThan (countUser2AfterKeys),
186- reason: 'User 2 retry should continue since no keys were received.' ,
187- );
170+ expect (countKeyRequests ('@bob:x.com' ), countAtMax);
188171
189172 await backend.dispose (groupCall);
190173 },
174+ timeout: Timeout (Duration (seconds: 20 )),
191175 );
192176 });
193177}
0 commit comments