// Copyright (c) 2014, the Dart project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. // ignore_for_file: inference_failure_on_instance_creation import 'dart:async'; import 'dart:convert'; import 'package:json_rpc_2/error_code.dart' as error_code; import 'package:json_rpc_2/json_rpc_2.dart' as json_rpc; import 'package:stream_channel/stream_channel.dart'; import 'package:test/test.dart'; void main() { late StreamSink incoming; late Stream outgoing; late json_rpc.Peer peer; setUp(() { var incomingController = StreamController(); incoming = incomingController.sink; var outgoingController = StreamController(); outgoing = outgoingController.stream; peer = json_rpc.Peer.withoutJson( StreamChannel(incomingController.stream, outgoingController)); }); group('like a client,', () { test('can send a message and receive a response', () { expect(outgoing.first.then((request) { expect( request, equals({ 'jsonrpc': '2.0', 'method': 'foo', 'params': {'bar': 'baz'}, 'id': 0 })); incoming.add({'jsonrpc': '2.0', 'result': 'qux', 'id': 0}); }), completes); peer.listen(); expect( peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux'))); }); test('can send a batch of messages and receive a batch of responses', () { expect(outgoing.first.then((request) { expect( request, equals([ { 'jsonrpc': '2.0', 'method': 'foo', 'params': {'bar': 'baz'}, 'id': 0 }, { 'jsonrpc': '2.0', 'method': 'a', 'params': {'b': 'c'}, 'id': 1 }, { 'jsonrpc': '2.0', 'method': 'w', 'params': {'x': 'y'}, 'id': 2 } ])); incoming.add([ {'jsonrpc': '2.0', 'result': 'qux', 'id': 0}, {'jsonrpc': '2.0', 'result': 'd', 'id': 1}, {'jsonrpc': '2.0', 'result': 'z', 'id': 2} ]); }), completes); peer.listen(); peer.withBatch(() { expect( peer.sendRequest('foo', {'bar': 'baz'}), completion(equals('qux'))); expect(peer.sendRequest('a', {'b': 'c'}), completion(equals('d'))); expect(peer.sendRequest('w', {'x': 'y'}), completion(equals('z'))); }); }); test('requests terminates when the channel is closed', () async { var incomingController = StreamController(); var channel = StreamChannel.withGuarantees( incomingController.stream, StreamController(), ); var peer = json_rpc.Peer.withoutJson(channel); unawaited(peer.listen()); var response = peer.sendRequest('foo'); await incomingController.close(); expect(response, throwsStateError); }); }); test('can be closed', () async { var incomingController = StreamController(); var channel = StreamChannel.withGuarantees( incomingController.stream, StreamController(), ); var peer = json_rpc.Peer.withoutJson(channel); unawaited(peer.listen()); await peer.close(); }); test('considered closed with misbehaving StreamChannel', () async { // If a StreamChannel does not enforce the guarantees stated in it's // contract - specifically that "Closing the sink causes the stream to close // before it emits any more events." - The `Peer` should still understand // when it has been closed manually. var channel = StreamChannel( StreamController().stream, StreamController(), ); var peer = json_rpc.Peer.withoutJson(channel); unawaited(peer.listen()); unawaited(peer.close()); expect(peer.isClosed, true); }); group('like a server,', () { test('can receive a call and return a response', () { expect(outgoing.first, completion(equals({'jsonrpc': '2.0', 'result': 'qux', 'id': 0}))); peer.registerMethod('foo', (_) => 'qux'); peer.listen(); incoming.add({ 'jsonrpc': '2.0', 'method': 'foo', 'params': {'bar': 'baz'}, 'id': 0 }); }); test('can receive a batch of calls and return a batch of responses', () { expect( outgoing.first, completion(equals([ {'jsonrpc': '2.0', 'result': 'qux', 'id': 0}, {'jsonrpc': '2.0', 'result': 'd', 'id': 1}, {'jsonrpc': '2.0', 'result': 'z', 'id': 2} ]))); peer.registerMethod('foo', (_) => 'qux'); peer.registerMethod('a', (_) => 'd'); peer.registerMethod('w', (_) => 'z'); peer.listen(); incoming.add([ { 'jsonrpc': '2.0', 'method': 'foo', 'params': {'bar': 'baz'}, 'id': 0 }, { 'jsonrpc': '2.0', 'method': 'a', 'params': {'b': 'c'}, 'id': 1 }, { 'jsonrpc': '2.0', 'method': 'w', 'params': {'x': 'y'}, 'id': 2 } ]); }); test('returns a response for malformed JSON', () { var incomingController = StreamController(); var outgoingController = StreamController(); var jsonPeer = json_rpc.Peer( StreamChannel(incomingController.stream, outgoingController)); expect( outgoingController.stream.first.then(jsonDecode), completion({ 'jsonrpc': '2.0', 'error': { 'code': error_code.PARSE_ERROR, 'message': startsWith('Invalid JSON: '), // TODO(nweiz): Always expect the source when sdk#25655 is fixed. 'data': { 'request': anyOf([isNull, '{invalid']) } }, 'id': null })); jsonPeer.listen(); incomingController.add('{invalid'); }); test('returns a response for incorrectly-structured JSON', () { expect( outgoing.first, completion({ 'jsonrpc': '2.0', 'error': { 'code': error_code.INVALID_REQUEST, 'message': 'Request must contain a "jsonrpc" key.', 'data': { 'request': {'completely': 'wrong'} } }, 'id': null })); peer.listen(); incoming.add({'completely': 'wrong'}); }); }); test('can notify on unhandled errors for if the method throws', () async { var exception = Exception('test exception'); var incomingController = StreamController(); var outgoingController = StreamController(); final completer = Completer(); peer = json_rpc.Peer.withoutJson( StreamChannel(incomingController.stream, outgoingController), onUnhandledError: (error, stack) { completer.complete(error); }, ); peer ..registerMethod('foo', () => throw exception) // ignore: unawaited_futures ..listen(); incomingController.add({'jsonrpc': '2.0', 'method': 'foo'}); var receivedException = await completer.future; expect(receivedException, equals(exception)); }); }