Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a fake WebSocket implementation #1200

Merged
merged 3 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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';
brianquinlan marked this conversation as resolved.
Show resolved Hide resolved
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);
}