From 1626d7432cbd3d11d16311b866294dba842b4132 Mon Sep 17 00:00:00 2001 From: Sahil Kumar Date: Thu, 3 Jun 2021 18:06:56 +0530 Subject: [PATCH] add websocket_test.dart --- packages/stream_chat/analysis_options.yaml | 2 +- .../lib/src/core/api/responses.dart | 5 +- .../lib/src/core/api/responses.g.dart | 9 + packages/stream_chat/pubspec.yaml | 2 +- packages/stream_chat/test/src/fakes.dart | 27 ++ packages/stream_chat/test/src/mocks.dart | 6 + .../test/src/ws/websocket_test.dart | 344 ++++++++++++++++++ packages/stream_chat/test/version_test.dart | 62 +--- 8 files changed, 400 insertions(+), 57 deletions(-) create mode 100644 packages/stream_chat/test/src/fakes.dart create mode 100644 packages/stream_chat/test/src/mocks.dart create mode 100644 packages/stream_chat/test/src/ws/websocket_test.dart diff --git a/packages/stream_chat/analysis_options.yaml b/packages/stream_chat/analysis_options.yaml index f0a87ea5..ed0ce983 100644 --- a/packages/stream_chat/analysis_options.yaml +++ b/packages/stream_chat/analysis_options.yaml @@ -6,7 +6,7 @@ analyzer: - example/** - lib/src/emoji - lib/**/*.freezed.dart - - test/** +# - test/** linter: rules: diff --git a/packages/stream_chat/lib/src/core/api/responses.dart b/packages/stream_chat/lib/src/core/api/responses.dart index b6d6e801..ccf03822 100644 --- a/packages/stream_chat/lib/src/core/api/responses.dart +++ b/packages/stream_chat/lib/src/core/api/responses.dart @@ -18,7 +18,7 @@ class _BaseResponse { } /// Model response for [StreamChatNetworkError] data -@JsonSerializable(createToJson: false) +@JsonSerializable() class ErrorResponse extends _BaseResponse { /// int? code; @@ -37,6 +37,9 @@ class ErrorResponse extends _BaseResponse { static ErrorResponse fromJson(Map json) => _$ErrorResponseFromJson(json); + /// Serialize to json + Map toJson() => _$ErrorResponseToJson(this); + @override String toString() => 'ErrorResponse(code: $code, ' 'message: $message, ' diff --git a/packages/stream_chat/lib/src/core/api/responses.g.dart b/packages/stream_chat/lib/src/core/api/responses.g.dart index c1c1f302..deba55f0 100644 --- a/packages/stream_chat/lib/src/core/api/responses.g.dart +++ b/packages/stream_chat/lib/src/core/api/responses.g.dart @@ -15,6 +15,15 @@ ErrorResponse _$ErrorResponseFromJson(Map json) { ..moreInfo = json['more_info'] as String?; } +Map _$ErrorResponseToJson(ErrorResponse instance) => + { + 'duration': instance.duration, + 'code': instance.code, + 'message': instance.message, + 'StatusCode': instance.statusCode, + 'more_info': instance.moreInfo, + }; + SyncResponse _$SyncResponseFromJson(Map json) { return SyncResponse() ..duration = json['duration'] as String? diff --git a/packages/stream_chat/pubspec.yaml b/packages/stream_chat/pubspec.yaml index f4554959..e6259b42 100644 --- a/packages/stream_chat/pubspec.yaml +++ b/packages/stream_chat/pubspec.yaml @@ -30,4 +30,4 @@ dev_dependencies: freezed: ^0.14.1+3 json_serializable: ^4.1.0 mocktail: ^0.1.1 - test: ^1.16.8 + test: ^1.16.8 \ No newline at end of file diff --git a/packages/stream_chat/test/src/fakes.dart b/packages/stream_chat/test/src/fakes.dart new file mode 100644 index 00000000..e96cbd12 --- /dev/null +++ b/packages/stream_chat/test/src/fakes.dart @@ -0,0 +1,27 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/core/http/token.dart'; +import 'package:stream_chat/src/core/http/token_manager.dart'; + +class FakeTokenManager extends Fake implements TokenManager { + final token = Token.development('test-user-id'); + + @override + bool get isStatic => true; + + @override + String? get userId => token.userId; + + @override + Future loadToken({bool refresh = false}) async => token; + + @override + Future setTokenOrProvider( + String userId, { + Token? token, + TokenProvider? provider, + }) async => + this.token; + + @override + void reset() {} +} diff --git a/packages/stream_chat/test/src/mocks.dart b/packages/stream_chat/test/src/mocks.dart new file mode 100644 index 00000000..98e84d77 --- /dev/null +++ b/packages/stream_chat/test/src/mocks.dart @@ -0,0 +1,6 @@ +import 'package:mocktail/mocktail.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +class MockWebSocketChannel extends Mock implements WebSocketChannel {} + +class MockWebSocketSink extends Mock implements WebSocketSink {} diff --git a/packages/stream_chat/test/src/ws/websocket_test.dart b/packages/stream_chat/test/src/ws/websocket_test.dart new file mode 100644 index 00000000..73eecf3f --- /dev/null +++ b/packages/stream_chat/test/src/ws/websocket_test.dart @@ -0,0 +1,344 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:stream_chat/src/core/http/token_manager.dart'; +import 'package:stream_chat/stream_chat.dart'; +import 'package:test/test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:stream_chat/src/ws/websocket.dart'; +import 'package:web_socket_channel/web_socket_channel.dart'; + +import '../fakes.dart'; +import '../mocks.dart'; + +void main() { + late TokenManager tokenManager; + late WebSocketChannel webSocketChannel; + late WebSocketSink webSocketSink; + late WebSocket webSocket; + + setUp(() { + tokenManager = FakeTokenManager(); + webSocketChannel = MockWebSocketChannel(); + + WebSocketChannel channelProvider( + Uri uri, { + Iterable? protocols, + }) => + webSocketChannel; + + webSocket = WebSocket( + apiKey: 'api-key', + baseUrl: 'base-url', + tokenManager: tokenManager, + webSocketChannelProvider: channelProvider, + ); + + webSocketSink = MockWebSocketSink(); + when(() => webSocketChannel.sink).thenReturn(webSocketSink); + + var webSocketController = StreamController.broadcast(); + when(() => webSocketChannel.stream).thenAnswer( + (_) => webSocketController.stream, + ); + when(() => webSocketSink.add(any())).thenAnswer((invocation) { + webSocketController.add(invocation.positionalArguments.first); + }); + when(() => webSocketSink.close(any(), any())).thenAnswer( + (_) { + final res = webSocketController.close(); + // re-initializing for future events + webSocketController = StreamController.broadcast(); + return res; + }, + ); + }); + + tearDown(() { + tokenManager.reset(); + webSocket.disconnect(); + }); + + test('`connect` successfully with the provided user', () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + addTearDown(timer.cancel); + }); + + test('`connect` should throw if already in connection attempt', () async { + final user = OwnUser(id: 'test-user'); + webSocket.connect(user); + try { + // calling again before previous attempt finishes + await webSocket.connect(user); + } catch (e) { + expect(e, isA()); + } + }); + + test('`connect` should throw if `onMessage` contains error', () async { + final user = OwnUser(id: 'test-user'); + final error = ErrorResponse() + ..code = 333 + ..message = 'Invalid request'; + // Sends error event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + webSocketSink.add(json.encode({'error': error})); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.disconnected, + ]), + ); + + try { + await webSocket.connect(user); + } catch (e) { + expect(e, isA()); + final err = e as StreamWebSocketError; + expect(err.code, error.code); + expect(err.message, error.message); + } + + addTearDown(timer.cancel); + }); + + test( + 'should `reconnect` automatically ' + 'if `onMessage` throws error after getting connected', + () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + // starts reconnecting + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + await webSocket.connect(user); + + final error = ErrorResponse() + ..code = 333 + ..message = 'Invalid request'; + // Sends error event to web-socket stream + webSocketSink.add(json.encode({'error': error})); + + final reconnectTimer = Timer(const Duration(seconds: 3), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expect(webSocket.connectionId, connectionId); + + addTearDown(() { + timer.cancel(); + reconnectTimer.cancel(); + }); + }, + ); + + test( + '`onMessage` should handle `health.check` event if `me` is null', + () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me, isNotNull); + expect(event.me!.id, user.id); + + const newConnectionId = 'new-connection-id'; + final healthCheckEvent = Event( + type: EventType.healthCheck, + connectionId: newConnectionId, + ); + webSocketSink.add(json.encode(healthCheckEvent)); + + await Future.delayed(const Duration(milliseconds: 300)); + + expectLater(webSocket.connectionId, newConnectionId); + + addTearDown(timer.cancel); + }, + ); + + test('should call `onConnectionError` if web-socket stream throws', () async { + final user = OwnUser(id: 'test-user'); + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + const error = StreamWebSocketError('test-error'); + webSocketSink.addError(error); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + // throws error, reconnects + ConnectionStatus.connected, + ]), + ); + + webSocket.connect(user); + + // Assuming web-socket stream will add error + // and web-socket now trying to reconnect + await Future.delayed(const Duration(seconds: 3)); + + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + + addTearDown(timer.cancel); + }); + + test( + 'should call `onConnectionClosed` if web-socket stream throws', + () async { + final user = OwnUser(id: 'test-user'); + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + webSocketSink.close(); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + // throws error, reconnects + ConnectionStatus.connected, + ]), + ); + + webSocket.connect(user); + + // Assuming web-socket stream will add error + // and web-socket now trying to reconnect + await Future.delayed(const Duration(seconds: 3)); + + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + + addTearDown(timer.cancel); + }, + ); + + test('`disconnect` successfully disconnects the current user', () async { + final user = OwnUser(id: 'test-user'); + const connectionId = 'test-connection-id'; + // Sends connect event to web-socket stream + final timer = Timer(const Duration(milliseconds: 300), () { + final event = Event( + type: EventType.healthCheck, + connectionId: connectionId, + me: user, + ); + webSocketSink.add(json.encode(event)); + }); + + expectLater( + webSocket.connectionStatusStream, + emitsInOrder([ + ConnectionStatus.disconnected, + ConnectionStatus.connecting, + ConnectionStatus.connected, + // after disconnect + ConnectionStatus.disconnected, + ]), + ); + + final event = await webSocket.connect(user); + + expect(event.type, EventType.healthCheck); + expect(event.connectionId, connectionId); + expect(event.me?.id, user.id); + + webSocket.disconnect(); + + addTearDown(timer.cancel); + }); +} diff --git a/packages/stream_chat/test/version_test.dart b/packages/stream_chat/test/version_test.dart index 29076db0..34ecc750 100644 --- a/packages/stream_chat/test/version_test.dart +++ b/packages/stream_chat/test/version_test.dart @@ -1,26 +1,7 @@ -import 'dart:async'; import 'dart:io'; -import 'package:stream_chat/stream_chat.dart'; import 'package:stream_chat/version.dart'; import 'package:test/test.dart'; -import 'dart:math' as math; - -// This alphabet uses `A-Za-z0-9_-` symbols. The genetic algorithm helped -// optimize the gzip compression for this alphabet. -const _alphabet = - 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW'; - -/// Generates a random String id -/// Adopted from: https://github.com/ai/nanoid/blob/main/non-secure/index.js -String randomId({int size = 21}) { - var id = ''; - for (var i = 0; i < size; i++) { - id += _alphabet[(math.Random().nextDouble() * 64).floor() | 0]; - } - return id; -} - void prepareTest() { // https://github.com/flutter/flutter/issues/20907 @@ -31,40 +12,13 @@ void prepareTest() { void main() { prepareTest(); - test('stream chat version matches pubspec', () async { - print(randomId()); - - // /// Create a new instance of [StreamChatClient] - // /// by passing the apikey obtained from your project dashboard. - // final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO); - // - // /// Set the current user. In a production scenario, this should be done using - // /// a backend to generate a user token using our server SDK. - // /// Please see the following for more information: - // /// https://getstream.io/chat/docs/ios_user_setup_and_tokens/ - // await client.connectUser( - // User( - // id: 'cool-shadow-7', - // extraData: { - // 'image': - // 'https://getstream.io/random_png/?id=cool-shadow-7&name=Cool+shadow', - // }, - // ), - // '''eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiY29vbC1zaGFkb3ctNyJ9.gkOlCRb1qgy4joHPaxFwPOdXcGvSPvp6QY0S4mpRkVo''', - // ); - // - // try { - // await client.banUser('asdasdas'); - // } catch (e) { - // print(e); - // } - - // final pubspecPath = '${Directory.current.path}/pubspec.yaml'; - // final pubspec = File(pubspecPath).readAsStringSync(); - // // ignore: unnecessary_string_escapes - // final regex = RegExp('version:\s*(.*)'); - // final match = regex.firstMatch(pubspec); - // expect(match, isNotNull); - // expect(PACKAGE_VERSION, match?.group(1)?.trim()); + test('stream chat version matches pubspec', () { + final pubspecPath = '${Directory.current.path}/pubspec.yaml'; + final pubspec = File(pubspecPath).readAsStringSync(); + // ignore: unnecessary_string_escapes + final regex = RegExp('version:\s*(.*)'); + final match = regex.firstMatch(pubspec); + expect(match, isNotNull); + expect(PACKAGE_VERSION, match?.group(1)?.trim()); }); }