Files
fl-query/packages/fl_query/test/websocket_test.dart
T

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;
});
});
*/
});
}