// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: Apache-2.0 import 'dart:async'; import 'dart:convert'; import 'package:amplify_api/amplify_api.dart'; import 'package:amplify_api_example/amplifyconfiguration.dart'; import 'package:amplify_api_example/models/ModelProvider.dart'; import 'package:amplify_auth_cognito/amplify_auth_cognito.dart'; import 'package:amplify_flutter/amplify_flutter.dart'; import 'package:flutter_test/flutter_test.dart'; const _subscriptionTimeoutInterval = 5; TestUser? testUser; // Keep track of what is created here so it can be deleted. final blogCache = []; final postCache = []; final ownerOnlyCache = []; final cpkParentCache = []; final cpkExplicitChildCache = []; final cpkImplicitChildCache = []; class TestUser { TestUser({ String? username, String? password, }) : _username = 'testUser${uuid()}', _password = uuid(secure: true); final String _username; final String _password; Future signUp() async { await signOut(); final testEmail = '$_username@amazon.com'; final result = await Amplify.Auth.signUp( username: _username, password: _password, options: SignUpOptions( userAttributes: {AuthUserAttributeKey.email: testEmail}, ), ); if (!result.isSignUpComplete) { throw Exception('Unable to sign up test user.'); } } Future signOut() async { final session = await Amplify.Auth.fetchAuthSession(); if (!session.isSignedIn) return; await Amplify.Auth.signOut(); } /// No-op if already signed in. Future signIn() async { final session = await Amplify.Auth.fetchAuthSession(); if (session.isSignedIn) return; final result = await Amplify.Auth.signIn( username: _username, password: _password, ); if (!result.isSignedIn) { throw Exception('Unable to sign in test user.'); } } Future delete() async { final session = await Amplify.Auth.fetchAuthSession(); if (!session.isSignedIn) await signInTestUser(); await Amplify.Auth.deleteUser(); testUser = null; } } Future configureAmplify() async { if (!Amplify.isConfigured) { await Amplify.addPlugins([ AmplifyAuthCognito( secureStorageFactory: AmplifySecureStorage.factoryFrom( macOSOptions: MacOSSecureStorageOptions(useDataProtection: false), ), ), AmplifyAPI(modelProvider: ModelProvider.instance), ]); await Amplify.configure(amplifyconfig); } } Future signUpTestUser() async { await signOutTestUser(); testUser = TestUser(); await testUser!.signUp(); } /// No-op if already signed in. Future signInTestUser() async { if (testUser == null) { throw const InvalidStateException( 'No test user to sign in.', recoverySuggestion: 'Ensure test user signed up.', ); } await testUser!.signIn(); } // No-op if not signed in. Future signOutTestUser() async { await testUser?.signOut(); } Future deleteTestUser() async { if (testUser == null) { throw const InvalidStateException( 'No test user to delete.', recoverySuggestion: 'Ensure test user signed up.', ); } await testUser!.delete(); } // declare utility which creates blog with title as parameter Future addBlog(String name) async { final request = ModelMutations.create( Blog(name: name), authorizationMode: APIAuthorizationType.userPools, ); final response = await Amplify.API.mutate(request: request).response; expect(response, hasNoGraphQLErrors); final blog = response.data!; blogCache.add(blog); return blog; } // declare utility which creates post with title and blog as parameter Future addPost(String name, int rating, Blog blog) async { final request = ModelMutations.create( Post( title: name, blog: blog, rating: rating, ), authorizationMode: APIAuthorizationType.userPools, ); final response = await Amplify.API.mutate(request: request).response; expect(response, hasNoGraphQLErrors); final post = response.data!; postCache.add(post); return post; } Future addCpkParent(String name) async { final request = ModelMutations.create( CpkOneToOneBidirectionalParentCD(customId: uuid(), name: name), ); final response = await Amplify.API.mutate(request: request).response; expect(response, hasNoGraphQLErrors); final cpkParent = response.data!; cpkParentCache.add(cpkParent); return cpkParent; } // declare utility which creates OwnerOnly model with name as parameter Future addOwnerOnly(String name) async { final request = ModelMutations.create( OwnerOnly(name: name), authorizationMode: APIAuthorizationType.userPools, ); final response = await Amplify.API.mutate(request: request).response; expect(response, hasNoGraphQLErrors); final ownerOnly = response.data!; ownerOnlyCache.add(ownerOnly); return ownerOnly; } /// Run a mutation on [Blog] with a partial selection set. /// /// This is used to trigger an error on subscriptions listening for the /// full selection set. Future> runPartialMutation(String name) async { const graphQLDocument = r'''mutation MyMutation($name: String!) { createBlog(input: {name: $name}) { id name } }'''; final request = GraphQLRequest( document: graphQLDocument, variables: {'name': name}, authorizationMode: APIAuthorizationType.userPools, ); final response = await Amplify.API.mutate(request: request).response; expect(response, hasNoGraphQLErrors); // Add to cache so it can be cleaned up with other test artifacts. final responseJson = json.decode(response.data!) as Map; final blogFromResponse = Blog.fromJson(responseJson['createBlog'] as Map); blogCache.add(blogFromResponse); return response; } Future addPostAndBlog( String title, int rating, ) async { const name = 'Integration Test Blog with a post - create'; final createdBlog = await addBlog(name); return addPost(title, rating, createdBlog); } Future deleteBlog(Blog blog) async { final request = ModelMutations.deleteById( Blog.classType, blog.modelIdentifier, authorizationMode: APIAuthorizationType.userPools, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); blogCache.removeWhere((blogFromCache) => blogFromCache.id == blog.id); return res.data; } Future deletePost(Post post) async { final request = ModelMutations.deleteById( Post.classType, post.modelIdentifier, authorizationMode: APIAuthorizationType.userPools, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); postCache.removeWhere((postFromCache) => postFromCache.id == post.id); return res.data; } Future deleteCpkParent( CpkOneToOneBidirectionalParentCD cpkParent, ) async { final request = ModelMutations.deleteById( CpkOneToOneBidirectionalParentCD.classType, cpkParent.modelIdentifier, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); cpkParentCache.removeWhere( (cpkParentFromCache) => cpkParentFromCache.customId == cpkParent.customId, ); return res.data; } Future deleteCpkExplicitChild( CpkOneToOneBidirectionalChildExplicitCD cpkExplicitChild, ) async { final request = ModelMutations.deleteById( CpkOneToOneBidirectionalChildExplicitCD.classType, cpkExplicitChild.modelIdentifier, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); cpkExplicitChildCache.removeWhere( (childFromCache) => childFromCache.id == cpkExplicitChild.id, ); return res.data; } Future deleteCpkImplicitChild( CpkOneToOneBidirectionalChildImplicitCD cpkImplicitChild, ) async { final request = ModelMutations.deleteById( CpkOneToOneBidirectionalChildImplicitCD.classType, cpkImplicitChild.modelIdentifier, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); cpkImplicitChildCache.removeWhere( (childFromCache) => childFromCache.id == cpkImplicitChild.id, ); return res.data; } Future deleteOwnerOnly(OwnerOnly model) async { final request = ModelMutations.deleteById( OwnerOnly.classType, model.modelIdentifier, authorizationMode: APIAuthorizationType.userPools, ); final res = await Amplify.API.mutate(request: request).response; expect(res, hasNoGraphQLErrors); ownerOnlyCache.removeWhere((modelFromCache) => modelFromCache.id == model.id); return res.data; } Future deleteTestModels() async { await Future.wait(blogCache.map(deleteBlog)); await Future.wait(postCache.map(deletePost)); await Future.wait(cpkExplicitChildCache.map(deleteCpkExplicitChild)); await Future.wait(cpkImplicitChildCache.map(deleteCpkImplicitChild)); await Future.wait(ownerOnlyCache.map(deleteOwnerOnly)); } /// Wait for subscription established for given request. Future>> getEstablishedSubscriptionOperation( GraphQLRequest subscriptionRequest, void Function(GraphQLResponse) onData, ) async { final establishedCompleter = Completer(); final stream = Amplify.API.subscribe( subscriptionRequest, onEstablished: establishedCompleter.complete, ); final subscription = stream.listen( onData, onError: (Object e) => fail('Error in subscription stream: $e'), ); await establishedCompleter.future .timeout(const Duration(seconds: _subscriptionTimeoutInterval)); return subscription; } /// Establish subscription for request, do the mutationFunction, then wait /// for the stream event, cancel the operation, return response from event. /// /// `eventFilter` is used to ensure completer only called for specific events /// as the subscription may get events from other client mutations (and is likely /// in CI). Future> establishSubscriptionAndMutate( GraphQLRequest subscriptionRequest, Future Function() mutationFunction, { required bool Function(GraphQLResponse) eventFilter, bool canFail = false, }) async { final dataCompleter = Completer>(); // With stream established, exec callback with stream events. final subscription = await getEstablishedSubscriptionOperation( subscriptionRequest, (event) { if (!canFail && event.hasErrors) { fail('subscription errors: ${event.errors}'); } if (!dataCompleter.isCompleted && (eventFilter(event))) { dataCompleter.complete(event); } }, ); await mutationFunction(); final response = await dataCompleter.future.timeout( const Duration(seconds: _subscriptionTimeoutInterval), ); await subscription.cancel(); return response; } final hasNoGraphQLErrors = predicate>( (GraphQLResponse response) => !response.hasErrors && response.data != null, 'Has no GraphQL Errors', ); /// Hub Event Matchers final connectedHubEvent = isA().having( (event) => event.status, 'status', SubscriptionStatus.connected, ); final connectingHubEvent = isA().having( (event) => event.status, 'status', SubscriptionStatus.connecting, ); final disconnectedHubEvent = isA().having( (event) => event.status, 'status', SubscriptionStatus.disconnected, ); final pendingDisconnectedHubEvent = isA().having( (event) => event.status, 'status', SubscriptionStatus.pendingDisconnected, ); final failedHubEvent = isA().having( (event) => event.status, 'status', SubscriptionStatus.failed, );