Files
IKAR/test/incoming_call_lifecycle_test.dart
T
sevenhill 0508b70732
Build Android (FCM) / build-android-fcm (push) Canceled after 0s
Build Android / build-android (push) Canceled after 0s
Build iOS / build-ios (push) Canceled after 0s
Build Linux / build-linux (push) Canceled after 0s
Build macOS / build-macos (push) Canceled after 0s
Build Windows / build-windows (push) Canceled after 0s
Native crypto / native-crypto (push) Canceled after 0s
Release (main) / android (oneme) (push) Canceled after 0s
Release (main) / android (qlyra) (push) Canceled after 0s
Release (main) / windows (push) Canceled after 0s
Release (main) / linux (push) Canceled after 0s
Release (main) / macos (push) Canceled after 0s
Release (main) / ios (push) Canceled after 0s
Tests / test (push) Canceled after 0s
Release (main) / release (push) Canceled after 0s
Release IKAR 1.0.8: fix incoming calls, cancellation and call history
2026-10-08 21:34:15 +03:00

99 lines
3.0 KiB
Dart

import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:ikar/backend/api.dart';
import 'package:ikar/core/calls/call_controller.dart';
import 'package:ikar/core/protocol/packet.dart';
import 'package:kusoft/src/rust/api.dart' show CallParams;
import 'package:kusoft/src/rust/frb_generated.dart' show RustLib, RustLibApi;
class _PushApi extends Api {
final packets = StreamController<Packet>.broadcast();
@override
Stream<Packet> get pushStream => packets.stream;
}
class _CallDecoder implements RustLibApi {
@override
CallParams? crateApiDecodeVcp({
required String vcp,
required String conversationId,
}) => const CallParams(
token: 'synthetic',
wsEndpoint: 'wss://calls.invalid/ws2',
turn: [],
isVideo: false,
);
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
setUpAll(() => RustLib.initMock(api: _CallDecoder()));
late _PushApi api;
late CallController controller;
setUp(() {
api = _PushApi();
controller = CallController.forTesting()..appResumed = true;
controller.init(api);
});
tearDown(() async {
controller.dispose();
await api.packets.close();
await api.dispose();
});
Future<void> push(Map<String, dynamic> data) async {
api.packets.add(Packet(opcode: 137, payload: data));
await Future<void>.delayed(Duration.zero);
}
Map<String, dynamic> invite(String id) => {
'conversationId': id,
'callerId': 2,
'vcp': 'synthetic-incoming',
'type': 'AUDIO',
};
test(
'caller cancellation dismisses a ringing recipient without WS2',
() async {
var canceled = 0;
final sub = controller.incomingCanceled.listen((_) => canceled++);
await push(invite('first'));
expect(controller.pendingIncoming?.conversationId, 'first');
await push({'conversationId': 'first', 'event': 'ended'});
expect(controller.pendingIncoming, isNull);
expect(canceled, 1);
await sub.cancel();
},
);
test('late invite after cancellation does not start ringing', () async {
await push({'conversationId': 'first', 'event': 'ended'});
await push(invite('first'));
controller.injectFromNative(invite('first'), autoAccept: true);
expect(controller.pendingIncoming, isNull);
});
test('old cancellation cannot dismiss a newer incoming call', () async {
await push(invite('new'));
await push({'conversationId': 'old', 'event': 'ended'});
expect(controller.pendingIncoming?.conversationId, 'new');
});
test(
'expired invite is ignored and committed history refreshes listeners',
() async {
await push({
...invite('expired'),
'expiresAt': DateTime.now().millisecondsSinceEpoch - 1000,
});
expect(controller.pendingIncoming, isNull);
var refreshed = 0;
final sub = controller.callEnded.listen((_) => refreshed++);
await push({'conversationId': 'expired', 'event': 'history'});
expect(refreshed, 1);
await sub.cancel();
},
);
}