diff --git a/lib/src/client.dart b/lib/src/client.dart index e59508b..9a9f2fa 100644 --- a/lib/src/client.dart +++ b/lib/src/client.dart @@ -59,6 +59,7 @@ class DeeplinkRpcClient { if (runningRequest == null) return; + if (runningRequest.completer.isCompleted) return; runningRequest.completer.complete(response); _runningRequests.remove(response.id); @@ -190,6 +191,8 @@ class _DeeplinkRpcResponseReceiver e, stack, ); + if (e is DeeplinkRpcFailure) rethrow; + throw const DeeplinkRpcResult.failure( failure: DeeplinkRpcFailure( code: DeeplinkRpcFailure.kServerError, diff --git a/test/client_server_communication_test.dart b/test/client_server_communication_test.dart index e98d79d..0c83cde 100644 --- a/test/client_server_communication_test.dart +++ b/test/client_server_communication_test.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:deeplink_rpc/deeplink_rpc.dart'; import 'package:deeplink_rpc/src/codec.dart'; import 'package:deeplink_rpc/src/url_launcher.dart'; @@ -13,27 +15,46 @@ class BrokenRPCCodec implements DeeplinkRpcCodec { } class MockUrlLauncher implements UrlLauncher { - MockUrlLauncher({ - required Future Function(Uri url) launchUrl, - }) : _launchUrl = launchUrl; + MockUrlLauncher() : launchUrlAction = _defaultLaunchUrl; + + static Future _defaultLaunchUrl(Uri _, {Logger? logger}) async => false; - final Future Function(Uri url) _launchUrl; + Future Function(Uri url, {Logger? logger}) launchUrlAction; @override Future canLaunchUrl(Uri url) async => true; @override - Future launchUrl(Uri url, {Logger? logger}) async => _launchUrl(url); + Future launchUrl(Uri url, {Logger? logger}) async => + launchUrlAction(url); +} + +class Context { + Context({ + DeeplinkRpcCodec? clientCodec, + DeeplinkRpcCodec? serverCodec, + }) { + clientUrlLauncherMock = MockUrlLauncher(); + client = DeeplinkRpcClient( + urlLauncher: clientUrlLauncherMock, + codec: clientCodec, + ); + serverUrlLauncherMock = MockUrlLauncher(); + server = DeeplinkRpcServer( + urlLauncher: serverUrlLauncherMock, + codec: serverCodec, + ); + } + + late final MockUrlLauncher clientUrlLauncherMock; + late final DeeplinkRpcClient client; + late final MockUrlLauncher serverUrlLauncherMock; + late final DeeplinkRpcServer server; } void main() { const defaultTimeout = Duration(milliseconds: 1); - late MockUrlLauncher clientUrlLauncherMock; - late DeeplinkRpcClient client; - late MockUrlLauncher serverUrlLauncherMock; - late DeeplinkRpcServer server; - bool _isSameOrigin(Uri uri1, Uri uri2) { return uri1.scheme == uri2.scheme && uri1.host == uri2.host && @@ -42,59 +63,67 @@ void main() { uri1.fragment == uri2.fragment; } - void _setupClientAndServer({ + Context _setupClientAndServer({ DeeplinkRpcCodec? clientCodec, + DeeplinkRpcCodec? serverCodec, String serverBasePath = 'serverapp://server.app/request_endpoint', String clientBasePath = 'clientapp://client.app/reply_endpoint', }) { + final serverClient = Context( + clientCodec: clientCodec, + serverCodec: serverCodec, + ); + final serverBaseUri = Uri.parse(serverBasePath); - clientUrlLauncherMock = MockUrlLauncher( - launchUrl: (uri) async { - if (!_isSameOrigin(uri, serverBaseUri)) { - return false; - } - await server.handle( + serverClient.clientUrlLauncherMock.launchUrlAction = ( + uri, { + Logger? logger, + }) async { + if (!_isSameOrigin(uri, serverBaseUri)) { + return false; + } + unawaited( + serverClient.server.handle( Uri( path: uri.path, query: uri.query, ), - ); - return true; - }, - ); - client = DeeplinkRpcClient( - urlLauncher: clientUrlLauncherMock, - codec: clientCodec, - ); + ), + ); + return true; + }; + final clientBaseUri = Uri.parse(clientBasePath); - serverUrlLauncherMock = MockUrlLauncher( - launchUrl: (uri) async { - if (!_isSameOrigin(uri, clientBaseUri)) { - return false; - } + serverClient.serverUrlLauncherMock.launchUrlAction = ( + uri, { + Logger? logger, + }) async { + if (!_isSameOrigin(uri, clientBaseUri)) { + return false; + } - return client.handleResponse( - Uri( - path: uri.path, - query: uri.query, - ), - ); - }, - ); - server = DeeplinkRpcServer( - urlLauncher: serverUrlLauncherMock, - ); + serverClient.client.handleResponse( + Uri( + path: uri.path, + query: uri.query, + ), + ); + return true; + }; + + return serverClient; } - void _setupClientWithNoServer() { - clientUrlLauncherMock = MockUrlLauncher( - launchUrl: (uri) async { - return false; - }, - ); - client = DeeplinkRpcClient( - urlLauncher: clientUrlLauncherMock, - ); + Context _setupClientWithNoServer() { + final serverClient = Context(); + + serverClient.clientUrlLauncherMock.launchUrlAction = ( + uri, { + Logger? logger, + }) async => + false; + + return serverClient; } for (final setup in [ @@ -138,11 +167,11 @@ void main() { test( 'Client should receive a success response when call is valid (${setup.name})', () async { - _setupClientAndServer( + final context = _setupClientAndServer( clientBasePath: setup.replyUrl, serverBasePath: setup.requestUrl, ); - server.registerHandler( + context.server.registerHandler( DeeplinkRpcRequestHandler( route: DeeplinkRpcRoute(setup.serverRoute), handle: (request) => { @@ -152,7 +181,7 @@ void main() { ), ); - final response = await client.send( + final response = await context.client.send( timeout: defaultTimeout, request: DeeplinkRpcRequest( requestUrl: setup.requestUrl, @@ -175,8 +204,8 @@ void main() { test('Client should receive a failure when called deeplink has no receiver', () async { - _setupClientWithNoServer(); - final response = await client.send( + final context = _setupClientWithNoServer(); + final response = await context.client.send( timeout: defaultTimeout, request: DeeplinkRpcRequest( requestUrl: 'serverapp://server.app/request_endpoint', @@ -195,24 +224,30 @@ void main() { test( 'Client should receive a timeout failure when call is longer than expected', () async { - _setupClientAndServer(); + final context = _setupClientAndServer(); - server.registerHandler( + context.server.registerHandler( DeeplinkRpcRequestHandler( - route: const DeeplinkRpcRoute('request_endpoint'), + route: const DeeplinkRpcRoute('/request_endpoint'), handle: (request) async { await Future.delayed(const Duration(milliseconds: 1)); - return {}; + return { + 'responseString': 'aString', + 'responseInt': 4, + }; }, ), ); - final response = await client.send( + final response = await context.client.send( timeout: const Duration(microseconds: 1), request: DeeplinkRpcRequest( requestUrl: 'serverapp://server.app/request_endpoint', replyUrl: 'clientapp://client.app/reply_endpoint', - params: {}, + params: { + 'clientProp1': 'value1', + 'clientProp2': 2, + }, ), ); expect( @@ -227,18 +262,18 @@ void main() { test( "Client should not receive response when call format is invalid (because the server doesn't know at which URL to send the response)", () async { - _setupClientAndServer( + final context = _setupClientAndServer( clientCodec: BrokenRPCCodec(), ); - server.registerHandler( + context.server.registerHandler( DeeplinkRpcRequestHandler( - route: const DeeplinkRpcRoute('request_endpoint'), + route: const DeeplinkRpcRoute('/request_endpoint'), handle: (request) async => {}, ), ); - final response = await client.send( + final response = await context.client.send( timeout: defaultTimeout, request: DeeplinkRpcRequest( requestUrl: 'serverapp://server.app/request_endpoint', @@ -257,9 +292,9 @@ void main() { test('Client should receive a failure when server endpoint does not exist', () async { - _setupClientAndServer(); + final context = _setupClientAndServer(); - final response = await client.send( + final response = await context.client.send( timeout: defaultTimeout, request: DeeplinkRpcRequest( requestUrl: 'serverapp://server.app/unknown_endpoint',