From 4d93cfa8ebc6c31b2601c194cf8c4b7688764f62 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 14 May 2024 18:08:30 -0700 Subject: [PATCH 1/3] Create a fake WebSocket implementation --- pkgs/web_socket/CHANGELOG.md | 5 + pkgs/web_socket/lib/src/fake_web_socket.dart | 120 ++++++++++++++++++ pkgs/web_socket/lib/testing.dart | 1 + pkgs/web_socket/pubspec.yaml | 2 +- .../web_socket/test/fake_web_socket_test.dart | 59 +++++++++ 5 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 pkgs/web_socket/lib/src/fake_web_socket.dart create mode 100644 pkgs/web_socket/lib/testing.dart create mode 100644 pkgs/web_socket/test/fake_web_socket_test.dart diff --git a/pkgs/web_socket/CHANGELOG.md b/pkgs/web_socket/CHANGELOG.md index 2f2a07b25d..9f6bbdbc56 100644 --- a/pkgs/web_socket/CHANGELOG.md +++ b/pkgs/web_socket/CHANGELOG.md @@ -1,3 +1,8 @@ +## 0.1.4 + +- Add a `fakes` function that returns a pair of `WebSocket`s useful in + testing. + ## 0.1.3 - Bring the behavior in line with the documentation by throwing diff --git a/pkgs/web_socket/lib/src/fake_web_socket.dart b/pkgs/web_socket/lib/src/fake_web_socket.dart new file mode 100644 index 0000000000..15c17ce589 --- /dev/null +++ b/pkgs/web_socket/lib/src/fake_web_socket.dart @@ -0,0 +1,120 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'dart:async'; +import 'dart:typed_data'; + +import '../web_socket.dart'; +import 'utils.dart'; +import 'web_socket.dart'; + +class FakeWebSocket implements WebSocket { + late FakeWebSocket _other; + + final String _protocol; + final _events = StreamController(); + + FakeWebSocket(this._protocol); + + @override + Future close([int? code, String? reason]) async { + if (_events.isClosed) { + throw WebSocketConnectionClosed(); + } + + checkCloseCode(code); + checkCloseReason(reason); + + unawaited(_events.close()); + if (!_other._events.isClosed) { + _other._events.add(CloseReceived(code ?? 1005, reason ?? '')); + unawaited(_other._events.close()); + } + } + + @override + Stream get events => _events.stream; + + @override + String get protocol => _protocol; + + @override + void sendBytes(Uint8List b) { + if (_events.isClosed) { + throw WebSocketConnectionClosed(); + } + if (_other._events.isClosed) return; + _other._events.add(BinaryDataReceived(b)); + } + + @override + void sendText(String s) { + if (_events.isClosed) { + throw WebSocketConnectionClosed(); + } + if (_other._events.isClosed) return; + _other._events.add(TextDataReceived(s)); + } +} + +/// Create a pair of fake [WebSocket]s that are connected to each other. +/// +/// Sending a message on one [WebSocket] will result in that same message being +/// received by the other. +/// +/// This can be useful in constructing tests. +/// +/// For example: +/// +/// ```dart +/// import 'package:test/test.dart'; +/// import 'package:web_socket/testing.dart'; +/// import 'package:web_socket/web_socket.dart'; +/// +/// Future sumServer(WebSocket webSocket) async { +/// var sum = 0; +/// +/// await webSocket.events.forEach((event) { +/// switch (event) { +/// case TextDataReceived(:final text): +/// sum += int.parse(text); +/// webSocket.sendText(sum.toString()); +/// case BinaryDataReceived(): +/// case CloseReceived(): +/// } +/// }); +/// } +/// +/// void main() async { +/// late WebSocket client; +/// late WebSocket server; +/// +/// setUp(() => (client, server) = fakes()); +/// tearDown(() => client.close()); +/// +/// test('test positive numbers', () { +/// sumServer(server); +/// client +/// ..sendText('1') +/// ..sendText('2') +/// ..sendText('3'); +/// expect( +/// client.events, +/// emitsInOrder([ +/// TextDataReceived('1'), +/// TextDataReceived('3'), +/// TextDataReceived('6') +/// ])); +/// }); +/// } +/// ``` +(WebSocket, WebSocket) fakes({String protocol = ''}) { + final peer1 = FakeWebSocket(protocol); + final peer2 = FakeWebSocket(protocol); + + peer1._other = peer2; + peer2._other = peer1; + + return (peer1, peer2); +} diff --git a/pkgs/web_socket/lib/testing.dart b/pkgs/web_socket/lib/testing.dart new file mode 100644 index 0000000000..6176a9717b --- /dev/null +++ b/pkgs/web_socket/lib/testing.dart @@ -0,0 +1 @@ +export 'src/fake_web_socket.dart'; diff --git a/pkgs/web_socket/pubspec.yaml b/pkgs/web_socket/pubspec.yaml index f9d95a75ff..1a04f8adff 100644 --- a/pkgs/web_socket/pubspec.yaml +++ b/pkgs/web_socket/pubspec.yaml @@ -3,7 +3,7 @@ description: >- Any easy-to-use library for communicating with WebSockets that has multiple implementations. repository: https://github.com/dart-lang/http/tree/master/pkgs/web_socket -version: 0.1.3 +version: 0.1.4 environment: sdk: ^3.3.0 diff --git a/pkgs/web_socket/test/fake_web_socket_test.dart b/pkgs/web_socket/test/fake_web_socket_test.dart new file mode 100644 index 0000000000..d8fd559433 --- /dev/null +++ b/pkgs/web_socket/test/fake_web_socket_test.dart @@ -0,0 +1,59 @@ +// Copyright (c) 2024, the Dart project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:web_socket/src/fake_web_socket.dart'; +import 'package:web_socket/web_socket.dart'; +import 'package:web_socket_conformance_tests/web_socket_conformance_tests.dart'; + +/// Forward data received from [from] to [to]. +void proxy(WebSocket from, WebSocket to) { + from.events.listen((event) { + try { + switch (event) { + case TextDataReceived(:final text): + to.sendText(text); + case BinaryDataReceived(:final data): + to.sendBytes(data); + case CloseReceived(:var code, :final reason): + if (code != null && (code < 3000 || code > 4999)) { + code = null; + } + to.close(code, reason); + } + } on WebSocketConnectionClosed { + // `to` may have been closed locally so ignore failures to forward the + // data. + } + }); +} + +/// Create a bidirectional proxy relationship between [a] and [b]. +/// +/// That means that events received by [a] will be forwarded to [b] and +/// vise-versa. +void bidirectionalProxy(WebSocket a, WebSocket b) { + proxy(a, b); + proxy(b, a); +} + +void main() { + // In order to use `testAll`, we need to provide a method that will connect + // to a real WebSocket server. + // + // The approach is to connect to the server with a real WebSocket and forward + // the data received by that data to one of the fakes. + // + // Like: + // + // 'hello' sendText('hello') TextDataReceived('hello') + // [Server] -> [realClient] -> [FakeServer] -> [fakeClient] + Future connect(Uri url, {Iterable? protocols}) async { + final realClient = await WebSocket.connect(url, protocols: protocols); + final (fakeServer, fakeClient) = fakes(protocol: realClient.protocol); + bidirectionalProxy(realClient, fakeServer); + return fakeClient; + } + + testAll(connect); +} From 241dd51dcbe5490f5bc2733519ec8f8dcc924f56 Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Tue, 14 May 2024 18:30:04 -0700 Subject: [PATCH 2/3] Better example --- pkgs/web_socket/lib/src/fake_web_socket.dart | 43 +++++++++++--------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/pkgs/web_socket/lib/src/fake_web_socket.dart b/pkgs/web_socket/lib/src/fake_web_socket.dart index 15c17ce589..1e4d07198a 100644 --- a/pkgs/web_socket/lib/src/fake_web_socket.dart +++ b/pkgs/web_socket/lib/src/fake_web_socket.dart @@ -68,44 +68,47 @@ class FakeWebSocket implements WebSocket { /// For example: /// /// ```dart +/// import 'dart:async'; +/// /// import 'package:test/test.dart'; +/// import 'package:web_socket/src/web_socket.dart'; /// import 'package:web_socket/testing.dart'; /// import 'package:web_socket/web_socket.dart'; /// -/// Future sumServer(WebSocket webSocket) async { -/// var sum = 0; -/// +/// Future fakeTimeServer(WebSocket webSocket, String time) async { /// await webSocket.events.forEach((event) { /// switch (event) { -/// case TextDataReceived(:final text): -/// sum += int.parse(text); -/// webSocket.sendText(sum.toString()); +/// case TextDataReceived(): /// case BinaryDataReceived(): +/// webSocket.sendText(time); /// case CloseReceived(): /// } /// }); /// } /// +/// Future getTime(WebSocket webSocket) async { +/// webSocket.sendText(''); +/// final time = switch (await webSocket.events.first) { +/// TextDataReceived(:final text) => DateTime.parse(text), +/// _ => throw Exception('unexpected response') +/// }; +/// await webSocket.close(); +/// return time; +/// } +/// /// void main() async { /// late WebSocket client; /// late WebSocket server; /// -/// setUp(() => (client, server) = fakes()); -/// tearDown(() => client.close()); +/// setUp(() { +/// (client, server) = fakes(); +/// }); /// -/// test('test positive numbers', () { -/// sumServer(server); -/// client -/// ..sendText('1') -/// ..sendText('2') -/// ..sendText('3'); +/// test('test valid time', () async { +/// unawaited(fakeTimeServer(server, '2024-05-15T01:18:10.456Z')); /// expect( -/// client.events, -/// emitsInOrder([ -/// TextDataReceived('1'), -/// TextDataReceived('3'), -/// TextDataReceived('6') -/// ])); +/// await getTime(client), +/// DateTime.parse('2024-05-15T01:18:10.456Z')); /// }); /// } /// ``` From 77914914bebabea7a5c8ae85d25df43d3be4059d Mon Sep 17 00:00:00 2001 From: Brian Quinlan Date: Fri, 17 May 2024 15:37:50 -0700 Subject: [PATCH 3/3] Update pkgs/web_socket/lib/testing.dart Co-authored-by: Nate Bosch --- pkgs/web_socket/lib/testing.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/web_socket/lib/testing.dart b/pkgs/web_socket/lib/testing.dart index 6176a9717b..669bf1dfb3 100644 --- a/pkgs/web_socket/lib/testing.dart +++ b/pkgs/web_socket/lib/testing.dart @@ -1 +1 @@ -export 'src/fake_web_socket.dart'; +export 'src/fake_web_socket.dart' show fakes;