450 lines
16 KiB
Dart
450 lines
16 KiB
Dart
import 'package:test/test.dart';
|
|
import 'package:webrtc_interface/webrtc_interface.dart';
|
|
|
|
import 'package:matrix/matrix.dart';
|
|
import 'fake_client.dart';
|
|
import 'webrtc_stub.dart';
|
|
|
|
void main() {
|
|
late Client matrix;
|
|
late Room room;
|
|
late VoIP voip;
|
|
group('Call tests', () {
|
|
Logs().level = Level.info;
|
|
setUp(() async {
|
|
matrix = await getClient();
|
|
voip = VoIP(matrix, MockWebRTCDelegate());
|
|
VoIP.customTxid = '1234';
|
|
final id = '!calls:example.com';
|
|
|
|
room = matrix.getRoomById(id)!;
|
|
});
|
|
|
|
test('Test call methods', () async {
|
|
final call = CallSession(CallOptions()..room = room);
|
|
await call.sendInviteToCall(room, '1234', 1234, '4567', '7890', 'sdp',
|
|
txid: '1234');
|
|
await call.sendAnswerCall(room, '1234', 'sdp', '4567', txid: '1234');
|
|
await call.sendCallCandidates(room, '1234', '4567', [], txid: '1234');
|
|
await call.sendSelectCallAnswer(room, '1234', '4567', '6789',
|
|
txid: '1234');
|
|
await call.sendCallReject(room, '1234', '4567', 'busy', txid: '1234');
|
|
await call.sendCallNegotiate(room, '1234', 1234, '4567', 'sdp',
|
|
txid: '1234');
|
|
await call.sendHangupCall(room, '1234', '4567', 'user_hangup',
|
|
txid: '1234');
|
|
await call.sendAssertedIdentity(
|
|
room,
|
|
'1234',
|
|
'4567',
|
|
AssertedIdentity()
|
|
..displayName = 'name'
|
|
..id = 'some_id',
|
|
txid: '1234');
|
|
await call.sendCallReplaces(room, '1234', '4567', CallReplaces(),
|
|
txid: '1234');
|
|
await call.sendSDPStreamMetadataChanged(
|
|
room, '1234', '4567', SDPStreamMetadata({}),
|
|
txid: '1234');
|
|
});
|
|
|
|
test('Call lifetime and age', () async {
|
|
expect(voip.currentCID, null);
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': '1702472924955oq1uQbNAfU7wAaEA',
|
|
'party_id': 'DPCIPPBGPO',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'newevent',
|
|
originServerTs: DateTime.utc(1969),
|
|
)
|
|
]))
|
|
})));
|
|
await Future.delayed(Duration(seconds: 2));
|
|
// confirm that no call got created after 3 seconds, which is
|
|
// expected in this case because the originTs was old asf
|
|
expect(voip.currentCID, null);
|
|
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
unsigned: {'age': 60001},
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'unsignedTsInvalidCall',
|
|
'party_id': 'DPCIPPBGPO',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'newevent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await Future.delayed(Duration(seconds: 2));
|
|
// confirm that no call got created after 3 seconds, which is
|
|
// expected in this case because age was older than lifetime
|
|
expect(voip.currentCID, null);
|
|
});
|
|
test('Call connection and hanging up', () async {
|
|
expect(voip.currentCID, null);
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'originTsValidCall',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerInviteEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.candidates',
|
|
content: {
|
|
'call_id': 'originTsValidCall',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'candidates': [
|
|
{
|
|
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
},
|
|
{
|
|
'candidate':
|
|
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
}
|
|
],
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerCallCandidatesEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
while (voip.currentCID != 'originTsValidCall') {
|
|
// call invite looks valid, call should be created now :D
|
|
await Future.delayed(Duration(milliseconds: 50));
|
|
Logs().d('Waiting for currentCID to update');
|
|
}
|
|
expect(voip.currentCID, 'originTsValidCall');
|
|
final call = voip.calls[voip.currentCID]!;
|
|
expect(call.state, CallState.kRinging);
|
|
await call.answer(txid: '1234');
|
|
|
|
call.pc!.onIceGatheringState!
|
|
.call(RTCIceGatheringState.RTCIceGatheringStateComplete);
|
|
// we send them manually anyway because our stub sends empty list of
|
|
// candidates
|
|
await call.sendCallCandidates(
|
|
room,
|
|
'originTsValidCall',
|
|
'GHTYAJCE',
|
|
[
|
|
{
|
|
'candidate': 'candidate:0 1 UDP 2122252543 uwu 50184 typ host',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
},
|
|
{
|
|
'candidate':
|
|
'candidate:3 1 TCP 2105524479 uwu 9 typ host tcptype active',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
}
|
|
],
|
|
txid: '1234');
|
|
|
|
expect(call.state, CallState.kConnecting);
|
|
|
|
// caller sends select answer
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.select_answer',
|
|
content: {
|
|
'call_id': 'originTsValidCall',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'lifetime': 10000,
|
|
'selected_party_id': 'GHTYAJCE'
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerSelectAnswerEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
|
|
call.pc!.onIceConnectionState!
|
|
.call(RTCIceConnectionState.RTCIceConnectionStateChecking);
|
|
call.pc!.onIceConnectionState!
|
|
.call(RTCIceConnectionState.RTCIceConnectionStateConnected);
|
|
// just to make sure there are no errors after running functions
|
|
// that are supposed to run once iceConnectionState is connected
|
|
await Future.delayed(Duration(seconds: 2));
|
|
|
|
expect(call.state, CallState.kConnected);
|
|
|
|
await call.hangup();
|
|
expect(call.state, CallState.kEnded);
|
|
expect(voip.currentCID, null);
|
|
});
|
|
|
|
test('Call answered elsewhere', () async {
|
|
expect(voip.currentCID, null);
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'answer_elseWhere',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerInviteEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.candidates',
|
|
content: {
|
|
'call_id': 'answer_elseWhere',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'candidates': [
|
|
{
|
|
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
},
|
|
{
|
|
'candidate':
|
|
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
}
|
|
],
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerCallCandidatesEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
while (voip.currentCID != 'answer_elseWhere') {
|
|
// call invite looks valid, call should be created now :D
|
|
await Future.delayed(Duration(milliseconds: 50));
|
|
Logs().d('Waiting for currentCID to update');
|
|
}
|
|
expect(voip.currentCID, 'answer_elseWhere');
|
|
final call = voip.calls[voip.currentCID]!;
|
|
expect(call.state, CallState.kRinging);
|
|
|
|
// caller sends select answer
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.select_answer',
|
|
content: {
|
|
'call_id': 'answer_elseWhere',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'lifetime': 10000,
|
|
'selected_party_id':
|
|
'not_us' // selected some other device for answer
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerSelectAnswerEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
// wait for select answer to end the call
|
|
await Future.delayed(Duration(seconds: 2));
|
|
// call ended because answered elsewhere
|
|
expect(call.state, CallState.kEnded);
|
|
expect(voip.currentCID, null);
|
|
});
|
|
|
|
test('Reject incoming call', () async {
|
|
expect(voip.currentCID, null);
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'reject_call',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerInviteEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.candidates',
|
|
content: {
|
|
'call_id': 'reject_call',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'candidates': [
|
|
{
|
|
'candidate': 'candidate:01UDP2122252543uwu50184typhost',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
},
|
|
{
|
|
'candidate':
|
|
'candidate:31TCP2105524479uwu9typhosttcptypeactive',
|
|
'sdpMid': '0',
|
|
'sdpMLineIndex': 0
|
|
}
|
|
],
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerCallCandidatesEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
while (voip.currentCID != 'reject_call') {
|
|
// call invite looks valid, call should be created now :D
|
|
await Future.delayed(Duration(milliseconds: 50));
|
|
Logs().d('Waiting for currentCID to update');
|
|
}
|
|
expect(voip.currentCID, 'reject_call');
|
|
final call = voip.calls[voip.currentCID]!;
|
|
expect(call.state, CallState.kRinging);
|
|
|
|
await call.reject();
|
|
|
|
// call ended because answered elsewhere
|
|
expect(call.state, CallState.kEnded);
|
|
expect(voip.currentCID, null);
|
|
});
|
|
|
|
test('Glare after invite was sent', () async {
|
|
expect(voip.currentCID, null);
|
|
final firstCall = await voip.inviteToCall(room.id, CallType.kVoice);
|
|
await firstCall.pc!.onRenegotiationNeeded!.call();
|
|
expect(firstCall.state, CallState.kInviteSent);
|
|
// KABOOM YOU JUST GLARED
|
|
await Future.delayed(Duration(seconds: 3));
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'zzzz_glare_2nd_call',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerInviteEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await Future.delayed(Duration(seconds: 3));
|
|
expect(voip.currentCID, firstCall.callId);
|
|
await firstCall.hangup();
|
|
});
|
|
test('Glare before invite was sent', () async {
|
|
expect(voip.currentCID, null);
|
|
final firstCall = await voip.inviteToCall(room.id, CallType.kVoice);
|
|
expect(firstCall.state, CallState.kCreateOffer);
|
|
// KABOOM YOU JUST GLARED, but this tiem you were still preparing your call
|
|
// so just cancel that instead
|
|
await Future.delayed(Duration(seconds: 3));
|
|
await matrix.handleSync(SyncUpdate(
|
|
nextBatch: 'something',
|
|
rooms: RoomsUpdate(join: {
|
|
room.id: JoinedRoomUpdate(
|
|
timeline: TimelineUpdate(events: [
|
|
MatrixEvent(
|
|
type: 'm.call.invite',
|
|
content: {
|
|
'lifetime': 60000,
|
|
'call_id': 'zzzz_glare_2nd_call',
|
|
'party_id': 'GHTYAJCE_caller',
|
|
'version': '1',
|
|
'offer': {'type': 'offer', 'sdp': 'sdp'}
|
|
},
|
|
senderId: '@alice:testing.com',
|
|
eventId: 'callerInviteEvent',
|
|
originServerTs: DateTime.now(),
|
|
)
|
|
]))
|
|
})));
|
|
await Future.delayed(Duration(seconds: 3));
|
|
expect(voip.currentCID, 'zzzz_glare_2nd_call');
|
|
});
|
|
});
|
|
}
|