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
99 lines
3.0 KiB
Dart
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();
|
|
},
|
|
);
|
|
}
|