Skip to content

Commit 3e978f9

Browse files
test: improve livekit backend key retry logic tests
1 parent bcc03e5 commit 3e978f9

File tree

1 file changed

+88
-104
lines changed

1 file changed

+88
-104
lines changed

test/livekit_backend_test.dart

Lines changed: 88 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)