432 lines
13 KiB
Dart
432 lines
13 KiB
Dart
import 'dart:async';
|
|
import 'dart:io';
|
|
|
|
import 'package:test/test.dart';
|
|
import 'dart:convert';
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:gql/language.dart';
|
|
import 'package:fl_query/fl_query.dart';
|
|
|
|
import './helpers.dart';
|
|
import './mock_server/ws_echo_server.dart';
|
|
import 'mock_server/ws_echo_server.dart';
|
|
|
|
SocketClient getTestClient(
|
|
{required String wsUrl,
|
|
StreamController? controller,
|
|
bool autoReconnect = true,
|
|
Map<String, dynamic>? customHeaders,
|
|
Duration delayBetweenReconnectionAttempts =
|
|
const Duration(milliseconds: 1)}) =>
|
|
SocketClient(
|
|
wsUrl,
|
|
config: SocketClientConfig(
|
|
autoReconnect: autoReconnect,
|
|
headers: customHeaders,
|
|
delayBetweenReconnectionAttempts: delayBetweenReconnectionAttempts,
|
|
),
|
|
randomBytesForUuid: Uint8List.fromList(
|
|
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
|
),
|
|
);
|
|
|
|
Future<void> main() async {
|
|
String wsUrl = await runWebSocketServer();
|
|
group('InitOperation', () {
|
|
test('null payload', () {
|
|
// ignore: deprecated_member_use_from_same_package
|
|
final operation = InitOperation(null);
|
|
expect(operation.toJson(), {'type': 'connection_init'});
|
|
});
|
|
test('simple payload', () {
|
|
// ignore: deprecated_member_use_from_same_package
|
|
final operation = InitOperation(42);
|
|
expect(operation.toJson(), {'type': 'connection_init', 'payload': 42});
|
|
});
|
|
test('complex payload', () {
|
|
// ignore: deprecated_member_use_from_same_package
|
|
final operation = InitOperation({
|
|
'value': 42,
|
|
'nested': {
|
|
'number': [3, 7],
|
|
'string': ['foo', 'bar']
|
|
}
|
|
});
|
|
expect(operation.toJson(), {
|
|
'type': 'connection_init',
|
|
'payload': {
|
|
'value': 42,
|
|
'nested': {
|
|
'number': [3, 7],
|
|
'string': ['foo', 'bar']
|
|
}
|
|
}
|
|
});
|
|
});
|
|
});
|
|
|
|
group('SocketClient without payload', () {
|
|
late SocketClient socketClient;
|
|
StreamController controller;
|
|
final expectedMessage = r'{'
|
|
r'"type":"start","id":"01020304-0506-4708-890a-0b0c0d0e0f10",'
|
|
r'"payload":{"operationName":null,"variables":{},"query":"subscription {\n \n}"}'
|
|
r'}';
|
|
setUp(overridePrint((log) {
|
|
controller = StreamController(sync: true);
|
|
socketClient = getTestClient(controller: controller, wsUrl: wsUrl);
|
|
}));
|
|
tearDown(overridePrint(
|
|
(log) => socketClient.dispose(),
|
|
));
|
|
test('connection', () async {
|
|
await expectLater(
|
|
socketClient.connectionState.asBroadcastStream(),
|
|
emitsInOrder(
|
|
[
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
],
|
|
),
|
|
);
|
|
});
|
|
test('disconnect via dispose', () async {
|
|
// First wait for connection to complete
|
|
await expectLater(
|
|
socketClient.connectionState.asBroadcastStream(),
|
|
emitsInOrder(
|
|
[
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
],
|
|
),
|
|
);
|
|
|
|
// We need to begin waiting on the connectionState
|
|
// before we issue the command to disconnect; otherwise
|
|
// it can reconnect so fast that it will be reconnected
|
|
// by the time that the expectLater check is initiated.
|
|
await overridePrint((_) async {
|
|
Timer(const Duration(milliseconds: 20), () async {
|
|
await socketClient.dispose();
|
|
});
|
|
})();
|
|
// The connectionState BehaviorController emits the current state
|
|
// to any new listener, so we expect it to start in the connected
|
|
// state and transition to notConnected because of dispose.
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connected,
|
|
SocketConnectionState.notConnected,
|
|
]),
|
|
);
|
|
|
|
// Have to wait for socket close to be fully processed after we reach
|
|
// the notConnected state, including updating channel with close code.
|
|
await Future.delayed(const Duration(milliseconds: 20));
|
|
|
|
// The websocket should be in a fully closed state at this point,
|
|
// we should have a confirmed close code in the channel.
|
|
expect(socketClient.socketChannel, isNotNull);
|
|
expect(socketClient.socketChannel!.closeCode, isNotNull);
|
|
});
|
|
test('subscription data', () async {
|
|
final payload = Request(
|
|
operation: Operation(document: parseString('subscription {}')),
|
|
);
|
|
final waitForConnection = true;
|
|
final subscriptionDataStream =
|
|
socketClient.subscribe(payload, waitForConnection);
|
|
await socketClient.connectionState
|
|
.where((state) => state == SocketConnectionState.connected)
|
|
.first;
|
|
|
|
// ignore: unawaited_futures
|
|
socketClient.socketChannel!.stream
|
|
.where((message) => message == expectedMessage)
|
|
.first
|
|
.then((_) {
|
|
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
'type': 'data',
|
|
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
'payload': {
|
|
'data': {'foo': 'bar'},
|
|
'errors': [
|
|
{'message': 'error and data can coexist'}
|
|
]
|
|
}
|
|
}));
|
|
});
|
|
|
|
await expectLater(
|
|
subscriptionDataStream,
|
|
emits(
|
|
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
Response(
|
|
data: {'foo': 'bar'},
|
|
errors: [
|
|
GraphQLError(message: 'error and data can coexist'),
|
|
],
|
|
context: Context().withEntry(ResponseExtensions(null)),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
test('resubscribe', () async {
|
|
final payload = Request(
|
|
operation: Operation(document: gql('subscription {}')),
|
|
);
|
|
final waitForConnection = true;
|
|
final subscriptionDataStream =
|
|
socketClient.subscribe(payload, waitForConnection);
|
|
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
]),
|
|
);
|
|
|
|
await overridePrint((_) async {
|
|
socketClient.onConnectionLost();
|
|
})();
|
|
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.notConnected,
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
]),
|
|
);
|
|
|
|
// ignore: unawaited_futures
|
|
socketClient.socketChannel!.stream
|
|
.where((message) => message == expectedMessage)
|
|
.first
|
|
.then((_) {
|
|
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
'type': 'data',
|
|
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
'payload': {
|
|
'data': {'foo': 'bar'},
|
|
'errors': [
|
|
{'message': 'error and data can coexist'}
|
|
]
|
|
}
|
|
}));
|
|
});
|
|
|
|
await expectLater(
|
|
subscriptionDataStream,
|
|
emits(
|
|
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
Response(
|
|
data: {'foo': 'bar'},
|
|
errors: [
|
|
GraphQLError(message: 'error and data can coexist'),
|
|
],
|
|
context: Context().withEntry(ResponseExtensions(null)),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
test('resubscribe after server disconnect', () async {
|
|
final payload = Request(
|
|
operation: Operation(document: gql('subscription {}')),
|
|
);
|
|
final waitForConnection = true;
|
|
final subscriptionDataStream =
|
|
socketClient.subscribe(payload, waitForConnection);
|
|
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
]),
|
|
);
|
|
|
|
// We need to begin waiting on the connectionState
|
|
// before we issue the command to disconnect; otherwise
|
|
// it can reconnect so fast that it will be reconnected
|
|
// by the time that the expectLater check is initiated.
|
|
Timer(const Duration(milliseconds: 20), () async {
|
|
socketClient.socketChannel!.sink.add(forceDisconnectCommand);
|
|
});
|
|
// The connectionState BehaviorController emits the current state
|
|
// to any new listener, so we expect it to start in the connected
|
|
// state, transition to notConnected, and then reconnect after that.
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connected,
|
|
SocketConnectionState.notConnected,
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
]),
|
|
);
|
|
|
|
// ignore: unawaited_futures
|
|
socketClient.socketChannel!.stream
|
|
.where((message) => message == expectedMessage)
|
|
.first
|
|
.then((_) {
|
|
socketClient.socketChannel!.sink.add(jsonEncode({
|
|
'type': 'data',
|
|
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
|
|
'payload': {
|
|
'data': {'foo': 'bar'},
|
|
'errors': [
|
|
{'message': 'error and data can coexist'}
|
|
]
|
|
}
|
|
}));
|
|
});
|
|
|
|
await expectLater(
|
|
subscriptionDataStream,
|
|
emits(
|
|
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
|
|
Response(
|
|
data: {'foo': 'bar'},
|
|
errors: [
|
|
GraphQLError(message: 'error and data can coexist'),
|
|
],
|
|
context: Context().withEntry(ResponseExtensions(null)),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}, tags: "integration");
|
|
|
|
group('SocketClient without autoReconnect', () {
|
|
late SocketClient socketClient;
|
|
StreamController controller;
|
|
setUp(overridePrint((log) {
|
|
controller = StreamController(sync: true);
|
|
socketClient = getTestClient(
|
|
controller: controller, wsUrl: wsUrl, autoReconnect: false);
|
|
}));
|
|
tearDown(overridePrint(
|
|
(log) => socketClient.dispose(),
|
|
));
|
|
test('server disconnect', () async {
|
|
final payload = Request(
|
|
operation: Operation(document: gql('subscription {}')),
|
|
);
|
|
final waitForConnection = true;
|
|
socketClient.subscribe(payload, waitForConnection);
|
|
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connecting,
|
|
SocketConnectionState.connected,
|
|
]),
|
|
);
|
|
|
|
Timer(const Duration(milliseconds: 20), () async {
|
|
socketClient.socketChannel!.sink.add(forceDisconnectCommand);
|
|
});
|
|
// Same strategy as elsewhere, start expecting the state on the
|
|
// stream before the disconnect actually happens...
|
|
await expectLater(
|
|
socketClient.connectionState,
|
|
emitsInOrder([
|
|
SocketConnectionState.connected,
|
|
SocketConnectionState.notConnected,
|
|
]),
|
|
);
|
|
|
|
expect(
|
|
socketClient.socketChannel!.closeCode, WebSocketStatus.normalClosure);
|
|
});
|
|
}, tags: "integration");
|
|
|
|
group('SocketClient with const payload', () {
|
|
late SocketClient socketClient;
|
|
const initPayload = {'token': 'mytoken'};
|
|
|
|
setUp(overridePrint((log) {
|
|
socketClient = SocketClient(
|
|
wsUrl,
|
|
config: SocketClientConfig(initialPayload: () => initPayload),
|
|
);
|
|
}));
|
|
|
|
tearDown(overridePrint(
|
|
(log) => expectLater(
|
|
socketClient.dispose().timeout(Duration(seconds: 1)),
|
|
completion(null),
|
|
),
|
|
));
|
|
|
|
test('connection', () async {
|
|
await socketClient.connectionState
|
|
.where((state) => state == SocketConnectionState.connected)
|
|
.first;
|
|
|
|
await expectLater(
|
|
socketClient.socketChannel!.stream.map((s) {
|
|
return jsonDecode(s)['payload'];
|
|
}),
|
|
emits(initPayload));
|
|
});
|
|
});
|
|
|
|
group('SocketClient with future payload', () {
|
|
late SocketClient socketClient;
|
|
const initPayload = {'token': 'mytoken'};
|
|
|
|
setUp(overridePrint((log) {
|
|
socketClient = SocketClient(
|
|
wsUrl,
|
|
config: SocketClientConfig(
|
|
initialPayload: () async {
|
|
await Future.delayed(Duration(seconds: 3));
|
|
return initPayload;
|
|
},
|
|
),
|
|
);
|
|
}));
|
|
|
|
tearDown(overridePrint((log) async {
|
|
await socketClient.dispose();
|
|
}));
|
|
|
|
test('connection', () async {
|
|
await socketClient.connectionState
|
|
.where((state) => state == SocketConnectionState.connected)
|
|
.first;
|
|
|
|
await expectLater(
|
|
socketClient.socketChannel!.stream.map((s) {
|
|
return jsonDecode(s)['payload'];
|
|
}),
|
|
emits(initPayload),
|
|
);
|
|
});
|
|
|
|
/*
|
|
FIXME: Testing the correct header in the request
|
|
group('SocketClient with custom headers with const payload', () {
|
|
const customHeaders = {'myHeader': 'myHeader'};
|
|
|
|
setUp(overridePrint((log) {
|
|
socketClient = getTestClient(wsUrl: wsUrl, customHeaders: customHeaders);
|
|
}));
|
|
|
|
test('check header', () async {
|
|
await socketClient.connectionState
|
|
.where((state) => state == SocketConnectionState.notConnected)
|
|
.first;
|
|
});
|
|
});
|
|
*/
|
|
});
|
|
}
|