import 'dart:async'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_webrtc/flutter_webrtc.dart'; import 'package:ikar/core/calls/call_session.dart'; import 'package:ikar/core/calls/ws2_signaling.dart'; final config = Ws2Config( userId: 1, uri: Uri(scheme: 'wss', host: 'calls.invalid'), ); class BrokenSignaling extends Ws2Signaling { BrokenSignaling() : super(config); int sent = 0; @override Future transmitCandidate({ required int participantId, required String candidate, required String sdpMid, required int sdpMLineIndex, String participantType = 'USER', int deviceIdx = 0, }) async { sent++; throw const SocketException('mobile network disconnected'); } @override Future hangup({String reason = 'HUNGUP'}) async {} @override Future close() async {} } void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('hangup reaches IKAR even before WS2 connects', () async { final reasons = []; final session = CallSession( ws2Config: config, role: CallRole.caller, endOnServer: (reason) async { reasons.add(reason); }, ); await session.hangup(); await session.hangup(); expect(reasons, ['CANCELED']); expect(session.currentState, CallSessionState.ended); }); test( 'ICE send failure is handled and late candidates are ignored after hangup', () async { final signaling = BrokenSignaling(); final session = CallSession(ws2Config: config, role: CallRole.caller); session.bindCandidateSignalingForTesting(signaling, 2); final errors = []; runZonedGuarded(() { session.receiveLocalCandidateForTesting( RTCIceCandidate('candidate:mobile', '0', 0), ); }, (error, stack) => errors.add(error)); await Future.delayed(Duration.zero); final reconnecting = session.isReconnecting; await session.hangup(); runZonedGuarded(() { session.receiveLocalCandidateForTesting( RTCIceCandidate('candidate:late', '0', 0), ); }, (error, stack) => errors.add(error)); await Future.delayed(Duration.zero); expect(reconnecting, isTrue); expect(signaling.sent, 1); expect(errors, isEmpty); expect(session.currentState, CallSessionState.ended); await Future.delayed(const Duration(milliseconds: 1100)); }, ); }