Skip to content

Commit

Permalink
Create a fake WebSocket implementation (#1200)
Browse files Browse the repository at this point in the history
* Create a fake WebSocket implementation

* Better example

* Update pkgs/web_socket/lib/testing.dart

Co-authored-by: Nate Bosch <[email protected]>

---------

Co-authored-by: Nate Bosch <[email protected]>
  • Loading branch information
brianquinlan and natebosch authored May 17, 2024
1 parent 4d2f9f9 commit 5c01453
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 1 deletion.
5 changes: 5 additions & 0 deletions pkgs/web_socket/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
123 changes: 123 additions & 0 deletions pkgs/web_socket/lib/src/fake_web_socket.dart
Original file line number Diff line number Diff line change
@@ -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<WebSocketEvent>();

FakeWebSocket(this._protocol);

@override
Future<void> 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<WebSocketEvent> 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<void> fakeTimeServer(WebSocket webSocket, String time) async {
/// await webSocket.events.forEach((event) {
/// switch (event) {
/// case TextDataReceived():
/// case BinaryDataReceived():
/// webSocket.sendText(time);
/// case CloseReceived():
/// }
/// });
/// }
///
/// Future<DateTime> 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);
}
1 change: 1 addition & 0 deletions pkgs/web_socket/lib/testing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/fake_web_socket.dart' show fakes;
2 changes: 1 addition & 1 deletion pkgs/web_socket/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
59 changes: 59 additions & 0 deletions pkgs/web_socket/test/fake_web_socket_test.dart
Original file line number Diff line number Diff line change
@@ -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<WebSocket> connect(Uri url, {Iterable<String>? protocols}) async {
final realClient = await WebSocket.connect(url, protocols: protocols);
final (fakeServer, fakeClient) = fakes(protocol: realClient.protocol);
bidirectionalProxy(realClient, fakeServer);
return fakeClient;
}

testAll(connect);
}

0 comments on commit 5c01453

Please sign in to comment.