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..1e4d07198a --- /dev/null +++ b/pkgs/web_socket/lib/src/fake_web_socket.dart @@ -0,0 +1,123 @@ +// 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 '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 fakeTimeServer(WebSocket webSocket, String time) async { +/// await webSocket.events.forEach((event) { +/// switch (event) { +/// 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(); +/// }); +/// +/// test('test valid time', () async { +/// unawaited(fakeTimeServer(server, '2024-05-15T01:18:10.456Z')); +/// expect( +/// await getTime(client), +/// DateTime.parse('2024-05-15T01:18:10.456Z')); +/// }); +/// } +/// ``` +(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..669bf1dfb3 --- /dev/null +++ b/pkgs/web_socket/lib/testing.dart @@ -0,0 +1 @@ +export 'src/fake_web_socket.dart' show fakes; 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); +}