add websocket_test.dart
This commit is contained in:
@@ -6,7 +6,7 @@ analyzer:
|
|||||||
- example/**
|
- example/**
|
||||||
- lib/src/emoji
|
- lib/src/emoji
|
||||||
- lib/**/*.freezed.dart
|
- lib/**/*.freezed.dart
|
||||||
- test/**
|
# - test/**
|
||||||
|
|
||||||
linter:
|
linter:
|
||||||
rules:
|
rules:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class _BaseResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Model response for [StreamChatNetworkError] data
|
/// Model response for [StreamChatNetworkError] data
|
||||||
@JsonSerializable(createToJson: false)
|
@JsonSerializable()
|
||||||
class ErrorResponse extends _BaseResponse {
|
class ErrorResponse extends _BaseResponse {
|
||||||
///
|
///
|
||||||
int? code;
|
int? code;
|
||||||
@@ -37,6 +37,9 @@ class ErrorResponse extends _BaseResponse {
|
|||||||
static ErrorResponse fromJson(Map<String, dynamic> json) =>
|
static ErrorResponse fromJson(Map<String, dynamic> json) =>
|
||||||
_$ErrorResponseFromJson(json);
|
_$ErrorResponseFromJson(json);
|
||||||
|
|
||||||
|
/// Serialize to json
|
||||||
|
Map<String, dynamic> toJson() => _$ErrorResponseToJson(this);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ErrorResponse(code: $code, '
|
String toString() => 'ErrorResponse(code: $code, '
|
||||||
'message: $message, '
|
'message: $message, '
|
||||||
|
|||||||
@@ -15,6 +15,15 @@ ErrorResponse _$ErrorResponseFromJson(Map<String, dynamic> json) {
|
|||||||
..moreInfo = json['more_info'] as String?;
|
..moreInfo = json['more_info'] as String?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ErrorResponseToJson(ErrorResponse instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'duration': instance.duration,
|
||||||
|
'code': instance.code,
|
||||||
|
'message': instance.message,
|
||||||
|
'StatusCode': instance.statusCode,
|
||||||
|
'more_info': instance.moreInfo,
|
||||||
|
};
|
||||||
|
|
||||||
SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) {
|
SyncResponse _$SyncResponseFromJson(Map<String, dynamic> json) {
|
||||||
return SyncResponse()
|
return SyncResponse()
|
||||||
..duration = json['duration'] as String?
|
..duration = json['duration'] as String?
|
||||||
|
|||||||
@@ -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<Token> loadToken({bool refresh = false}) async => token;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Token> setTokenOrProvider(
|
||||||
|
String userId, {
|
||||||
|
Token? token,
|
||||||
|
TokenProvider? provider,
|
||||||
|
}) async =>
|
||||||
|
this.token;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void reset() {}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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<String>? protocols,
|
||||||
|
}) =>
|
||||||
|
webSocketChannel;
|
||||||
|
|
||||||
|
webSocket = WebSocket(
|
||||||
|
apiKey: 'api-key',
|
||||||
|
baseUrl: 'base-url',
|
||||||
|
tokenManager: tokenManager,
|
||||||
|
webSocketChannelProvider: channelProvider,
|
||||||
|
);
|
||||||
|
|
||||||
|
webSocketSink = MockWebSocketSink();
|
||||||
|
when(() => webSocketChannel.sink).thenReturn(webSocketSink);
|
||||||
|
|
||||||
|
var webSocketController = StreamController<String>.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<String>.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<StreamWebSocketError>());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
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<StreamWebSocketError>());
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,26 +1,7 @@
|
|||||||
import 'dart:async';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:stream_chat/stream_chat.dart';
|
|
||||||
import 'package:stream_chat/version.dart';
|
import 'package:stream_chat/version.dart';
|
||||||
import 'package:test/test.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() {
|
void prepareTest() {
|
||||||
// https://github.com/flutter/flutter/issues/20907
|
// https://github.com/flutter/flutter/issues/20907
|
||||||
@@ -31,40 +12,13 @@ void prepareTest() {
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
prepareTest();
|
prepareTest();
|
||||||
test('stream chat version matches pubspec', () async {
|
test('stream chat version matches pubspec', () {
|
||||||
print(randomId());
|
final pubspecPath = '${Directory.current.path}/pubspec.yaml';
|
||||||
|
final pubspec = File(pubspecPath).readAsStringSync();
|
||||||
// /// Create a new instance of [StreamChatClient]
|
// ignore: unnecessary_string_escapes
|
||||||
// /// by passing the apikey obtained from your project dashboard.
|
final regex = RegExp('version:\s*(.*)');
|
||||||
// final client = StreamChatClient('b67pax5b2wdq', logLevel: Level.INFO);
|
final match = regex.firstMatch(pubspec);
|
||||||
//
|
expect(match, isNotNull);
|
||||||
// /// Set the current user. In a production scenario, this should be done using
|
expect(PACKAGE_VERSION, match?.group(1)?.trim());
|
||||||
// /// 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());
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user