From 6f91ddb5703fadef5b5834079df238a47d3c3534 Mon Sep 17 00:00:00 2001 From: Tyler-Larkin Date: Wed, 5 Jun 2024 10:21:40 -0700 Subject: [PATCH 1/3] chore(datastore): added unit tests for Amplify Api Native Bridge Unit tests for Query, Mutate, Subscribe, and Unsubscribe --- .../lib/amplify_datastore.dart | 9 +- .../test/native_amplify_api_test.dart | 971 ++++++++++++++++++ 2 files changed, 976 insertions(+), 4 deletions(-) create mode 100644 packages/amplify_datastore/test/native_amplify_api_test.dart diff --git a/packages/amplify_datastore/lib/amplify_datastore.dart b/packages/amplify_datastore/lib/amplify_datastore.dart index 685165c76a..35a949c4d9 100644 --- a/packages/amplify_datastore/lib/amplify_datastore.dart +++ b/packages/amplify_datastore/lib/amplify_datastore.dart @@ -110,7 +110,7 @@ class AmplifyDataStore extends DataStorePluginInterface config.api?.awsPlugin?.all.entries.forEach((e) { endpoints[e.key] = e.value.authorizationType.name; }); - final nativePlugin = _NativeAmplifyApi(authProviders); + final nativePlugin = NativeAmplifyApi(authProviders); NativeApiPlugin.setup(nativePlugin); final nativeBridge = NativeApiBridge(); @@ -292,10 +292,11 @@ class _NativeAmplifyAuthCognito String get runtimeTypeName => '_NativeAmplifyAuthCognito'; } -class _NativeAmplifyApi +@visibleForTesting +class NativeAmplifyApi with AWSDebuggable, AmplifyLoggerMixin implements NativeApiPlugin { - _NativeAmplifyApi(this._authProviders); + NativeAmplifyApi(this._authProviders); /// The registered [APIAuthProvider] instances. final Map, APIAuthProvider> @@ -305,7 +306,7 @@ class _NativeAmplifyApi _subscriptionsCache = {}; @override - String get runtimeTypeName => '_NativeAmplifyApi'; + String get runtimeTypeName => 'NativeAmplifyApi'; @override Future getLatestAuthToken(String providerName) { diff --git a/packages/amplify_datastore/test/native_amplify_api_test.dart b/packages/amplify_datastore/test/native_amplify_api_test.dart new file mode 100644 index 0000000000..a1a8eb63d4 --- /dev/null +++ b/packages/amplify_datastore/test/native_amplify_api_test.dart @@ -0,0 +1,971 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import 'dart:async'; + +import 'package:amplify_core/amplify_core.dart'; +import 'package:amplify_datastore/amplify_datastore.dart'; +import 'package:amplify_datastore/src/native_plugin.g.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() async { + TestWidgetsFlutterBinding binding = + TestWidgetsFlutterBinding.ensureInitialized(); + + BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.amplify_datastore.NativeApiBridge.sendSubscriptionEvent', + MockNativeAuthBridgeCodec(), + ); + + MockAPIPlugin mockAPIPlugin = MockAPIPlugin(); + + setUp(() async { + mockAPIPlugin.clear(); + + await Amplify.API.addPlugin( + mockAPIPlugin, + authProviderRepo: AmplifyAuthProviderRepository(), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + null, + ); + }); + + tearDown(() async { + await Amplify.reset(); + }); + + group('NativeAmplifyAPI', () { + group('Query', () { + test('Empty Request/Response', () async { + String document = ''; + Map emptyMap = {}; + String payloadJson = '{"data":{},"errors":[]}'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.document, document); + expect(mockRequest.variables, emptyMap); + expect(mockRequest.apiName, null); + expect(mockRequest.authorizationMode, null); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: null as String?, + errors: [], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest(document: ''); + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + + expect(response.payloadJson, payloadJson); + }); + + test('Filled Request/Response', () async { + String document = 'document'; + String apiName = 'apiName'; + String variable1Key = 'variable1'; + int variable1Value = 1; + String variable2Key = 'variable2'; + String variable2Value = 'test string'; + String variablesJson = + '{"$variable1Key": $variable1Value, "$variable2Key": "$variable2Value"}'; + String responseType = 'responseType'; + String decodePath = 'decodePath'; + String options = 'options'; + String data = '{"data1":1,"data2":"data string"}'; + String errorMessage1 = 'errorsJson1'; + String errorMessage2 = 'errorsJson2'; + String payloadJson = + '{"data":$data,"errors":[{"message":"$errorMessage1"},{"message":"$errorMessage2"}]}'; + String authMode = 'apiKey'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.id.length, greaterThan(0)); + expect(mockRequest.document, document); + expect(mockRequest.variables[variable1Key], variable1Value); + expect(mockRequest.variables[variable2Key], variable2Value); + expect(mockRequest.apiName, apiName); + expect(mockRequest.authorizationMode, APIAuthorizationType.apiKey); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: data as String?, + errors: [ + GraphQLResponseError(message: errorMessage1), + GraphQLResponseError(message: errorMessage2), + ], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: document, + apiName: apiName, + variablesJson: variablesJson, + responseType: responseType, + decodePath: decodePath, + options: options, + authMode: authMode); + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + + expect(response.payloadJson, payloadJson); + }); + + test('Empty Response Parse Exception', () async { + String document = ''; + String data = 'Invalid Json'; + Map emptyMap = {}; + String payloadJson = + '{"data":{},"errors":[{"message":"Error parsing payload json: FormatException: Unexpected character (at character 1)\\nInvalid Json\\n^\\n"}]}'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.document, document); + expect(mockRequest.variables, emptyMap); + expect(mockRequest.apiName, null); + expect(mockRequest.authorizationMode, null); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: data as String?, + errors: [], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest(document: ''); + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + + expect(response.payloadJson, payloadJson); + }); + + Future _authModeExpectHelepr( + String? authMode, + APIAuthorizationType? expected, + ) async { + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.authorizationMode, expected); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: null as String?, + errors: [], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + await nativeAmplifyApi.query( + NativeGraphQLRequest( + document: '', + authMode: authMode, + ), + ); + } + + test('AuthModeTypes', () async { + await _authModeExpectHelepr('apiKey', APIAuthorizationType.apiKey); + await _authModeExpectHelepr('awsIAM', APIAuthorizationType.iam); + await _authModeExpectHelepr('openIDConnect', APIAuthorizationType.oidc); + await _authModeExpectHelepr( + 'amazonCognitoUserPools', APIAuthorizationType.userPools); + await _authModeExpectHelepr('function', APIAuthorizationType.function); + await _authModeExpectHelepr('none', APIAuthorizationType.none); + await _authModeExpectHelepr(null, null); + }); + + test('Non-AmplifyException Exception', () async { + String document = ''; + String invalidJson = 'INVALID JSON'; + String payloadJson = + '{"errors":[{"message":"FormatException: Unexpected character (at character 1)\\nINVALID JSON\\n^\\n"}]}'; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: document, + variablesJson: invalidJson, + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + expect(response.payloadJson, payloadJson); + }); + + test('AmplifyException Exception', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + throw NetworkException(exceptionMessage); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + expect(response.payloadJson, payloadJson); + }); + + test('AmplifyException', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + throw NetworkException(exceptionMessage); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + expect(response.payloadJson, payloadJson); + }); + + test('Unauthorized AmplifyException', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; + + mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { + throw NetworkException( + exceptionMessage, + underlyingException: 'SignedOutException', + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.query(request); + expect(response.payloadJson, payloadJson); + }); + }); + + group('Mutate', () { + test('Empty Request/Response', () async { + String document = ''; + Map emptyMap = {}; + String payloadJson = '{"data":{},"errors":[]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.document, document); + expect(mockRequest.variables, emptyMap); + expect(mockRequest.apiName, null); + expect(mockRequest.authorizationMode, null); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: null as String?, + errors: [], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest(document: document); + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + + expect(response.payloadJson, payloadJson); + }); + + test('Filled Request/Response', () async { + String document = 'document'; + String apiName = 'apiName'; + String authMode = 'apiKey'; + String variable1Key = 'variable1'; + int variable1Value = 1; + String variable2Key = 'variable2'; + String variable2Value = 'test string'; + String variablesJson = + '{"$variable1Key": $variable1Value, "$variable2Key": "$variable2Value"}'; + String responseType = 'responseType'; + String decodePath = 'decodePath'; + String options = 'options'; + String data = '{"data1":1,"data2":"data string"}'; + String errorMessage1 = 'errorsJson1'; + String errorMessage2 = 'errorsJson2'; + String payloadJson = + '{"data":$data,"errors":[{"message":"$errorMessage1"},{"message":"$errorMessage2"}]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.id.length, greaterThan(0)); + expect(mockRequest.document, document); + expect(mockRequest.variables[variable1Key], variable1Value); + expect(mockRequest.variables[variable2Key], variable2Value); + expect(mockRequest.apiName, apiName); + expect(mockRequest.authorizationMode, APIAuthorizationType.apiKey); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: data as String?, + errors: [ + GraphQLResponseError(message: errorMessage1), + GraphQLResponseError(message: errorMessage2), + ], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: document, + apiName: apiName, + authMode: authMode, + variablesJson: variablesJson, + responseType: responseType, + decodePath: decodePath, + options: options, + ); + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + + expect(response.payloadJson, payloadJson); + }); + + test('Empty Response Parse Exception', () async { + String document = ''; + String data = 'Invalid Json'; + Map emptyMap = {}; + String payloadJson = + '{"data":{},"errors":[{"message":"Error parsing payload json: FormatException: Unexpected character (at character 1)\\nInvalid Json\\n^\\n"}]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + expect(mockRequest.document, document); + expect(mockRequest.variables, emptyMap); + expect(mockRequest.apiName, null); + expect(mockRequest.authorizationMode, null); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + return GraphQLOperation( + CancelableOperation>.fromValue( + GraphQLResponse( + data: data as String?, + errors: [], + ), + ), + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest(document: ''); + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + + expect(response.payloadJson, payloadJson); + }); + + test('AmplifyException Exception', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + throw NetworkException(exceptionMessage); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + expect(response.payloadJson, payloadJson); + }); + + test('AmplifyException', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + throw NetworkException(exceptionMessage); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + expect(response.payloadJson, payloadJson); + }); + + test('Unauthorized AmplifyException', () async { + String exceptionMessage = 'API Exception'; + String payloadJson = + '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; + + mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { + throw NetworkException( + exceptionMessage, + underlyingException: 'SignedOutException', + ); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + NativeGraphQLResponse response = await nativeAmplifyApi.mutate(request); + expect(response.payloadJson, payloadJson); + }); + }); + + group('Subscribe', () { + test('Empty Request/Response', () async { + String responseType = 'connecting'; + String document = ''; + Map emptyMap = {}; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + expect(mockRequest.document, document); + expect(mockRequest.variables, emptyMap); + expect(mockRequest.apiName, null); + expect(mockRequest.authorizationMode, null); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + var controller = StreamController>(); + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest(document: document); + NativeGraphQLSubscriptionResponse response = + await nativeAmplifyApi.subscribe(request); + + expect(response.payloadJson, null); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, responseType); + }); + + test('Filled Request/Response', () async { + String responseType = 'connecting'; + String document = 'document'; + String apiName = 'apiName'; + String authMode = 'apiKey'; + String variable1Key = 'variable1'; + int variable1Value = 1; + String variable2Key = 'variable2'; + String variable2Value = 'test string'; + String variablesJson = + '{"$variable1Key": $variable1Value, "$variable2Key": "$variable2Value"}'; + String decodePath = 'decodePath'; + String options = 'options'; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + expect(mockRequest.id.length, greaterThan(0)); + expect(mockRequest.document, document); + expect(mockRequest.variables[variable1Key], variable1Value); + expect(mockRequest.variables[variable2Key], variable2Value); + expect(mockRequest.apiName, apiName); + expect(mockRequest.authorizationMode, APIAuthorizationType.apiKey); + expect(mockRequest.headers, null); + expect(mockRequest.decodePath, null); + expect(mockRequest.modelType, null); + + var controller = StreamController>(); + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: document, + apiName: apiName, + authMode: authMode, + variablesJson: variablesJson, + responseType: responseType, + decodePath: decodePath, + options: options, + ); + NativeGraphQLSubscriptionResponse response = + await nativeAmplifyApi.subscribe(request); + + expect(response.payloadJson, null); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, responseType); + }); + + test('Invalid Request Variables Json Exception', () async { + String document = ''; + String invalidJson = 'INVALID JSON'; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: document, + variablesJson: invalidJson, + ); + + expect(() async => await nativeAmplifyApi.subscribe(request), + throwsA(TypeMatcher())); + }); + + test('API Exception', () async { + String exceptionMessage = 'API Exception'; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + throw NetworkException(exceptionMessage); + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + expect( + () async => await nativeAmplifyApi.subscribe(request), + throwsA( + predicate( + (e) => e is NetworkException && e.message == 'API Exception'), + ), + ); + }); + + test('Established/Connected Callback', () async { + void Function()? onEstablishedCallback; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + onEstablishedCallback = onEstablished; + var controller = StreamController>(); + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + NativeGraphQLRequest request = NativeGraphQLRequest( + document: '', + ); + + await nativeAmplifyApi.subscribe(request); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, null); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'start_ack'); + + return Future.value(message); + }, + ); + + expect(onEstablishedCallback, isNotNull); + expect(() => onEstablishedCallback?.call(), returnsNormally); + }); + + test('Send Data Event', () async { + String data = '{"data1":1,"data2":"data string"}'; + String payloadJson = '{"data":$data,"errors":[]}'; + StreamController? responseController; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + responseController = controller; + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.subscribe( + NativeGraphQLRequest(document: ''), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, payloadJson); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'data'); + + return Future.value(message); + }, + ); + + expect(responseController, isNotNull); + expect( + () => responseController?.add( + GraphQLResponse( + data: data, + errors: [], + ), + ), + returnsNormally, + ); + }); + + test('Send Data With Errors Event', () async { + String data = '{"data1":1,"data2":"data string"}'; + String errorMessage1 = 'errorsJson1'; + String errorMessage2 = 'errorsJson2'; + String payloadJson = + '{"data":$data,"errors":[{"message":"$errorMessage1"},{"message":"$errorMessage2"}]}'; + + StreamController? responseController; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + responseController = controller; + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.subscribe( + NativeGraphQLRequest(document: ''), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, payloadJson); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'error'); + + return Future.value(message); + }, + ); + + expect(responseController, isNotNull); + expect( + () => responseController?.add( + GraphQLResponse( + data: data, + errors: [ + GraphQLResponseError(message: errorMessage1), + GraphQLResponseError(message: errorMessage2), + ], + ), + ), + returnsNormally, + ); + }); + + test('Send Data Event Exception', () async { + String data = 'Invalid Json'; + String payloadJson = + '{"data":{},"errors":[{"message":"Error parsing payload json: FormatException: Unexpected character (at character 1)\\nInvalid Json\\n^\\n"}]}'; + StreamController? responseController; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + responseController = controller; + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.subscribe( + NativeGraphQLRequest(document: ''), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, payloadJson); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'error'); + + return Future.value(message); + }, + ); + + expect(responseController, isNotNull); + expect( + () => responseController?.add( + GraphQLResponse( + data: data, + errors: [], + ), + ), + returnsNormally, + ); + }); + + test('Send Error Event', () async { + String exceptionMessage = 'Intentional Error'; + String errorsJson = + '{"errors":[{"message":"Exception: $exceptionMessage"}]}'; + + StreamController? responseController; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + responseController = controller; + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.subscribe( + NativeGraphQLRequest(document: ''), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, errorsJson); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'error'); + + return Future.value(message); + }, + ); + + expect(responseController, isNotNull); + expect( + () => responseController?.addError(Exception(exceptionMessage)), + returnsNormally, + ); + }); + + test('Send Done Event', () async { + StreamController? responseController; + + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + responseController = controller; + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.subscribe( + NativeGraphQLRequest(document: ''), + ); + + binding.defaultBinaryMessenger.setMockDecodedMessageHandler( + channel, + (Object? message) { + expect(message, isA()); + List responses = message as List; + + expect(responses.length, greaterThan(0)); + expect(responses[0], isA()); + NativeGraphQLSubscriptionResponse response = responses[0]; + + expect(response.payloadJson, null); + expect(response.subscriptionId.length, greaterThan(0)); + expect(response.type, 'complete'); + + return Future.value(message); + }, + ); + + expect(responseController, isNotNull); + expect( + () => responseController?.close(), + returnsNormally, + ); + }); + + group('Unubscribe', () { + test('Existing Subscription', () async { + mockAPIPlugin.subscribeMethod = ( + GraphQLRequest mockRequest, + void Function()? onEstablished, + ) { + var controller = StreamController>(); + return controller.stream; + }; + + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + var response = await nativeAmplifyApi.subscribe( + NativeGraphQLRequest( + document: '', + ), + ); + + await nativeAmplifyApi.unsubscribe(response.subscriptionId); + }); + + test('Fake Subscription', () async { + NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); + + await nativeAmplifyApi.unsubscribe('Fake Subscription ID'); + }); + }); + }); + }); +} + +class MockAPIPlugin extends APIPluginInterface { + GraphQLOperation Function(GraphQLRequest)? queryMethod; + GraphQLOperation Function(GraphQLRequest)? mutateMethod; + Stream> Function(GraphQLRequest, void Function()?)? + subscribeMethod; + + MockAPIPlugin({ + this.queryMethod, + this.mutateMethod, + this.subscribeMethod, + }); + + void clear() { + queryMethod = null; + mutateMethod = null; + subscribeMethod = null; + } + + @override + GraphQLOperation query({required GraphQLRequest request}) { + return queryMethod!(request); + } + + @override + GraphQLOperation mutate({required GraphQLRequest request}) { + return mutateMethod!(request); + } + + @override + Stream> subscribe( + GraphQLRequest request, { + void Function()? onEstablished, + }) { + return subscribeMethod!(request, onEstablished); + } +} + +class MockNativeAuthBridgeCodec extends StandardMessageCodec { + @override + void writeValue(WriteBuffer buffer, Object? value) { + if (value is NativeGraphQLSubscriptionResponse) { + buffer.putUint8(128); + writeValue(buffer, value.encode()); + } else { + super.writeValue(buffer, value); + } + } + + @override + Object? readValueOfType(int type, ReadBuffer buffer) { + switch (type) { + case 128: + return NativeGraphQLSubscriptionResponse.decode(readValue(buffer)!); + default: + return super.readValueOfType(type, buffer); + } + } +} From c4879de099f7eb2e9b20047c9fa02b8422b66ab8 Mon Sep 17 00:00:00 2001 From: Tyler-Larkin Date: Fri, 7 Jun 2024 10:29:09 -0700 Subject: [PATCH 2/3] chore(datastore): updated Amplify Api Native Bridge unit tests per feedback --- .../test/native_amplify_api_test.dart | 90 +++++++++++-------- 1 file changed, 53 insertions(+), 37 deletions(-) diff --git a/packages/amplify_datastore/test/native_amplify_api_test.dart b/packages/amplify_datastore/test/native_amplify_api_test.dart index a1a8eb63d4..590b9109ec 100644 --- a/packages/amplify_datastore/test/native_amplify_api_test.dart +++ b/packages/amplify_datastore/test/native_amplify_api_test.dart @@ -40,7 +40,7 @@ void main() async { group('NativeAmplifyAPI', () { group('Query', () { - test('Empty Request/Response', () async { + test('Should handle empty request/response', () async { String document = ''; Map emptyMap = {}; String payloadJson = '{"data":{},"errors":[]}'; @@ -72,7 +72,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Filled Request/Response', () async { + test('Should handle filled request/response', () async { String document = 'document'; String apiName = 'apiName'; String variable1Key = 'variable1'; @@ -130,7 +130,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Empty Response Parse Exception', () async { + test('Should handle empty response parse exception', () async { String document = ''; String data = 'Invalid Json'; Map emptyMap = {}; @@ -190,7 +190,7 @@ void main() async { ); } - test('AuthModeTypes', () async { + test('Should handle all AuthModeTypes', () async { await _authModeExpectHelepr('apiKey', APIAuthorizationType.apiKey); await _authModeExpectHelepr('awsIAM', APIAuthorizationType.iam); await _authModeExpectHelepr('openIDConnect', APIAuthorizationType.oidc); @@ -201,7 +201,7 @@ void main() async { await _authModeExpectHelepr(null, null); }); - test('Non-AmplifyException Exception', () async { + test('Should handle non-AmplifyException exceptions', () async { String document = ''; String invalidJson = 'INVALID JSON'; String payloadJson = @@ -218,7 +218,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('AmplifyException Exception', () async { + test('Should handle AmplifyException', () async { String exceptionMessage = 'API Exception'; String payloadJson = '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; @@ -237,13 +237,16 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('AmplifyException', () async { + test('Should handle unauthorized AmplifyException - SignedOutException', () async { String exceptionMessage = 'API Exception'; String payloadJson = - '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { - throw NetworkException(exceptionMessage); + throw NetworkException( + exceptionMessage, + underlyingException: 'SignedOutException', + ); }; NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); @@ -256,15 +259,15 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Unauthorized AmplifyException', () async { - String exceptionMessage = 'API Exception'; + test('Should handle unauthorized AmplifyException - Unauthrorized', () async { + String exceptionMessage = 'Not Authorized to access onDeletePrivateNote on type Subscription'; String payloadJson = - '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"$exceptionMessage\\",\\n \\"underlyingException\\": \\"Unauthrorized\\"\\n}"}]}'; mockAPIPlugin.queryMethod = (GraphQLRequest mockRequest) { throw NetworkException( exceptionMessage, - underlyingException: 'SignedOutException', + underlyingException: 'Unauthrorized', ); }; @@ -280,7 +283,7 @@ void main() async { }); group('Mutate', () { - test('Empty Request/Response', () async { + test('Should handle empty request/response', () async { String document = ''; Map emptyMap = {}; String payloadJson = '{"data":{},"errors":[]}'; @@ -312,7 +315,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Filled Request/Response', () async { + test('Should handle filled request/response', () async { String document = 'document'; String apiName = 'apiName'; String authMode = 'apiKey'; @@ -371,7 +374,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Empty Response Parse Exception', () async { + test('Should handle empty response parse exception', () async { String document = ''; String data = 'Invalid Json'; Map emptyMap = {}; @@ -405,7 +408,7 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('AmplifyException Exception', () async { + test('Should handle AmplifyException', () async { String exceptionMessage = 'API Exception'; String payloadJson = '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; @@ -424,13 +427,16 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('AmplifyException', () async { + test('Should handle unauthorized AmplifyException - SignedOutException', () async { String exceptionMessage = 'API Exception'; String payloadJson = - '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"API Exception\\"\\n}"}]}'; + '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { - throw NetworkException(exceptionMessage); + throw NetworkException( + exceptionMessage, + underlyingException: 'SignedOutException', + ); }; NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); @@ -443,15 +449,15 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Unauthorized AmplifyException', () async { - String exceptionMessage = 'API Exception'; + test('Should handle unauthorized AmplifyException - Unauthrorized', () async { + String exceptionMessage = 'Not Authorized to access onDeletePrivateNote on type Subscription'; String payloadJson = - '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; + '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"$exceptionMessage\\",\\n \\"underlyingException\\": \\"Unauthrorized\\"\\n}"}]}'; mockAPIPlugin.mutateMethod = (GraphQLRequest mockRequest) { throw NetworkException( exceptionMessage, - underlyingException: 'SignedOutException', + underlyingException: 'Unauthrorized', ); }; @@ -467,7 +473,7 @@ void main() async { }); group('Subscribe', () { - test('Empty Request/Response', () async { + test('Should handle empty request/response', () async { String responseType = 'connecting'; String document = ''; Map emptyMap = {}; @@ -499,7 +505,7 @@ void main() async { expect(response.type, responseType); }); - test('Filled Request/Response', () async { + test('Should handle filled request/response', () async { String responseType = 'connecting'; String document = 'document'; String apiName = 'apiName'; @@ -550,7 +556,7 @@ void main() async { expect(response.type, responseType); }); - test('Invalid Request Variables Json Exception', () async { + test('Should handle invalid Request variablesJson exception', () async { String document = ''; String invalidJson = 'INVALID JSON'; @@ -565,7 +571,7 @@ void main() async { throwsA(TypeMatcher())); }); - test('API Exception', () async { + test('Should handle API exception', () async { String exceptionMessage = 'API Exception'; mockAPIPlugin.subscribeMethod = ( @@ -590,7 +596,7 @@ void main() async { ); }); - test('Established/Connected Callback', () async { + test('Should handle established/connected callback', () async { void Function()? onEstablishedCallback; mockAPIPlugin.subscribeMethod = ( @@ -632,7 +638,7 @@ void main() async { expect(() => onEstablishedCallback?.call(), returnsNormally); }); - test('Send Data Event', () async { + test('Should handle send data event', () async { String data = '{"data1":1,"data2":"data string"}'; String payloadJson = '{"data":$data,"errors":[]}'; StreamController? responseController; @@ -682,7 +688,7 @@ void main() async { ); }); - test('Send Data With Errors Event', () async { + test('Should handle send data with errors event', () async { String data = '{"data1":1,"data2":"data string"}'; String errorMessage1 = 'errorsJson1'; String errorMessage2 = 'errorsJson2'; @@ -739,7 +745,7 @@ void main() async { ); }); - test('Send Data Event Exception', () async { + test('Should handle send data event exception', () async { String data = 'Invalid Json'; String payloadJson = '{"data":{},"errors":[{"message":"Error parsing payload json: FormatException: Unexpected character (at character 1)\\nInvalid Json\\n^\\n"}]}'; @@ -790,7 +796,7 @@ void main() async { ); }); - test('Send Error Event', () async { + test('Should handle send error event', () async { String exceptionMessage = 'Intentional Error'; String errorsJson = '{"errors":[{"message":"Exception: $exceptionMessage"}]}'; @@ -837,7 +843,7 @@ void main() async { ); }); - test('Send Done Event', () async { + test('Should handle send done event', () async { StreamController? responseController; mockAPIPlugin.subscribeMethod = ( @@ -881,7 +887,7 @@ void main() async { }); group('Unubscribe', () { - test('Existing Subscription', () async { + test('Should handle existing subscription', () async { mockAPIPlugin.subscribeMethod = ( GraphQLRequest mockRequest, void Function()? onEstablished, @@ -899,12 +905,22 @@ void main() async { ); await nativeAmplifyApi.unsubscribe(response.subscriptionId); + + expect( + () async => + await nativeAmplifyApi.unsubscribe(response.subscriptionId), + returnsNormally, + ); }); - test('Fake Subscription', () async { + test('Should handle non-existing subscriptions', () async { NativeAmplifyApi nativeAmplifyApi = NativeAmplifyApi({}); - await nativeAmplifyApi.unsubscribe('Fake Subscription ID'); + expect( + () async => + await nativeAmplifyApi.unsubscribe('Fake Subscription ID'), + returnsNormally, + ); }); }); }); From 49e184081178be9f855c9b5ef06740fefd4801c6 Mon Sep 17 00:00:00 2001 From: Tyler-Larkin Date: Fri, 7 Jun 2024 10:40:30 -0700 Subject: [PATCH 3/3] chore(datastore): Formatted file --- .../test/native_amplify_api_test.dart | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/packages/amplify_datastore/test/native_amplify_api_test.dart b/packages/amplify_datastore/test/native_amplify_api_test.dart index 590b9109ec..bb90e31a22 100644 --- a/packages/amplify_datastore/test/native_amplify_api_test.dart +++ b/packages/amplify_datastore/test/native_amplify_api_test.dart @@ -237,7 +237,8 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Should handle unauthorized AmplifyException - SignedOutException', () async { + test('Should handle unauthorized AmplifyException - SignedOutException', + () async { String exceptionMessage = 'API Exception'; String payloadJson = '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; @@ -259,8 +260,10 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Should handle unauthorized AmplifyException - Unauthrorized', () async { - String exceptionMessage = 'Not Authorized to access onDeletePrivateNote on type Subscription'; + test('Should handle unauthorized AmplifyException - Unauthrorized', + () async { + String exceptionMessage = + 'Not Authorized to access onDeletePrivateNote on type Subscription'; String payloadJson = '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"$exceptionMessage\\",\\n \\"underlyingException\\": \\"Unauthrorized\\"\\n}"}]}'; @@ -427,7 +430,8 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Should handle unauthorized AmplifyException - SignedOutException', () async { + test('Should handle unauthorized AmplifyException - SignedOutException', + () async { String exceptionMessage = 'API Exception'; String payloadJson = '{"errors":[{"message":"Unauthorized - API Exception - SignedOutException"}]}'; @@ -449,8 +453,10 @@ void main() async { expect(response.payloadJson, payloadJson); }); - test('Should handle unauthorized AmplifyException - Unauthrorized', () async { - String exceptionMessage = 'Not Authorized to access onDeletePrivateNote on type Subscription'; + test('Should handle unauthorized AmplifyException - Unauthrorized', + () async { + String exceptionMessage = + 'Not Authorized to access onDeletePrivateNote on type Subscription'; String payloadJson = '{"errors":[{"message":"NetworkException {\\n \\"message\\": \\"$exceptionMessage\\",\\n \\"underlyingException\\": \\"Unauthrorized\\"\\n}"}]}';