Skip to content

Commit 02f3baa

Browse files
committed
test(graphql): add test for graphql-transport-ws
Make websocket test server respond with `connection_ack` when graphql-transport-ws is used and test that state change sequence includes `handshake`.
1 parent e1b7f82 commit 02f3baa

File tree

2 files changed

+287
-3
lines changed

2 files changed

+287
-3
lines changed

packages/graphql/test/mock_server/ws_echo_server.dart

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
/// to run the test and cover the web socket test
33
///
44
/// author: https://github.com/vincenzopalazzo
5+
import 'dart:convert';
56
import 'dart:io';
67

78
const String forceDisconnectCommand = '___force_disconnect___';
@@ -17,10 +18,19 @@ Future<String> runWebSocketServer(
1718
/// Handle event received on server.
1819
void onWebSocketData(WebSocket client) {
1920
client.listen((data) async {
20-
if (data != null && data.toString().contains(forceDisconnectCommand)) {
21+
if (data == forceDisconnectCommand) {
2122
client.close(WebSocketStatus.normalClosure, 'shutting down');
2223
} else {
23-
client.add(data);
24+
final message = json.decode(data.toString());
25+
if (message['type'] == 'connection_init' &&
26+
message['payload']?['protocol'] == 'graphql-transport-ws') {
27+
client.add(json.encode({
28+
'type': 'connection_ack',
29+
'payload': null,
30+
}));
31+
} else {
32+
client.add(data);
33+
}
2434
}
2535
});
2636
}

packages/graphql/test/websocket_test.dart

Lines changed: 275 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ SocketClient getTestClient(
1818
bool autoReconnect = true,
1919
Map<String, dynamic>? customHeaders,
2020
Duration delayBetweenReconnectionAttempts =
21-
const Duration(milliseconds: 1)}) =>
21+
const Duration(milliseconds: 1),
22+
String protocol = SocketSubProtocol.graphqlWs,
23+
}) =>
2224
SocketClient(
2325
wsUrl,
26+
protocol: protocol,
2427
config: SocketClientConfig(
2528
autoReconnect: autoReconnect,
2629
headers: customHeaders,
2730
delayBetweenReconnectionAttempts: delayBetweenReconnectionAttempts,
31+
initialPayload: {
32+
'protocol': protocol,
33+
},
2834
),
2935
randomBytesForUuid: Uint8List.fromList(
3036
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
@@ -324,6 +330,274 @@ Future<void> main() async {
324330
});
325331
}, tags: "integration");
326332

333+
group('SocketClient without payload graphql-transport-ws', () {
334+
late SocketClient socketClient;
335+
StreamController<dynamic> controller;
336+
final expectedMessage = r'{'
337+
r'"type":"subscribe","id":"01020304-0506-4708-890a-0b0c0d0e0f10",'
338+
r'"payload":{"operationName":null,"variables":{},"query":"subscription {\n \n}"}'
339+
r'}';
340+
setUp(overridePrint((log) {
341+
controller = StreamController(sync: true);
342+
socketClient = getTestClient(
343+
controller: controller,
344+
protocol: SocketSubProtocol.graphqlTransportWs,
345+
wsUrl: wsUrl,
346+
);
347+
}));
348+
tearDown(overridePrint(
349+
(log) => socketClient.dispose(),
350+
));
351+
test('connection graphql-transport-ws', () async {
352+
await expectLater(
353+
socketClient.connectionState.asBroadcastStream(),
354+
emitsInOrder(
355+
[
356+
SocketConnectionState.connecting,
357+
SocketConnectionState.handshake,
358+
SocketConnectionState.connected,
359+
],
360+
),
361+
);
362+
});
363+
test('disconnect via dispose graphql-transport-ws', () async {
364+
// First wait for connection to complete
365+
await expectLater(
366+
socketClient.connectionState.asBroadcastStream(),
367+
emitsInOrder(
368+
[
369+
SocketConnectionState.connecting,
370+
SocketConnectionState.handshake,
371+
SocketConnectionState.connected,
372+
],
373+
),
374+
);
375+
376+
// We need to begin waiting on the connectionState
377+
// before we issue the command to disconnect; otherwise
378+
// it can reconnect so fast that it will be reconnected
379+
// by the time that the expectLater check is initiated.
380+
await overridePrint((_) async {
381+
Timer(const Duration(milliseconds: 20), () async {
382+
await socketClient.dispose();
383+
});
384+
})();
385+
// The connectionState BehaviorController emits the current state
386+
// to any new listener, so we expect it to start in the connected
387+
// state and transition to notConnected because of dispose.
388+
await expectLater(
389+
socketClient.connectionState,
390+
emitsInOrder([
391+
SocketConnectionState.connected,
392+
SocketConnectionState.notConnected,
393+
]),
394+
);
395+
396+
// Have to wait for socket close to be fully processed after we reach
397+
// the notConnected state, including updating channel with close code.
398+
await Future<void>.delayed(const Duration(milliseconds: 20));
399+
400+
// The websocket should be in a fully closed state at this point,
401+
// we should have a confirmed close code in the channel.
402+
expect(socketClient.socketChannel, isNotNull);
403+
expect(socketClient.socketChannel!.closeCode, isNotNull);
404+
});
405+
test('subscription data graphql-transport-ws', () async {
406+
final payload = Request(
407+
operation: Operation(document: parseString('subscription {}')),
408+
);
409+
final waitForConnection = true;
410+
final subscriptionDataStream =
411+
socketClient.subscribe(payload, waitForConnection);
412+
await socketClient.connectionState
413+
.where((state) => state == SocketConnectionState.connected)
414+
.first;
415+
416+
// ignore: unawaited_futures
417+
socketClient.socketChannel!.stream
418+
.where((message) => message == expectedMessage)
419+
.first
420+
.then((_) {
421+
socketClient.socketChannel!.sink.add(jsonEncode({
422+
'type': 'next',
423+
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
424+
'payload': {
425+
'data': {'foo': 'bar'},
426+
'errors': [
427+
{'message': 'error and data can coexist'}
428+
]
429+
}
430+
}));
431+
});
432+
433+
await expectLater(
434+
subscriptionDataStream,
435+
emits(
436+
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
437+
Response(
438+
data: {'foo': 'bar'},
439+
errors: [
440+
GraphQLError(message: 'error and data can coexist'),
441+
],
442+
context: Context().withEntry(ResponseExtensions(null)),
443+
response: {
444+
"type": "next",
445+
"data": {"foo": "bar"},
446+
"errors": [
447+
{"message": "error and data can coexist"}
448+
]
449+
},
450+
),
451+
),
452+
);
453+
});
454+
test('resubscribe', () async {
455+
final payload = Request(
456+
operation: Operation(document: gql('subscription {}')),
457+
);
458+
final waitForConnection = true;
459+
final subscriptionDataStream =
460+
socketClient.subscribe(payload, waitForConnection);
461+
462+
await expectLater(
463+
socketClient.connectionState,
464+
emitsInOrder([
465+
SocketConnectionState.connecting,
466+
SocketConnectionState.handshake,
467+
SocketConnectionState.connected,
468+
]),
469+
);
470+
471+
await overridePrint((_) async {
472+
socketClient.onConnectionLost();
473+
})();
474+
475+
await expectLater(
476+
socketClient.connectionState,
477+
emitsInOrder([
478+
SocketConnectionState.notConnected,
479+
SocketConnectionState.connecting,
480+
SocketConnectionState.handshake,
481+
SocketConnectionState.connected,
482+
]),
483+
);
484+
485+
// ignore: unawaited_futures
486+
socketClient.socketChannel!.stream
487+
.where((message) => message == expectedMessage)
488+
.first
489+
.then((_) {
490+
socketClient.socketChannel!.sink.add(jsonEncode({
491+
'type': 'next',
492+
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
493+
'payload': {
494+
'data': {'foo': 'bar'},
495+
'errors': [
496+
{'message': 'error and data can coexist'}
497+
]
498+
}
499+
}));
500+
});
501+
502+
await expectLater(
503+
subscriptionDataStream,
504+
emits(
505+
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
506+
Response(
507+
data: {'foo': 'bar'},
508+
errors: [
509+
GraphQLError(message: 'error and data can coexist'),
510+
],
511+
context: Context().withEntry(ResponseExtensions(null)),
512+
response: {
513+
"type": "next",
514+
"data": {"foo": "bar"},
515+
"errors": [
516+
{"message": "error and data can coexist"}
517+
]
518+
},
519+
),
520+
),
521+
);
522+
});
523+
test('resubscribe after server disconnect', () async {
524+
final payload = Request(
525+
operation: Operation(document: gql('subscription {}')),
526+
);
527+
final waitForConnection = true;
528+
final subscriptionDataStream =
529+
socketClient.subscribe(payload, waitForConnection);
530+
531+
await expectLater(
532+
socketClient.connectionState,
533+
emitsInOrder([
534+
SocketConnectionState.connecting,
535+
SocketConnectionState.handshake,
536+
SocketConnectionState.connected,
537+
]),
538+
);
539+
540+
// We need to begin waiting on the connectionState
541+
// before we issue the command to disconnect; otherwise
542+
// it can reconnect so fast that it will be reconnected
543+
// by the time that the expectLater check is initiated.
544+
Timer(const Duration(milliseconds: 20), () async {
545+
socketClient.socketChannel!.sink.add(forceDisconnectCommand);
546+
});
547+
// The connectionState BehaviorController emits the current state
548+
// to any new listener, so we expect it to start in the connected
549+
// state, transition to notConnected, and then reconnect after that.
550+
await expectLater(
551+
socketClient.connectionState,
552+
emitsInOrder([
553+
SocketConnectionState.connected,
554+
SocketConnectionState.notConnected,
555+
SocketConnectionState.connecting,
556+
SocketConnectionState.handshake,
557+
SocketConnectionState.connected,
558+
]),
559+
);
560+
561+
// ignore: unawaited_futures
562+
socketClient.socketChannel!.stream
563+
.where((message) => message == expectedMessage)
564+
.first
565+
.then((_) {
566+
socketClient.socketChannel!.sink.add(jsonEncode({
567+
'type': 'next',
568+
'id': '01020304-0506-4708-890a-0b0c0d0e0f10',
569+
'payload': {
570+
'data': {'foo': 'bar'},
571+
'errors': [
572+
{'message': 'error and data can coexist'}
573+
]
574+
}
575+
}));
576+
});
577+
578+
await expectLater(
579+
subscriptionDataStream,
580+
emits(
581+
// todo should ids be included in response context? probably '01020304-0506-4708-890a-0b0c0d0e0f10'
582+
Response(
583+
data: {'foo': 'bar'},
584+
errors: [
585+
GraphQLError(message: 'error and data can coexist'),
586+
],
587+
context: Context().withEntry(ResponseExtensions(null)),
588+
response: {
589+
"type": "next",
590+
"data": {"foo": "bar"},
591+
"errors": [
592+
{"message": "error and data can coexist"}
593+
]
594+
},
595+
),
596+
),
597+
);
598+
});
599+
}, tags: "integration");
600+
327601
group('SocketClient without autoReconnect', () {
328602
late SocketClient socketClient;
329603
StreamController<dynamic> controller;

0 commit comments

Comments
 (0)