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

Provide a fake WebSocketChannel implementation useful for tests #361

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
## 3.0.1-wip
## 3.1.0-wip

- Remove unnecessary `dependency_overrides`.
- Add a `fakes` function that returns a connected pair of `WebSocketChannels`
useful for testing. In the context of tests, this can be used in place
of the `WebSocketChannel` constructor, which was removed in version 3.0.0.

## 3.0.0

Expand Down
16 changes: 0 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,19 +64,3 @@ other socket exactly why they're closing the connection.
[sink]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/sink.html
[WebSocketSink]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketSink-class.html
[sink.close]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketSink/close.html

`WebSocketChannel` also works as a cross-platform implementation of the
WebSocket protocol. The [`WebSocketChannel.connect` constructor][connect]
connects to a listening server using the appropriate implementation for the
platform. The [`WebSocketChannel()` constructor][new] takes an underlying
[`StreamChannel`][stream_channel] over which it communicates using the WebSocket
protocol. It also provides the static [`signKey()`][signKey] method to make it
easier to implement the [initial WebSocket handshake][]. These are used in the
[`shelf_web_socket`][shelf_web_socket] package to support WebSockets in a
cross-platform way.

[connect]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/WebSocketChannel.connect.html
[new]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/WebSocketChannel.html
[signKey]: https://pub.dev/documentation/web_socket_channel/latest/web_socket_channel/WebSocketChannel/signKey.html
[initial WebSocket handshake]: https://tools.ietf.org/html/rfc6455#section-4.2.2
[shelf_web_socket]: https://pub.dev/packages/shelf_web_socket
111 changes: 111 additions & 0 deletions lib/src/fake_web_socket_channel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
// 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 'package:async/async.dart';
import 'package:stream_channel/stream_channel.dart';

import '../web_socket_channel.dart';

const _noStatusCodePresent = 1005;

class _FakeSink extends DelegatingStreamSink implements WebSocketSink {
final _FakeWebSocketChannel _channel;

_FakeSink(this._channel) : super(_channel._controller.sink);

@override
Future close([int? closeCode, String? closeReason]) async {
if (!_channel._isClosed) {
_channel._isClosed = true;
unawaited(super.close());
_channel._closeCode = closeCode;
_channel._closeReason = closeReason;
unawaited(_channel._close(closeCode, closeReason));
}
}
}

class _FakeWebSocketChannel extends StreamChannelMixin
implements WebSocketChannel {
final StreamChannel _controller;
final Future Function(int? closeCode, String? closeReason) _close;
int? _closeCode;
String? _closeReason;
var _isClosed = false;

_FakeWebSocketChannel(this._controller, this._close);

@override
int? get closeCode => _closeCode;

@override
String? get closeReason => _closeReason;

@override
String? get protocol => throw UnimplementedError();

@override
Future<void> get ready => Future.value();

@override
WebSocketSink get sink => _FakeSink(this);

@override
Stream get stream => _controller.stream;
}

/// Create a pair of fake [WebSocketChannel]s that are connected to each other.
///
/// For example:
///
/// ```
/// import 'package:test/test.dart';
/// import 'package:web_socket_channel/testing.dart';
/// import 'package:web_socket_channel/web_socket_channel.dart';
///
/// Future<void> sumServer(WebSocketChannel channel) async {
/// var sum = 0;
/// await channel.stream.forEach((number) {
/// sum += int.parse(number as String);
/// channel.sink.add(sum.toString());
/// });
/// }
///
/// void main() async {
/// late WebSocketChannel client;
/// late WebSocketChannel server;
///
/// setUp(() => (client, server) = fakes());
/// tearDown(() => client.sink.close());
/// tearDown(() => server.sink.close());
///
/// test('test positive numbers', () {
/// sumServer(server);
/// client.sink.add('1');
/// client.sink.add('2');
/// client.sink.add('3');
/// expect(client.stream, emitsInOrder(['1', '3', '6']));
/// });
/// }
/// ```
(WebSocketChannel, WebSocketChannel) fakes() {
final peer1Write = StreamController<dynamic>();
final peer2Write = StreamController<dynamic>();

late _FakeWebSocketChannel peer1;
late _FakeWebSocketChannel peer2;

peer1 = _FakeWebSocketChannel(
StreamChannel(peer2Write.stream, peer1Write.sink),
(closeCode, closeReason) =>
peer2.sink.close(closeCode ?? _noStatusCodePresent, closeReason));
peer2 = _FakeWebSocketChannel(
StreamChannel(peer1Write.stream, peer2Write.sink),
(closeCode, closeReason) =>
peer1.sink.close(closeCode ?? _noStatusCodePresent, closeReason));

return (peer1, peer2);
}
5 changes: 5 additions & 0 deletions lib/testing.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// 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.

export 'src/fake_web_socket_channel.dart';
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: web_socket_channel
version: 3.0.1-wip
version: 3.1.0-wip
description: >-
StreamChannel wrappers for WebSockets. Provides a cross-platform
WebSocketChannel API, a cross-platform implementation of that API that
Expand Down
66 changes: 66 additions & 0 deletions test/fake_web_socket_channel_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 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:test/test.dart';
import 'package:web_socket_channel/testing.dart';
import 'package:web_socket_channel/web_socket_channel.dart';

void main() {
group('fakes', () {
late WebSocketChannel client;
late WebSocketChannel server;

setUp(() => (client, server) = fakes());
tearDown(() => client.sink.close());
tearDown(() => server.sink.close());

test('string send and receive', () async {
server.sink.add('Hello');
server.sink.add('How are you?');
expect(client.stream, emitsInOrder(['Hello', 'How are you?']));
client.sink.add('Great!');
client.sink.add('And you?');
expect(server.stream, emitsInOrder(['Great!', 'And you?']));
});

test('list<int> send and receive', () async {
server.sink.add([1, 2, 3]);
server.sink.add([4, 5, 6]);
expect(
client.stream,
emitsInOrder([
[1, 2, 3],
[4, 5, 6]
]));
client.sink.add([7, 8]);
client.sink.add([10, 11]);
expect(
server.stream,
emitsInOrder([
[7, 8],
[10, 11]
]));
});

test('close', () async {
await server.sink.close();

expect(server.closeCode, isNull);
expect(server.closeReason, isNull);

expect(client.closeCode, 1005); // 1005: closed without a code.
expect(client.closeReason, isNull);
});

test('close with code and reason', () async {
await server.sink.close(3001, 'bye!');

expect(server.closeCode, 3001);
expect(server.closeReason, 'bye!');

expect(client.closeCode, 3001);
expect(client.closeReason, 'bye!');
});
});
}
Loading