|
| 1 | +import 'dart:async'; |
1 | 2 | import 'dart:convert'; |
2 | 3 | import 'dart:io'; |
3 | 4 |
|
@@ -643,4 +644,131 @@ void main() { |
643 | 644 | verifyNever(() => mockedSink.add(any())); |
644 | 645 | }); |
645 | 646 | }); |
| 647 | + |
| 648 | + group('connect/disconnect race condition', () { |
| 649 | + test( |
| 650 | + 'connect does not crash if disconnect nullifies conn during await ready', |
| 651 | + () async { |
| 652 | + final readyCompleter = Completer<void>(); |
| 653 | + final mockedSocketChannel = MockIOWebSocketChannel(); |
| 654 | + final mockedSink = MockWebSocketSink(); |
| 655 | + |
| 656 | + when(() => mockedSocketChannel.ready) |
| 657 | + .thenAnswer((_) => readyCompleter.future); |
| 658 | + when(() => mockedSocketChannel.sink).thenReturn(mockedSink); |
| 659 | + when(() => mockedSink.close(any(), any())) |
| 660 | + .thenAnswer((_) => Future.value()); |
| 661 | + when(() => mockedSink.close()).thenAnswer((_) => Future.value()); |
| 662 | + |
| 663 | + final socket = RealtimeClient( |
| 664 | + socketEndpoint, |
| 665 | + transport: (url, headers) => mockedSocketChannel, |
| 666 | + ); |
| 667 | + |
| 668 | + // Start connect — it will suspend at await ready |
| 669 | + final connectFuture = socket.connect(); |
| 670 | + |
| 671 | + // Start disconnect (also suspends on ready since state is connecting) |
| 672 | + final disconnectFuture = socket.disconnect(); |
| 673 | + |
| 674 | + // Now complete the ready future — both connect and disconnect can proceed |
| 675 | + readyCompleter.complete(); |
| 676 | + await disconnectFuture; |
| 677 | + await connectFuture; |
| 678 | + |
| 679 | + // Should NOT have transitioned to open because disconnect nullified conn |
| 680 | + expect(socket.connState, isNot(SocketStates.open)); |
| 681 | + expect(socket.conn, isNull); |
| 682 | + }); |
| 683 | + |
| 684 | + test('connect bails out when connState changes during await ready', |
| 685 | + () async { |
| 686 | + final readyCompleter = Completer<void>(); |
| 687 | + final mockedSocketChannel = MockIOWebSocketChannel(); |
| 688 | + final mockedSink = MockWebSocketSink(); |
| 689 | + |
| 690 | + when(() => mockedSocketChannel.ready) |
| 691 | + .thenAnswer((_) => readyCompleter.future); |
| 692 | + when(() => mockedSocketChannel.sink).thenReturn(mockedSink); |
| 693 | + when(() => mockedSink.close(any(), any())) |
| 694 | + .thenAnswer((_) => Future.value()); |
| 695 | + when(() => mockedSink.close()).thenAnswer((_) => Future.value()); |
| 696 | + |
| 697 | + final socket = RealtimeClient( |
| 698 | + socketEndpoint, |
| 699 | + transport: (url, headers) => mockedSocketChannel, |
| 700 | + ); |
| 701 | + |
| 702 | + // Start connect |
| 703 | + final connectFuture = socket.connect(); |
| 704 | + |
| 705 | + // Start disconnect — also awaits ready |
| 706 | + final disconnectFuture = socket.disconnect(); |
| 707 | + |
| 708 | + // Complete ready — both proceed |
| 709 | + readyCompleter.complete(); |
| 710 | + await disconnectFuture; |
| 711 | + await connectFuture; |
| 712 | + |
| 713 | + expect(socket.connState, isNot(SocketStates.open)); |
| 714 | + }); |
| 715 | + |
| 716 | + test('rapid connect-disconnect-connect cycle does not crash', () async { |
| 717 | + final readyCompleter1 = Completer<void>(); |
| 718 | + final mockedSocketChannel1 = MockIOWebSocketChannel(); |
| 719 | + final mockedSink1 = MockWebSocketSink(); |
| 720 | + |
| 721 | + when(() => mockedSocketChannel1.ready) |
| 722 | + .thenAnswer((_) => readyCompleter1.future); |
| 723 | + when(() => mockedSocketChannel1.sink).thenReturn(mockedSink1); |
| 724 | + when(() => mockedSink1.close(any(), any())) |
| 725 | + .thenAnswer((_) => Future.value()); |
| 726 | + when(() => mockedSink1.close()).thenAnswer((_) => Future.value()); |
| 727 | + |
| 728 | + final readyCompleter2 = Completer<void>(); |
| 729 | + final mockedSocketChannel2 = MockIOWebSocketChannel(); |
| 730 | + final mockedSink2 = MockWebSocketSink(); |
| 731 | + final streamController2 = StreamController<dynamic>.broadcast(); |
| 732 | + |
| 733 | + when(() => mockedSocketChannel2.ready) |
| 734 | + .thenAnswer((_) => readyCompleter2.future); |
| 735 | + when(() => mockedSocketChannel2.sink).thenReturn(mockedSink2); |
| 736 | + when(() => mockedSocketChannel2.stream) |
| 737 | + .thenAnswer((_) => streamController2.stream); |
| 738 | + when(() => mockedSink2.close(any(), any())) |
| 739 | + .thenAnswer((_) => Future.value()); |
| 740 | + when(() => mockedSink2.close()).thenAnswer((_) => Future.value()); |
| 741 | + |
| 742 | + var callCount = 0; |
| 743 | + final socket = RealtimeClient( |
| 744 | + socketEndpoint, |
| 745 | + transport: (url, headers) { |
| 746 | + callCount++; |
| 747 | + if (callCount == 1) return mockedSocketChannel1; |
| 748 | + return mockedSocketChannel2; |
| 749 | + }, |
| 750 | + ); |
| 751 | + |
| 752 | + // First connect — suspends at await ready |
| 753 | + final connectFuture1 = socket.connect(); |
| 754 | + |
| 755 | + // Start disconnect (also suspends on ready) |
| 756 | + final disconnectFuture = socket.disconnect(); |
| 757 | + |
| 758 | + // Complete the first ready — both proceed, connect bails out |
| 759 | + readyCompleter1.complete(); |
| 760 | + await disconnectFuture; |
| 761 | + await connectFuture1; |
| 762 | + |
| 763 | + // Second connect with a fresh mock |
| 764 | + readyCompleter2.complete(); |
| 765 | + await socket.connect(); |
| 766 | + |
| 767 | + expect(socket.connState, SocketStates.open); |
| 768 | + expect(socket.conn, mockedSocketChannel2); |
| 769 | + |
| 770 | + await socket.disconnect(); |
| 771 | + await streamController2.close(); |
| 772 | + }); |
| 773 | + }); |
646 | 774 | } |
0 commit comments