From 07207076dd77a5ce7af0799579aa56ed4a2b0440 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Mon, 4 Mar 2024 16:24:31 -0500 Subject: [PATCH 01/23] WIP --- .../xcschemes/AWSPluginsSDKCore.xcscheme | 66 +++++++++++++++++++ .../xcschemes/Amplify-Package.xcscheme | 23 +++++++ .../AWSAuthService.swift | 0 .../AWSAuthServiceBehavior.swift | 0 .../AWSAuthSessionBehavior.swift | 0 .../AWSPluginExtension.swift | 0 .../AmplifyAWSCredentialsProvider.swift | 0 .../AmplifyAWSServiceConfiguration.swift | 0 .../AmplifyAWSSignatureV4Signer.swift | 0 .../AuthAWSCredentialsProvider.swift | 0 .../AuthCognitoIdentityProvider.swift | 0 .../AuthCognitoTokensProvider.swift | 0 .../AuthTokenProvider.swift | 0 .../PluginClientEngine.swift | 0 .../SdkHttpRequest+updatingUserAgent.swift | 0 .../UserAgentSettingClientEngine.swift | 0 .../UserAgentSuffixAppender.swift | 0 .../IAMCredentialProvider.swift | 0 .../Resources/PrivacyInfo.xcprivacy | 8 +++ ...lifyAWSServiceConfiguration+Platform.swift | 0 Package.swift | 26 +++++++- 21 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme rename AmplifyPlugins/Core/{AWSPluginsCore/Auth => AWSPluginsSDKCore}/AWSAuthService.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth => AWSPluginsSDKCore}/AWSAuthServiceBehavior.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth => AWSPluginsSDKCore}/AWSAuthSessionBehavior.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Utils => AWSPluginsSDKCore}/AWSPluginExtension.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AmplifyAWSCredentialsProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/ServiceConfiguration => AWSPluginsSDKCore}/AmplifyAWSServiceConfiguration.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AmplifyAWSSignatureV4Signer.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AuthAWSCredentialsProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AuthCognitoIdentityProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AuthCognitoTokensProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/AuthTokenProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Utils => AWSPluginsSDKCore}/CustomHttpClientEngine/PluginClientEngine.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Utils => AWSPluginsSDKCore}/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Utils => AWSPluginsSDKCore}/CustomHttpClientEngine/UserAgentSettingClientEngine.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Utils => AWSPluginsSDKCore}/CustomHttpClientEngine/UserAgentSuffixAppender.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsCore/Auth/Provider => AWSPluginsSDKCore}/IAMCredentialProvider.swift (100%) create mode 100644 AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy rename AmplifyPlugins/Core/{AWSPluginsCore => AWSPluginsSDKCore}/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift (100%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme new file mode 100644 index 0000000000..595e4a2023 --- /dev/null +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme index 6970be959c..7b90cae3b1 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme @@ -482,6 +482,20 @@ ReferencedContainer = "container:"> + + + + + + + + diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthServiceBehavior.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthServiceBehavior.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthSessionBehavior.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthSessionBehavior.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthSessionBehavior.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthSessionBehavior.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/AWSPluginExtension.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSPluginExtension.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/AWSPluginExtension.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AWSPluginExtension.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSServiceConfiguration.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSServiceConfiguration.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSSignatureV4Signer.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSSignatureV4Signer.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSSignatureV4Signer.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSSignatureV4Signer.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthAWSCredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AuthAWSCredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoIdentityProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoIdentityProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoIdentityProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoIdentityProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoTokensProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoTokensProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoTokensProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoTokensProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthTokenProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthTokenProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/PluginClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/PluginClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSettingClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSettingClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSettingClientEngine.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSettingClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSuffixAppender.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSuffixAppender.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSuffixAppender.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSuffixAppender.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/IAMCredentialProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/IAMCredentialProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy b/AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..74f8af8564 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyAccessedAPITypes + + + diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift diff --git a/Package.swift b/Package.swift index 43065db332..1f3d46a58f 100644 --- a/Package.swift +++ b/Package.swift @@ -30,11 +30,24 @@ let amplifyTargets: [Target] = [ ), .target( name: "AWSPluginsCore", + dependencies: [ + "Amplify" + ], + path: "AmplifyPlugins/Core/AWSPluginsCore", + exclude: [ + "Info.plist" + ], + resources: [ + .copy("Resources/PrivacyInfo.xcprivacy") + ] + ), + .target( + name: "AWSPluginsSDKCore", dependencies: [ "Amplify", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], - path: "AmplifyPlugins/Core/AWSPluginsCore", + path: "AmplifyPlugins/Core/AWSPluginsSDKCore", exclude: [ "Info.plist" ], @@ -117,6 +130,7 @@ let apiTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "AppSyncRealTimeClient", package: "aws-appsync-realtime-client-ios")], path: "AmplifyPlugins/API/Sources/AWSAPIPlugin", exclude: [ @@ -162,6 +176,7 @@ let authTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AmplifySRP"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "AWSClientRuntime", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentityProvider", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentity", package: "aws-sdk-swift") @@ -235,6 +250,7 @@ let storageTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "AWSS3", package: "aws-sdk-swift")], path: "AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin", exclude: [ @@ -265,6 +281,7 @@ let geoTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "AWSLocation", package: "aws-sdk-swift")], path: "AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin", exclude: [ @@ -296,6 +313,7 @@ let internalPinpointTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AWSCognitoAuthPlugin"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "SQLite", package: "SQLite.swift"), .product(name: "AWSPinpoint", package: "aws-sdk-swift"), .product(name: "AmplifyUtilsNotifications", package: "amplify-swift-utils-notifications") @@ -364,6 +382,7 @@ let predictionsTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .target(name: "CoreMLPredictionsPlugin"), .product(name: "AWSComprehend", package: "aws-sdk-swift"), .product(name: "AWSPolly", package: "aws-sdk-swift"), @@ -414,6 +433,7 @@ let loggingTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "AWSPluginsSDKCore"), .product(name: "AWSCloudWatchLogs", package: "aws-sdk-swift"), ], path: "AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin", @@ -459,6 +479,10 @@ let package = Package( name: "AWSPluginsCore", targets: ["AWSPluginsCore"] ), + .library( + name: "AWSPluginsSDKCore", + targets: ["AWSPluginsSDKCore"] + ), .library( name: "AWSAPIPlugin", targets: ["AWSAPIPlugin"] From 1d47dcffe457de841158b7a25380ad16b97d0f22 Mon Sep 17 00:00:00 2001 From: Michael Law <1365977+lawmicha@users.noreply.github.com> Date: Wed, 6 Mar 2024 11:24:57 -0800 Subject: [PATCH 02/23] DataStore compiles without SDK dependency --- .../Auth}/AWSAuthService.swift | 5 ----- .../Auth}/AWSAuthServiceBehavior.swift | 3 --- .../Auth}/AWSAuthSessionBehavior.swift | 0 .../Auth}/AuthAWSCredentialsProvider.swift | 0 .../Auth}/AuthCognitoIdentityProvider.swift | 0 .../Auth}/AuthCognitoTokensProvider.swift | 0 .../AWSAuthCredentialsProvider.swift | 16 ++++++++++++++++ .../AWSAuthCredentialsProviderBehavior.swift | 14 ++++++++++++++ 8 files changed, 30 insertions(+), 8 deletions(-) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AWSAuthService.swift (96%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AWSAuthServiceBehavior.swift (86%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AWSAuthSessionBehavior.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AuthAWSCredentialsProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AuthCognitoIdentityProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AWSPluginsCore/Auth}/AuthCognitoTokensProvider.swift (100%) create mode 100644 AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift similarity index 96% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift index d0c279314c..b15b4b7d6c 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift @@ -7,16 +7,11 @@ import Foundation import Amplify -import AWSClientRuntime public class AWSAuthService: AWSAuthServiceBehavior { public init() {} - public func getCredentialsProvider() -> CredentialsProviding { - return AmplifyAWSCredentialsProvider() - } - /// Retrieves the identity identifier for this authentication session from Cognito. public func getIdentityID() async throws -> String { let session = try await Amplify.Auth.fetchAuthSession() diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthServiceBehavior.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift similarity index 86% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthServiceBehavior.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift index 6c302cc928..1c23190588 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthServiceBehavior.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift @@ -7,12 +7,9 @@ import Foundation import Amplify -import AWSClientRuntime public protocol AWSAuthServiceBehavior: AnyObject { - func getCredentialsProvider() -> CredentialsProviding - func getTokenClaims(tokenString: String) -> Result<[String: AnyObject], AuthError> /// Retrieves the identity identifier of for the Auth service diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthSessionBehavior.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthSessionBehavior.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthSessionBehavior.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthSessionBehavior.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthAWSCredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AuthAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthAWSCredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoIdentityProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoIdentityProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoIdentityProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoIdentityProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoTokensProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoTokensProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AuthCognitoTokensProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoTokensProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift new file mode 100644 index 0000000000..8c651aefb7 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift @@ -0,0 +1,16 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import AWSClientRuntime + +public class AWSAuthCredentialsProvider: AWSAuthCredentialsProviderBehavior { + public func getCredentialsProvider() -> CredentialsProviding { + return AmplifyAWSCredentialsProvider() + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift new file mode 100644 index 0000000000..80f1b3c4ee --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift @@ -0,0 +1,14 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import AWSClientRuntime + +public protocol AWSAuthCredentialsProviderBehavior { + func getCredentialsProvider() -> CredentialsProviding +} From 6e29d884d508804f195ef08e47dfa64f1a779a44 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 4 Apr 2024 11:53:05 -0700 Subject: [PATCH 03/23] refactor(datastore-v2): use api plugin with async sequences --- .../Operation/RetryableGraphQLOperation.swift | 266 ++++++++---------- .../AmplifyAsyncThrowingSequence.swift | 2 + .../AmplifyTask+OperationTaskAdapters.swift | 4 +- .../Sources/AWSAPIPlugin/AWSAPIPlugin.swift | 2 +- .../AWSGraphQLSubscriptionTaskRunner.swift | 4 +- .../APICategoryGraphQLBehaviorExtended.swift | 41 --- .../Auth/AWSAuthModeStrategy.swift | 30 ++ .../StorageEngine+SyncRequirement.swift | 2 +- .../InitialSync/InitialSyncOperation.swift | 73 ++--- .../InitialSync/InitialSyncOrchestrator.swift | 6 +- .../OutgoingMutationQueue+Action.swift | 2 +- .../OutgoingMutationQueue+State.swift | 2 +- .../OutgoingMutationQueue.swift | 12 +- ...ocessMutationErrorFromCloudOperation.swift | 62 ++-- .../SyncMutationToCloudOperation.swift | 85 +++--- .../Sync/RemoteSyncEngine+Action.swift | 4 +- .../Sync/RemoteSyncEngine+State.swift | 4 +- .../Sync/RemoteSyncEngine.swift | 8 +- .../Sync/RemoteSyncEngineBehavior.swift | 2 +- .../AWSIncomingEventReconciliationQueue.swift | 4 +- ...WSIncomingSubscriptionEventPublisher.swift | 2 +- ...omingAsyncSubscriptionEventPublisher.swift | 174 ++++++------ .../AWSModelReconciliationQueue.swift | 4 +- .../Sync/Support/AsyncStream+Extensions.swift | 21 ++ .../Mocks/MockOutgoingMutationQueue.swift | 2 +- .../Mocks/MockRemoteSyncEngine.swift | 2 +- .../TestSupport/Mocks/NoOpMutationQueue.swift | 2 +- .../DataStoreConnectionScenario1Tests.swift | 2 +- .../DataStoreHubEventsTests.swift | 1 - ...reLargeNumberModelsSubscriptionTests.swift | 2 +- .../HubEventsIntegrationTestBase.swift | 4 +- .../Mocks/MockAPICategoryPlugin.swift | 2 +- 32 files changed, 423 insertions(+), 410 deletions(-) delete mode 100644 AmplifyPlugins/Core/AWSPluginsCore/API/APICategoryGraphQLBehaviorExtended.swift create mode 100644 AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift diff --git a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift index ed2a6e2753..86281585b0 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -6,183 +6,159 @@ // import Foundation +import Combine -/// Convenience protocol to handle any kind of GraphQLOperation -public protocol AnyGraphQLOperation { - associatedtype Success - associatedtype Failure: Error - typealias ResultListener = (Result) -> Void -} -/// Abastraction for a retryable GraphQLOperation. -public protocol RetryableGraphQLOperationBehavior: Operation, DefaultLogger { - associatedtype Payload: Decodable +// MARK: - RetryableGraphQLOperation +public final class RetryableGraphQLOperation { + public typealias Payload = Payload - /// GraphQLOperation concrete type - associatedtype OperationType: AnyGraphQLOperation + public let requestFactory: AsyncStream<() -> GraphQLRequest> + public weak var api: APICategoryGraphQLBehavior? + private var task: Task? - typealias RequestFactory = () async -> GraphQLRequest - typealias OperationFactory = (GraphQLRequest, @escaping OperationResultListener) -> OperationType - typealias OperationResultListener = OperationType.ResultListener + public init( + requestFactory: T, + api: APICategoryGraphQLBehavior + ) where T.Element == () -> GraphQLRequest { + self.requestFactory = requestFactory.asyncStream + self.api = api + } - /// Operation unique identifier - var id: UUID { get } + deinit { + cancel() + } - /// Number of attempts (min 1) - var attempts: Int { get set } + public func execute( + _ operationType: GraphQLOperationType + ) -> Future.Success, APIError> { + Future() { promise in + self.task = Task { promise(await self.run(operationType)) } + } + } - /// Underlying GraphQL operation instantiated by `operationFactory` - var underlyingOperation: AtomicValue { get set } + public func run(_ operationType: GraphQLOperationType) async -> Result.Success, APIError> { + for await request in requestFactory { + do { + try Task.checkCancellation() + switch (self.api, operationType) { + case (.some(let api), .query): + return .success(try await api.query(request: request())) + case (.some(let api), .mutation): + return .success(try await api.mutate(request: request())) + default: + return .failure(.operationError("Unable to run GraphQL operation with type \(operationType)", "")) + } - /// Maximum number of allowed retries - var maxRetries: Int { get } + } catch is CancellationError { + return .failure(.operationError("GraphQL operation cancelled", "")) + } catch { + guard let error = error as? APIError, + let authError = error.underlyingError as? AuthError + else { + return .failure(.operationError("Failed to send \(operationType) GraphQL request", "", error)) + } - /// GraphQLRequest factory, invoked to create a new operation - var requestFactory: RequestFactory { get } + switch authError { + case .signedOut, .notAuthorized: break; + default: return .failure(error) + } + } + } + return .failure(APIError.operationError("Failed to execute GraphQL operation \(operationType)", "", nil)) + } - /// GraphQL operation factory, invoked with a newly created GraphQL request - /// and a wrapped result listener. - var operationFactory: OperationFactory { get } + public func cancel() { + task?.cancel() + } - var resultListener: OperationResultListener { get } +} - init(requestFactory: @escaping RequestFactory, - maxRetries: Int, - resultListener: @escaping OperationResultListener, - _ operationFactory: @escaping OperationFactory) +public final class RetryableGraphQLSubscriptionOperation { - func start(request: GraphQLRequest) + public typealias Payload = Payload - func shouldRetry(error: APIError?) -> Bool -} + public let requestFactory: AsyncStream<() async -> GraphQLRequest> + public weak var api: APICategoryGraphQLBehavior? + private var task: Task? -extension RetryableGraphQLOperationBehavior { - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.api.displayName, forNamespace: String(describing: self)) + public init( + requestFactory: T, + api: APICategoryGraphQLBehavior + ) where T.Element == () async -> GraphQLRequest { + self.requestFactory = requestFactory.asyncStream + self.api = api } - public var log: Logger { - Self.log + + deinit { + cancel() } -} -// MARK: RetryableGraphQLOperationBehavior + default implementation -extension RetryableGraphQLOperationBehavior { - public func start(request: GraphQLRequest) { - attempts += 1 - log.debug("[\(id)] - Try [\(attempts)/\(maxRetries)]") - let wrappedResultListener: OperationResultListener = { result in - if case let .failure(error) = result, self.shouldRetry(error: error as? APIError) { - self.log.debug("\(error)") - Task { - self.start(request: await self.requestFactory()) - } - return - } + public func subscribe() -> AnyPublisher, APIError> { + let subject = PassthroughSubject, APIError>() + self.task = Task { await self.trySubscribe(subject) } + return subject.eraseToAnyPublisher() + } - if case let .failure(error) = result { - self.log.debug("\(error)") - self.log.debug("[\(self.id)] - Failed") + private func trySubscribe(_ subject: PassthroughSubject, APIError>) async { + var apiError: APIError? + for await request in requestFactory { + guard let sequence = self.api?.subscribe(request: await request()) else { + continue } + do { + try Task.checkCancellation() - if case .success = result { - self.log.debug("[Operation \(self.id)] - Success") + for try await event in sequence { + try Task.checkCancellation() + Self.log.debug("Subscribe event \(event)") + subject.send(event) + } + } catch is CancellationError { + subject.send(completion: .finished) + } catch { + if let error = error as? APIError { + apiError = error + } + Self.log.debug("Failed with subscription request: \(error)") } - self.resultListener(result) + sequence.cancel() } - underlyingOperation.set(operationFactory(request, wrappedResultListener)) - } -} - -// MARK: - RetryableGraphQLOperation -public final class RetryableGraphQLOperation: Operation, RetryableGraphQLOperationBehavior { - public typealias Payload = Payload - public typealias OperationType = GraphQLOperation - - public var id: UUID - public var maxRetries: Int - public var attempts: Int = 0 - public var requestFactory: RequestFactory - public var underlyingOperation: AtomicValue?> = AtomicValue(initialValue: nil) - public var resultListener: OperationResultListener - public var operationFactory: OperationFactory - - public init(requestFactory: @escaping RequestFactory, - maxRetries: Int, - resultListener: @escaping OperationResultListener, - _ operationFactory: @escaping OperationFactory) { - self.id = UUID() - self.maxRetries = max(1, maxRetries) - self.requestFactory = requestFactory - self.operationFactory = operationFactory - self.resultListener = resultListener - } - - public override func main() { - Task { - start(request: await requestFactory()) + if apiError != nil { + subject.send(completion: .failure(apiError!)) + } else { + subject.send(completion: .finished) } } - override public func cancel() { - self.underlyingOperation.get()?.cancel() - } - - public func shouldRetry(error: APIError?) -> Bool { - guard case let .operationError(_, _, underlyingError) = error, - let authError = underlyingError as? AuthError else { - return false - } - - switch authError { - case .signedOut, .notAuthorized: - return attempts < maxRetries - default: - return false - } + public func cancel() { + self.task?.cancel() } } -// MARK: - RetryableGraphQLSubscriptionOperation -public final class RetryableGraphQLSubscriptionOperation: Operation, - RetryableGraphQLOperationBehavior { - public typealias OperationType = GraphQLSubscriptionOperation - - public typealias Payload = Payload - - public var id: UUID - public var maxRetries: Int - public var attempts: Int = 0 - public var underlyingOperation: AtomicValue?> = AtomicValue(initialValue: nil) - public var requestFactory: RequestFactory - public var resultListener: OperationResultListener - public var operationFactory: OperationFactory - - public init(requestFactory: @escaping RequestFactory, - maxRetries: Int, - resultListener: @escaping OperationResultListener, - _ operationFactory: @escaping OperationFactory) { - self.id = UUID() - self.maxRetries = max(1, maxRetries) - self.requestFactory = requestFactory - self.operationFactory = operationFactory - self.resultListener = resultListener - } - public override func main() { - Task { - start(request: await requestFactory()) +extension AsyncSequence { + fileprivate var asyncStream: AsyncStream { + AsyncStream { continuation in + Task { + var it = self.makeAsyncIterator() + do { + while let ele = try await it.next() { + continuation.yield(ele) + } + continuation.finish() + } catch { + continuation.finish() + } + } } } +} - public override func cancel() { - self.underlyingOperation.get()?.cancel() +extension RetryableGraphQLSubscriptionOperation { + public static var log: Logger { + Amplify.Logging.logger(forCategory: CategoryType.api.displayName, forNamespace: String(describing: self)) } - - public func shouldRetry(error: APIError?) -> Bool { - return attempts < maxRetries + public var log: Logger { + Self.log } - } - -// MARK: GraphQLOperation - GraphQLSubscriptionOperation + AnyGraphQLOperation -extension GraphQLOperation: AnyGraphQLOperation {} -extension GraphQLSubscriptionOperation: AnyGraphQLOperation {} diff --git a/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift index 38772392da..6a4841f13b 100644 --- a/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift +++ b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine public typealias WeakAmplifyAsyncThrowingSequenceRef = WeakRef> @@ -49,4 +50,5 @@ public class AmplifyAsyncThrowingSequence: AsyncSequence, Can parent?.cancel() finish() } + } diff --git a/Amplify/Core/Support/AmplifyTask+OperationTaskAdapters.swift b/Amplify/Core/Support/AmplifyTask+OperationTaskAdapters.swift index fb56c6df18..50e505bce9 100644 --- a/Amplify/Core/Support/AmplifyTask+OperationTaskAdapters.swift +++ b/Amplify/Core/Support/AmplifyTask+OperationTaskAdapters.swift @@ -20,7 +20,9 @@ public class AmplifyOperationTaskAdapter) { self.operation = operation self.childTask = ChildTask(parent: operation) - resultToken = operation.subscribe(resultListener: resultListener) + resultToken = operation.subscribe { [weak self] in + self?.resultListener($0) + } } deinit { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift index ce124f1f54..9d3117dd17 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift @@ -9,7 +9,7 @@ import Amplify import AWSPluginsCore import Foundation -final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQLBehaviorExtended, AWSAPIAuthInformation { +final public class AWSAPIPlugin: NSObject, APICategoryPlugin, AWSAPIAuthInformation { /// The unique key of the plugin within the API category. public var key: PluginKey { return "awsAPIPlugin" diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 3e70654298..c58bc2da90 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -106,8 +106,8 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, self.subscription = try await appSyncClient?.subscribe( id: subscriptionId, query: encodeRequest(query: request.document, variables: request.variables) - ).sink(receiveValue: { [weak self] event in - self?.onAsyncSubscriptionEvent(event: event) + ).sink(receiveValue: { event in + self.onAsyncSubscriptionEvent(event: event) }) } catch { let error = APIError.operationError("Unable to get connection for api \(endpointConfig.name)", "", error) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/API/APICategoryGraphQLBehaviorExtended.swift b/AmplifyPlugins/Core/AWSPluginsCore/API/APICategoryGraphQLBehaviorExtended.swift deleted file mode 100644 index 91d56f9763..0000000000 --- a/AmplifyPlugins/Core/AWSPluginsCore/API/APICategoryGraphQLBehaviorExtended.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation -import Amplify - -/// Extending the existing `APICategoryGraphQLBehavior` to include callback based APIs. -/// -/// This exists to allow DataStore to continue to use the `APICategoryGraphQLCallbackBehavior` APIs without exposing -/// them publicly from Amplify in `APICategoryGraphQLBehavior`. Eventually, the goal is for DataStore to use the -/// Async APIs, at which point, this protocol can be completely removed. Introducing this protocol allows Amplify to -/// to fully deprecate the callback based APIs, while allowing DataStore a gradual migration path forward in moving -/// away from APIPlugin's callback APIs to the Async APIs. -/// See https://github.com/aws-amplify/amplify-ios/issues/2252 for more details -/// -/// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly -/// by host applications. The behavior of this may change without warning. -public protocol APICategoryGraphQLBehaviorExtended: - APICategoryGraphQLCallbackBehavior, APICategoryGraphQLBehavior, AnyObject { } - -/// Listener callback based APIs -/// -/// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly -/// by host applications. The behavior of this may change without warning. -public protocol APICategoryGraphQLCallbackBehavior { - @discardableResult - func query(request: GraphQLRequest, - listener: GraphQLOperation.ResultListener?) -> GraphQLOperation - @discardableResult - func mutate(request: GraphQLRequest, - listener: GraphQLOperation.ResultListener?) -> GraphQLOperation - - func subscribe(request: GraphQLRequest, - valueListener: GraphQLSubscriptionOperation.InProcessListener?, - completionListener: GraphQLSubscriptionOperation.ResultListener?) - -> GraphQLSubscriptionOperation -} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift index 020dc68e60..eeeea09ff1 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift @@ -6,6 +6,7 @@ // import Foundation +import Combine import Amplify /// Represents different auth strategies supported by a client @@ -95,6 +96,35 @@ public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator { } } +extension AuthorizationTypeIterator { + public var asyncStream: AsyncStream { + var it = self + return AsyncStream { continuation in + while let authType = it.next() { + continuation.yield(authType) + } + continuation.finish() + } + } + + public var optionalAsyncStream: AsyncStream { + var it = self + if it.hasNext { + return AsyncStream { continuation in + while let authType = it.next() { + continuation.yield(authType) + } + continuation.finish() + } + } else { + return AsyncStream { continuation in + continuation.yield(nil) + continuation.finish() + } + } + } +} + // MARK: - AWSDefaultAuthModeStrategy /// AWS default auth mode strategy. diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift index 0b99894ea6..b6a8aac20c 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Storage/StorageEngine+SyncRequirement.swift @@ -25,7 +25,7 @@ extension StorageEngine { )) } - guard let apiGraphQL = api as? APICategoryGraphQLBehaviorExtended else { + guard let apiGraphQL = api as? APICategoryGraphQLBehavior else { log.info("Unable to find GraphQL API plugin for syncEngine. syncEngine will not be started") return .failure(.configuration( "Unable to find suitable GraphQL API plugin for syncEngine. syncEngine will not be started", diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift index e01f235b88..80b29e2245 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift @@ -13,7 +13,7 @@ import Foundation final class InitialSyncOperation: AsynchronousOperation { typealias SyncQueryResult = PaginatedList - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? private weak var storageAdapter: StorageEngineAdapter? private let dataStoreConfiguration: DataStoreConfiguration @@ -22,6 +22,7 @@ final class InitialSyncOperation: AsynchronousOperation { private let modelSchema: ModelSchema private var recordsReceived: UInt + private var queryTask: Task? private var syncMaxRecords: UInt { return dataStoreConfiguration.syncMaxRecords @@ -61,7 +62,7 @@ final class InitialSyncOperation: AsynchronousOperation { } init(modelSchema: ModelSchema, - api: APICategoryGraphQLBehaviorExtended?, + api: APICategoryGraphQLBehavior?, reconciliationQueue: IncomingEventReconciliationQueue?, storageAdapter: StorageEngineAdapter?, dataStoreConfiguration: DataStoreConfiguration, @@ -86,7 +87,7 @@ final class InitialSyncOperation: AsynchronousOperation { log.info("Beginning sync for \(modelSchema.name)") let lastSyncMetadata = getLastSyncMetadata() let lastSyncTime = getLastSyncTime(lastSyncMetadata) - Task { + self.queryTask = Task { await query(lastSyncTime: lastSyncTime) } } @@ -168,42 +169,41 @@ final class InitialSyncOperation: AsynchronousOperation { } let minSyncPageSize = Int(min(syncMaxRecords - recordsReceived, syncPageSize)) let limit = minSyncPageSize < 0 ? Int(syncPageSize) : minSyncPageSize - let completionListener: GraphQLOperation.ResultListener = { result in - switch result { - case .failure(let apiError): - if self.isAuthSignedOutError(apiError: apiError) { - self.log.error("Sync for \(self.modelSchema.name) failed due to signed out error \(apiError.errorDescription)") - } - - // TODO: Retry query on error - let error = DataStoreError.api(apiError) - self.dataStoreConfiguration.errorHandler(error) - self.finish(result: .failure(error)) - case .success(let graphQLResult): - self.handleQueryResults(lastSyncTime: lastSyncTime, graphQLResult: graphQLResult) + let authTypes = await authModeStrategy.authTypesFor(schema: modelSchema, operation: .read) + .optionalAsyncStream.map { authType in { + GraphQLRequest.syncQuery(modelSchema: self.modelSchema, + where: self.syncPredicate, + limit: limit, + nextToken: nextToken, + lastSync: lastSyncTime, + authType: authType + ) + }} + + + let result: Result, APIError> = await RetryableGraphQLOperation( + requestFactory: authTypes, + api: api + ).run(.query) + + switch result { + case .success(let graphQLResult): + await handleQueryResults(lastSyncTime: lastSyncTime, graphQLResult: graphQLResult) + case .failure(let apiError): + if self.isAuthSignedOutError(apiError: apiError) { + self.log.error("Sync for \(self.modelSchema.name) failed due to signed out error \(apiError.errorDescription)") } + self.dataStoreConfiguration.errorHandler(DataStoreError.api(apiError)) + self.finish(result: .failure(.api(apiError))) } - - var authTypes = await authModeStrategy.authTypesFor(schema: modelSchema, operation: .read) - - RetryableGraphQLOperation(requestFactory: { - GraphQLRequest.syncQuery(modelSchema: self.modelSchema, - where: self.syncPredicate, - limit: limit, - nextToken: nextToken, - lastSync: lastSyncTime, - authType: authTypes.next()) - }, - maxRetries: authTypes.count, - resultListener: completionListener) { nextRequest, wrappedCompletionListener in - api.query(request: nextRequest, listener: wrappedCompletionListener) - }.main() } /// Disposes of the query results: Stops if error, reconciles results if success, and kick off a new query if there /// is a next token - private func handleQueryResults(lastSyncTime: Int64?, - graphQLResult: Result>) { + private func handleQueryResults( + lastSyncTime: Int64?, + graphQLResult: Result> + ) async { guard !isCancelled else { finish(result: .successfulVoid) return @@ -238,9 +238,7 @@ final class InitialSyncOperation: AsynchronousOperation { } if let nextToken = syncQueryResult.nextToken, recordsReceived < syncMaxRecords { - Task { - await self.query(lastSyncTime: lastSyncTime, nextToken: nextToken) - } + await self.query(lastSyncTime: lastSyncTime, nextToken: nextToken) } else { updateModelSyncMetadata(lastSyncTime: syncQueryResult.startedAt) } @@ -292,6 +290,9 @@ final class InitialSyncOperation: AsynchronousOperation { super.finish() } + override func cancel() { + self.queryTask?.cancel() + } } extension InitialSyncOperation: DefaultLogger { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift index dbfe953ab1..806b19a240 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOrchestrator.swift @@ -19,7 +19,7 @@ protocol InitialSyncOrchestrator { typealias InitialSyncOrchestratorFactory = (DataStoreConfiguration, AuthModeStrategy, - APICategoryGraphQLBehaviorExtended?, + APICategoryGraphQLBehavior?, IncomingEventReconciliationQueue?, StorageEngineAdapter?) -> InitialSyncOrchestrator @@ -30,7 +30,7 @@ final class AWSInitialSyncOrchestrator: InitialSyncOrchestrator { private var initialSyncOperationSinks: [String: AnyCancellable] private let dataStoreConfiguration: DataStoreConfiguration - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? private weak var storageAdapter: StorageEngineAdapter? private let authModeStrategy: AuthModeStrategy @@ -52,7 +52,7 @@ final class AWSInitialSyncOrchestrator: InitialSyncOrchestrator { init(dataStoreConfiguration: DataStoreConfiguration, authModeStrategy: AuthModeStrategy, - api: APICategoryGraphQLBehaviorExtended?, + api: APICategoryGraphQLBehavior?, reconciliationQueue: IncomingEventReconciliationQueue?, storageAdapter: StorageEngineAdapter?) { self.initialSyncOperationSinks = [:] diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+Action.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+Action.swift index a9c8309ad6..f042cfab00 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+Action.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+Action.swift @@ -15,7 +15,7 @@ extension OutgoingMutationQueue { enum Action { // Startup/config actions case initialized - case receivedStart(APICategoryGraphQLBehaviorExtended, MutationEventPublisher, IncomingEventReconciliationQueue?) + case receivedStart(APICategoryGraphQLBehavior, MutationEventPublisher, IncomingEventReconciliationQueue?) case receivedSubscription // Event loop diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+State.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+State.swift index f7c59eb8ea..b8839a4b3e 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+State.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue+State.swift @@ -16,7 +16,7 @@ extension OutgoingMutationQueue { // Startup/config states case notInitialized case stopped - case starting(APICategoryGraphQLBehaviorExtended, MutationEventPublisher, IncomingEventReconciliationQueue?) + case starting(APICategoryGraphQLBehavior, MutationEventPublisher, IncomingEventReconciliationQueue?) // Event loop case requestingEvent diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift index 26cde77852..98ce9a3ce2 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/OutgoingMutationQueue.swift @@ -13,7 +13,7 @@ import AWSPluginsCore /// Submits outgoing mutation events to the provisioned API protocol OutgoingMutationQueueBehavior: AnyObject { func stopSyncingToCloud(_ completion: @escaping BasicClosure) - func startSyncingToCloud(api: APICategoryGraphQLBehaviorExtended, + func startSyncingToCloud(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) var publisher: AnyPublisher { get } @@ -32,7 +32,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { target: DispatchQueue.global() ) - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? private var subscription: Subscription? @@ -84,7 +84,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { // MARK: - Public API - func startSyncingToCloud(api: APICategoryGraphQLBehaviorExtended, + func startSyncingToCloud(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { log.verbose(#function) @@ -130,7 +130,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { /// Responder method for `starting`. Starts the operation queue and subscribes to /// the publisher. After subscribing to the publisher, return actions: /// - receivedSubscription - private func doStart(api: APICategoryGraphQLBehaviorExtended, + private func doStart(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { log.verbose(#function) @@ -222,7 +222,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { private func processSyncMutationToCloudResult(_ result: GraphQLOperation>.OperationResult, mutationEvent: MutationEvent, - api: APICategoryGraphQLBehaviorExtended) { + api: APICategoryGraphQLBehavior) { if case let .success(graphQLResponse) = result { if case let .success(graphQLResult) = graphQLResponse { processSuccessEvent(mutationEvent, @@ -271,7 +271,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { } private func processMutationErrorFromCloud(mutationEvent: MutationEvent, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, apiError: APIError?, graphQLResponseError: GraphQLResponseError>?) { if let apiError = apiError, apiError.isOperationCancelledError { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift index bbc4ec0895..c334745fb7 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/ProcessMutationErrorFromCloudOperation.swift @@ -27,12 +27,12 @@ class ProcessMutationErrorFromCloudOperation: AsynchronousOperation { private let apiError: APIError? private let completion: (Result) -> Void private var mutationOperation: AtomicValue>?> - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? init(dataStoreConfiguration: DataStoreConfiguration, mutationEvent: MutationEvent, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, storageAdapter: StorageEngineAdapter, graphQLResponseError: GraphQLResponseError>? = nil, apiError: APIError? = nil, @@ -296,44 +296,44 @@ class ProcessMutationErrorFromCloudOperation: AsynchronousOperation { } log.verbose("\(#function) sending mutation with data: \(apiRequest)") - let graphQLOperation = api.mutate(request: apiRequest) { [weak self] result in - guard let self = self, !self.isCancelled else { - return - } + Task { [weak self] in + do { + let result = try await api.mutate(request: apiRequest) + guard let self = self, !self.isCancelled else { + self?.finish(result: .failure(APIError.operationError("Mutation operation cancelled", ""))) + return + } - self.log.verbose("sendMutationToCloud received asyncEvent: \(result)") - self.validate(cloudResult: result, request: apiRequest) + self.log.verbose("sendMutationToCloud received asyncEvent: \(result)") + self.validate(cloudResult: result, request: apiRequest) + } catch { + self?.finish(result: .failure(APIError.operationError("Failed to do mutation", "", error))) + } } - mutationOperation.set(graphQLOperation) } - private func validate(cloudResult: MutationSyncCloudResult, request: MutationSyncAPIRequest) { + private func validate(cloudResult: GraphQLResponse, request: MutationSyncAPIRequest) { guard !isCancelled else { return } - if case .failure(let error) = cloudResult { - dataStoreConfiguration.errorHandler(error) - } - - if case let .success(graphQLResponse) = cloudResult { - if case .failure(let error) = graphQLResponse { - dataStoreConfiguration.errorHandler(error) - } else if case let .success(graphQLResult) = graphQLResponse { - guard let reconciliationQueue = reconciliationQueue else { - let dataStoreError = DataStoreError.configuration( - "reconciliationQueue is unexpectedly nil", - """ - The reference to reconciliationQueue has been released while an ongoing mutation was being processed. - \(AmplifyErrorMessages.reportBugToAWS()) - """ - ) - finish(result: .failure(dataStoreError)) - return - } - - reconciliationQueue.offer([graphQLResult], modelName: mutationEvent.modelName) + switch cloudResult { + case .success(let mutationSyncResult): + guard let reconciliationQueue = reconciliationQueue else { + let dataStoreError = DataStoreError.configuration( + "reconciliationQueue is unexpectedly nil", + """ + The reference to reconciliationQueue has been released while an ongoing mutation was being processed. + \(AmplifyErrorMessages.reportBugToAWS()) + """ + ) + finish(result: .failure(dataStoreError)) + return } + + reconciliationQueue.offer([mutationSyncResult], modelName: mutationEvent.modelName) + case .failure(let graphQLResponseError): + dataStoreConfiguration.errorHandler(graphQLResponseError) } finish(result: .success(nil)) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift index 3732bafb4a..db45220167 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift @@ -17,23 +17,23 @@ class SyncMutationToCloudOperation: AsynchronousOperation { typealias MutationSyncCloudResult = GraphQLOperation>.OperationResult - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private let mutationEvent: MutationEvent private let getLatestSyncMetadata: () -> MutationSyncMetadata? private let completion: GraphQLOperation>.ResultListener private let requestRetryablePolicy: RequestRetryablePolicy - private let lock: NSRecursiveLock +// private let lock: NSRecursiveLock private var networkReachabilityPublisher: AnyPublisher? - private var mutationOperation: GraphQLOperation>? + private var mutationOperation: Task? private var mutationRetryNotifier: MutationRetryNotifier? private var currentAttemptNumber: Int private var authTypesIterator: AWSAuthorizationTypeIterator? init(mutationEvent: MutationEvent, getLatestSyncMetadata: @escaping () -> MutationSyncMetadata?, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, authModeStrategy: AuthModeStrategy, networkReachabilityPublisher: AnyPublisher? = nil, currentAttemptNumber: Int = 1, @@ -46,7 +46,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { self.completion = completion self.currentAttemptNumber = currentAttemptNumber self.requestRetryablePolicy = requestRetryablePolicy ?? RequestRetryablePolicy() - self.lock = NSRecursiveLock() +// self.lock = NSRecursiveLock() if let modelSchema = ModelRegistry.modelSchema(from: mutationEvent.modelName), let mutationType = GraphQLMutationType(rawValue: mutationEvent.mutationType) { @@ -66,11 +66,11 @@ class SyncMutationToCloudOperation: AsynchronousOperation { override func cancel() { log.verbose(#function) - lock.execute { - mutationOperation?.cancel() - mutationRetryNotifier?.cancel() - mutationRetryNotifier = nil - } +// lock.execute { + mutationOperation?.cancel() + mutationRetryNotifier?.cancel() + mutationRetryNotifier = nil +// } let apiError = APIError(error: OperationCancelledError()) finish(result: .failure(apiError)) @@ -209,41 +209,56 @@ class SyncMutationToCloudOperation: AsynchronousOperation { return } log.verbose("\(#function) sending mutation with sync data: \(apiRequest)") - lock.execute { - mutationOperation = api.mutate(request: apiRequest) { [weak self] result in - self?.respond(toCloudResult: result, withAPIRequest: apiRequest) + + mutationOperation = Task { [weak self] in + let result: GraphQLResponse> + do { + result = try await api.mutate(request: apiRequest) + } catch { + result = .failure(.unknown("Failed to send sync mutation request", "", error)) } + +// self?.lock.execute { [weak self] in + self?.respond( + toCloudResult: result, + withAPIRequest: apiRequest + ) +// } } + } /// Initiates a locking context private func respond( - toCloudResult result: GraphQLOperation>.OperationResult, + toCloudResult result: GraphQLResponse>, withAPIRequest apiRequest: GraphQLRequest> ) { - lock.execute { - guard !self.isCancelled else { - Amplify.log.debug("SyncMutationToCloudOperation cancelled, aborting") - return - } - - log.verbose("GraphQL mutation operation received result: \(result)") - validate(cloudResult: result, request: apiRequest) +// lock.execute { + guard !self.isCancelled else { + Amplify.log.debug("SyncMutationToCloudOperation cancelled, aborting") + return } + + log.verbose("GraphQL mutation operation received result: \(result)") + validate(cloudResult: result, request: apiRequest) +// } } /// - Warning: Must be invoked from a locking context - private func validate(cloudResult: MutationSyncCloudResult, - request: GraphQLRequest>) { - guard !isCancelled else { + private func validate( + cloudResult: GraphQLResponse>, + request: GraphQLRequest> + ) { + guard !isCancelled, let mutationOperation, !mutationOperation.isCancelled else { return } - if case .failure(let error) = cloudResult { - let advice = getRetryAdviceIfRetryable(error: error) + if case .failure(let error) = cloudResult, + let apiError = error.underlyingError as? APIError { + let advice = getRetryAdviceIfRetryable(error: apiError) guard advice.shouldRetry else { - finish(result: .failure(error)) + finish(result: .failure(apiError)) return } @@ -257,7 +272,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { return } - finish(result: cloudResult) + finish(result: .success(cloudResult)) } /// - Warning: Must be invoked from a locking context @@ -341,20 +356,20 @@ class SyncMutationToCloudOperation: AsynchronousOperation { /// Initiates a locking context private func respondToMutationNotifierTriggered(withAuthType authType: AWSAuthorizationType?) { log.verbose("\(#function) mutationRetryNotifier triggered") - lock.execute { +// lock.execute { sendMutationToCloud(withAuthType: authType) mutationRetryNotifier = nil - } +// } } /// Cleans up operation resources, finalizes AsynchronousOperation states, and invokes `completion` with `result` /// - Parameter result: The MutationSyncCloudResult to pass to `completion` private func finish(result: MutationSyncCloudResult) { log.verbose(#function) - lock.execute { - mutationOperation?.removeResultListener() - mutationOperation = nil - } +// lock.execute { + mutationOperation?.cancel() + mutationOperation = nil +// } DispatchQueue.global().async { self.completion(result) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+Action.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+Action.swift index 7421637a54..7ace6cd086 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+Action.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+Action.swift @@ -18,10 +18,10 @@ extension RemoteSyncEngine { case pausedSubscriptions case pausedMutationQueue(StorageEngineAdapter) - case clearedStateOutgoingMutations(APICategoryGraphQLBehaviorExtended, StorageEngineAdapter) + case clearedStateOutgoingMutations(APICategoryGraphQLBehavior, StorageEngineAdapter) case initializedSubscriptions case performedInitialSync - case activatedCloudSubscriptions(APICategoryGraphQLBehaviorExtended, MutationEventPublisher, IncomingEventReconciliationQueue?) + case activatedCloudSubscriptions(APICategoryGraphQLBehavior, MutationEventPublisher, IncomingEventReconciliationQueue?) case activatedMutationQueue case notifiedSyncStarted case cleanedUp(AmplifyError) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+State.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+State.swift index d55c3fe5c3..a1ecebfbbb 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+State.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine+State.swift @@ -18,10 +18,10 @@ extension RemoteSyncEngine { case pausingSubscriptions case pausingMutationQueue case clearingStateOutgoingMutations(StorageEngineAdapter) - case initializingSubscriptions(APICategoryGraphQLBehaviorExtended, StorageEngineAdapter) + case initializingSubscriptions(APICategoryGraphQLBehavior, StorageEngineAdapter) case performingInitialSync case activatingCloudSubscriptions - case activatingMutationQueue(APICategoryGraphQLBehaviorExtended, MutationEventPublisher, IncomingEventReconciliationQueue?) + case activatingMutationQueue(APICategoryGraphQLBehavior, MutationEventPublisher, IncomingEventReconciliationQueue?) case notifyingSyncStarted case syncEngineActive diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine.swift index 26bb453571..fd30c9ecae 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngine.swift @@ -21,7 +21,7 @@ class RemoteSyncEngine: RemoteSyncEngineBehavior { private var authModeStrategy: AuthModeStrategy // Assigned at `start` - weak var api: APICategoryGraphQLBehaviorExtended? + weak var api: APICategoryGraphQLBehavior? weak var auth: AuthCategoryBehavior? // Assigned and released inside `performInitialQueries`, but we maintain a reference so we can `reset` @@ -197,7 +197,7 @@ class RemoteSyncEngine: RemoteSyncEngineBehavior { } // swiftlint:enable cyclomatic_complexity - func start(api: APICategoryGraphQLBehaviorExtended, auth: AuthCategoryBehavior?) { + func start(api: APICategoryGraphQLBehavior, auth: AuthCategoryBehavior?) { guard storageAdapter != nil else { log.error(error: DataStoreError.nilStorageAdapter()) remoteSyncTopicPublisher.send(completion: .failure(DataStoreError.nilStorageAdapter())) @@ -280,7 +280,7 @@ class RemoteSyncEngine: RemoteSyncEngineBehavior { } } - private func initializeSubscriptions(api: APICategoryGraphQLBehaviorExtended, + private func initializeSubscriptions(api: APICategoryGraphQLBehavior, storageAdapter: StorageEngineAdapter) async { log.debug("[InitializeSubscription] \(#function)") let syncableModelSchemas = ModelRegistry.modelSchemas.filter { $0.isSyncable } @@ -363,7 +363,7 @@ class RemoteSyncEngine: RemoteSyncEngineBehavior { reconciliationQueue.start() } - private func startMutationQueue(api: APICategoryGraphQLBehaviorExtended, + private func startMutationQueue(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { log.debug(#function) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngineBehavior.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngineBehavior.swift index ee5710ff21..765d72473f 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngineBehavior.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/RemoteSyncEngineBehavior.swift @@ -41,7 +41,7 @@ protocol RemoteSyncEngineBehavior: AnyObject { /// the updates in the Datastore /// 1. Mutation processor drains messages off the queue in serial and sends to the service, invoking /// any local callbacks on error if necessary - func start(api: APICategoryGraphQLBehaviorExtended, auth: AuthCategoryBehavior?) + func start(api: APICategoryGraphQLBehavior, auth: AuthCategoryBehavior?) func stop(completion: @escaping DataStoreCallback) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingEventReconciliationQueue.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingEventReconciliationQueue.swift index 7705120b4b..4a6d765aca 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingEventReconciliationQueue.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingEventReconciliationQueue.swift @@ -15,7 +15,7 @@ typealias DisableSubscriptions = () -> Bool // Used for testing: typealias IncomingEventReconciliationQueueFactory = ([ModelSchema], - APICategoryGraphQLBehaviorExtended, + APICategoryGraphQLBehavior, StorageEngineAdapter, [DataStoreSyncExpression], AuthCategoryBehavior?, @@ -46,7 +46,7 @@ final class AWSIncomingEventReconciliationQueue: IncomingEventReconciliationQueu private let modelSchemasCount: Int init(modelSchemas: [ModelSchema], - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, storageAdapter: StorageEngineAdapter, syncExpressions: [DataStoreSyncExpression], auth: AuthCategoryBehavior? = nil, diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingSubscriptionEventPublisher.swift index 32365419fe..76c4b7dc9a 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/AWSIncomingSubscriptionEventPublisher.swift @@ -23,7 +23,7 @@ final class AWSIncomingSubscriptionEventPublisher: IncomingSubscriptionEventPubl } init(modelSchema: ModelSchema, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, modelPredicate: QueryPredicate?, auth: AuthCategoryBehavior?, authModeStrategy: AuthModeStrategy) async { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift index d5dae69b37..b125e239a1 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift @@ -6,7 +6,7 @@ // import Amplify -import AWSPluginsCore +@_spi(WebSocket) import AWSPluginsCore import Combine import Foundation @@ -39,7 +39,8 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { return onCreateConnected && onUpdateConnected && onDeleteConnected } - private let incomingSubscriptionEvents: PassthroughSubject + private let incomingSubscriptionEvents = PassthroughSubject() + private var cancelables = Set() private let awsAuthService: AWSAuthServiceBehavior private let consistencyQueue: DispatchQueue @@ -47,7 +48,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { private let modelName: ModelName init(modelSchema: ModelSchema, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, modelPredicate: QueryPredicate?, auth: AuthCategoryBehavior?, authModeStrategy: AuthModeStrategy, @@ -67,72 +68,74 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { connectionStatusQueue.maxConcurrentOperationCount = 1 connectionStatusQueue.isSuspended = false - let incomingSubscriptionEvents = PassthroughSubject() - self.incomingSubscriptionEvents = incomingSubscriptionEvents self.awsAuthService = awsAuthService ?? AWSAuthService() // onCreate operation - let onCreateValueListener = onCreateValueListenerHandler(event:) - let onCreateAuthTypeProvider = await authModeStrategy.authTypesFor(schema: modelSchema, - operations: [.create, .read]) - self.onCreateValueListener = onCreateValueListener - self.onCreateOperation = RetryableGraphQLSubscriptionOperation( - requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( - for: modelSchema, - subscriptionType: .onCreate, - api: api, - auth: auth, - awsAuthService: self.awsAuthService, - authTypeProvider: onCreateAuthTypeProvider), - maxRetries: onCreateAuthTypeProvider.count, - resultListener: genericCompletionListenerHandler) { nextRequest, wrappedCompletion in - api.subscribe(request: nextRequest, - valueListener: onCreateValueListener, - completionListener: wrappedCompletion) - } - onCreateOperation?.main() + self.onCreateValueListener = onCreateValueListenerHandler(event:) + self.onCreateOperation = await retryableOperation( + subscriptionType: .create, + modelSchema: modelSchema, + authModeStrategy: authModeStrategy, + auth: auth, + api: api + ) + onCreateOperation?.subscribe() + .sink(receiveCompletion: genericCompletionListenerHandler(result:), receiveValue: onCreateValueListener!) + .store(in: &cancelables) // onUpdate operation - let onUpdateValueListener = onUpdateValueListenerHandler(event:) - let onUpdateAuthTypeProvider = await authModeStrategy.authTypesFor(schema: modelSchema, - operations: [.update, .read]) - self.onUpdateValueListener = onUpdateValueListener - self.onUpdateOperation = RetryableGraphQLSubscriptionOperation( - requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( - for: modelSchema, - subscriptionType: .onUpdate, - api: api, - auth: auth, - awsAuthService: self.awsAuthService, - authTypeProvider: onUpdateAuthTypeProvider), - maxRetries: onUpdateAuthTypeProvider.count, - resultListener: genericCompletionListenerHandler) { nextRequest, wrappedCompletion in - api.subscribe(request: nextRequest, - valueListener: onUpdateValueListener, - completionListener: wrappedCompletion) - } - onUpdateOperation?.main() + self.onUpdateValueListener = onUpdateValueListenerHandler(event:) + self.onUpdateOperation = await retryableOperation( + subscriptionType: .update, + modelSchema: modelSchema, + authModeStrategy: authModeStrategy, + auth: auth, + api: api + ) + onUpdateOperation?.subscribe() + .sink(receiveCompletion: genericCompletionListenerHandler(result:), receiveValue: onUpdateValueListener!) + .store(in: &cancelables) // onDelete operation - let onDeleteValueListener = onDeleteValueListenerHandler(event:) - let onDeleteAuthTypeProvider = await authModeStrategy.authTypesFor(schema: modelSchema, - operations: [.delete, .read]) - self.onDeleteValueListener = onDeleteValueListener - self.onDeleteOperation = RetryableGraphQLSubscriptionOperation( - requestFactory: IncomingAsyncSubscriptionEventPublisher.apiRequestFactoryFor( - for: modelSchema, - subscriptionType: .onDelete, - api: api, - auth: auth, - awsAuthService: self.awsAuthService, - authTypeProvider: onDeleteAuthTypeProvider), - maxRetries: onUpdateAuthTypeProvider.count, - resultListener: genericCompletionListenerHandler) { nextRequest, wrappedCompletion in - api.subscribe(request: nextRequest, - valueListener: onDeleteValueListener, - completionListener: wrappedCompletion) - } - onDeleteOperation?.main() + self.onDeleteValueListener = onDeleteValueListenerHandler(event:) + self.onDeleteOperation = await retryableOperation( + subscriptionType: .delete, + modelSchema: modelSchema, + authModeStrategy: authModeStrategy, + auth: auth, + api: api + ) + onDeleteOperation?.subscribe() + .sink(receiveCompletion: genericCompletionListenerHandler(result:), receiveValue: onDeleteValueListener!) + .store(in: &cancelables) + } + + + func retryableOperation( + subscriptionType: IncomingAsyncSubscriptionType, + modelSchema: ModelSchema, + authModeStrategy: AuthModeStrategy, + auth: AuthCategoryBehavior?, + api: APICategoryGraphQLBehavior + ) async -> RetryableGraphQLSubscriptionOperation { + let authTypeProvider = await authModeStrategy.authTypesFor( + schema: modelSchema, + operations: subscriptionType.operations + ) + + return RetryableGraphQLSubscriptionOperation( + requestFactory: authTypeProvider.optionalAsyncStream.map { authType in { + await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( + for: modelSchema, + subscriptionType: subscriptionType.subscriptionType, + api: api, + auth: auth, + authType: authType, + awsAuthService: self.awsAuthService + ) + }}, + api: api + ) } func onCreateValueListenerHandler(event: Event) { @@ -183,9 +186,9 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { } } - func genericCompletionListenerHandler(result: Result) { + func genericCompletionListenerHandler(result: Subscribers.Completion) { switch result { - case .success: + case .finished: send(completion: .finished) case .failure(let apiError): log.verbose("[InitializeSubscription.1] API.subscribe failed for `\(modelName)` error: \(apiError.errorDescription)") @@ -196,7 +199,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { static func makeAPIRequest(for modelSchema: ModelSchema, subscriptionType: GraphQLSubscriptionType, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, auth: AuthCategoryBehavior?, authType: AWSAuthorizationType?, awsAuthService: AWSAuthServiceBehavior) async -> GraphQLRequest { @@ -226,7 +229,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { return request } - static func hasOIDCAuthProviderAvailable(api: APICategoryGraphQLBehaviorExtended) -> AmplifyOIDCAuthProvider? { + static func hasOIDCAuthProviderAvailable(api: APICategoryGraphQLBehavior) -> AmplifyOIDCAuthProvider? { if let apiPlugin = api as? APICategoryAuthProviderFactoryBehavior, let oidcAuthProvider = apiPlugin.apiAuthProviderFactory().oidcAuthProvider() { return oidcAuthProvider @@ -254,7 +257,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { func cancel() { consistencyQueue.sync { - genericCompletionListenerHandler(result: .successfulVoid) + genericCompletionListenerHandler(result: .finished) onCreateOperation?.cancel() onCreateOperation = nil @@ -287,30 +290,33 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { onDeleteOperation = nil onDeleteValueListener?(.connection(.disconnected)) - genericCompletionListenerHandler(result: .successfulVoid) + genericCompletionListenerHandler(result: .finished) } } } -// MARK: - IncomingAsyncSubscriptionEventPublisher + API request factory -extension IncomingAsyncSubscriptionEventPublisher { - static func apiRequestFactoryFor(for modelSchema: ModelSchema, - subscriptionType: GraphQLSubscriptionType, - api: APICategoryGraphQLBehaviorExtended, - auth: AuthCategoryBehavior?, - awsAuthService: AWSAuthServiceBehavior, - authTypeProvider: AWSAuthorizationTypeIterator) -> RetryableGraphQLOperation.RequestFactory { - var authTypes = authTypeProvider - return { - return await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest(for: modelSchema, - subscriptionType: subscriptionType, - api: api, - auth: auth, - authType: authTypes.next(), - awsAuthService: awsAuthService) +enum IncomingAsyncSubscriptionType { + case create + case delete + case update + + var operations: [ModelOperation] { + switch self { + case .create: return [.create, .read] + case .delete: return [.delete, .read] + case .update: return [.update, .read] } } + + var subscriptionType: GraphQLSubscriptionType { + switch self { + case .create: return .onCreate + case .delete: return .onDelete + case .update: return .onUpdate + } + } + } extension IncomingAsyncSubscriptionEventPublisher: DefaultLogger { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/AWSModelReconciliationQueue.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/AWSModelReconciliationQueue.swift index 7eacedb029..03074d82e3 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/AWSModelReconciliationQueue.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/AWSModelReconciliationQueue.swift @@ -14,7 +14,7 @@ import Foundation typealias ModelReconciliationQueueFactory = ( ModelSchema, StorageEngineAdapter, - APICategoryGraphQLBehaviorExtended, + APICategoryGraphQLBehavior, ReconcileAndSaveOperationQueue, QueryPredicate?, AuthCategoryBehavior?, @@ -78,7 +78,7 @@ final class AWSModelReconciliationQueue: ModelReconciliationQueue { init(modelSchema: ModelSchema, storageAdapter: StorageEngineAdapter?, - api: APICategoryGraphQLBehaviorExtended, + api: APICategoryGraphQLBehavior, reconcileAndSaveQueue: ReconcileAndSaveOperationQueue, modelPredicate: QueryPredicate?, auth: AuthCategoryBehavior?, diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift new file mode 100644 index 0000000000..0dee36be66 --- /dev/null +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift @@ -0,0 +1,21 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation +import Combine + +extension AsyncStream { + static func from(seq: any Sequence) -> AsyncStream { + AsyncStream { continuation in + for ele in seq { + continuation.yield(ele) + } + continuation.finish() + } + } +} diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockOutgoingMutationQueue.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockOutgoingMutationQueue.swift index 3eec2bce57..e0223bac2b 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockOutgoingMutationQueue.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockOutgoingMutationQueue.swift @@ -17,7 +17,7 @@ class MockOutgoingMutationQueue: OutgoingMutationQueueBehavior { completion() } - func startSyncingToCloud(api: APICategoryGraphQLBehaviorExtended, + func startSyncingToCloud(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { // no-op diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockRemoteSyncEngine.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockRemoteSyncEngine.swift index 105ab41ebf..1f2036784e 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockRemoteSyncEngine.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/MockRemoteSyncEngine.swift @@ -37,7 +37,7 @@ class MockRemoteSyncEngine: RemoteSyncEngineBehavior { init() { self.remoteSyncTopicPublisher = PassthroughSubject() } - func start(api: APICategoryGraphQLBehaviorExtended, auth: AuthCategoryBehavior?) { + func start(api: APICategoryGraphQLBehavior, auth: AuthCategoryBehavior?) { syncing = true } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/NoOpMutationQueue.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/NoOpMutationQueue.swift index 56a65bc96b..82c9b031af 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/NoOpMutationQueue.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/Mocks/NoOpMutationQueue.swift @@ -17,7 +17,7 @@ class NoOpMutationQueue: OutgoingMutationQueueBehavior { completion() } - func startSyncingToCloud(api: APICategoryGraphQLBehaviorExtended, + func startSyncingToCloud(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { // do nothing diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/Connection/DataStoreConnectionScenario1Tests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/Connection/DataStoreConnectionScenario1Tests.swift index ae26e441e7..49c5fb097e 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/Connection/DataStoreConnectionScenario1Tests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/Connection/DataStoreConnectionScenario1Tests.swift @@ -228,7 +228,7 @@ class DataStoreConnectionScenario1Tests: SyncEngineIntegrationTestBase { } func testDeleteWithInvalidCondition() async throws { - await setUp(withModels: TestModelRegistration()) + await setUp(withModels: TestModelRegistration(), logLevel: .verbose) try await startAmplifyAndWaitForSync() let team = Team1(name: "name") let project = Project1(team: team) diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreHubEventsTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreHubEventsTests.swift index 916a4d26ea..a5fe3115f4 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreHubEventsTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreHubEventsTests.swift @@ -37,7 +37,6 @@ class DataStoreHubEventTests: HubEventsIntegrationTestBase { /// {modelName: "Some Model name", isFullSync: true/false, isDeltaSync: false/true, createCount: #, updateCount: #, deleteCount: #} /// - syncQueriesReady received, payload should be nil func testDataStoreConfiguredDispatchesHubEvents() async throws { - Amplify.Logging.logLevel = .verbose try configureAmplify(withModels: TestModelRegistration()) try await Amplify.DataStore.clear() await Amplify.reset() diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreLargeNumberModelsSubscriptionTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreLargeNumberModelsSubscriptionTests.swift index cc09f0f40d..4121f562bd 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreLargeNumberModelsSubscriptionTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/DataStoreLargeNumberModelsSubscriptionTests.swift @@ -46,7 +46,7 @@ class DataStoreLargeNumberModelsSubscriptionTests: SyncEngineIntegrationTestBase } func testDataStoreStop_subscriptionsShouldAllUnsubscribed() async throws { - await setUp(withModels: TestModelRegistration()) + await setUp(withModels: TestModelRegistration(), logLevel: .verbose) try await startAmplifyAndWaitForSync() try await stopDataStoreAndVerifyAppSyncClientDisconnected() diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/TestSupport/HubEventsIntegrationTestBase.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/TestSupport/HubEventsIntegrationTestBase.swift index 9bb4167312..e480689b9e 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/TestSupport/HubEventsIntegrationTestBase.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginIntegrationTests/TestSupport/HubEventsIntegrationTestBase.swift @@ -52,7 +52,9 @@ class HubEventsIntegrationTestBase: XCTestCase { #if os(watchOS) try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: models, configuration: .subscriptionsDisabled)) #else - try Amplify.add(plugin: AWSDataStorePlugin(modelRegistration: models)) + try Amplify.add(plugin: AWSDataStorePlugin( + modelRegistration: models + )) #endif try Amplify.add(plugin: AWSAPIPlugin( modelRegistration: models, diff --git a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift index 2b65491429..31ac246db1 100644 --- a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift @@ -13,7 +13,7 @@ import Foundation class MockAPICategoryPlugin: MessageReporter, APICategoryPlugin, APICategoryReachabilityBehavior, - APICategoryGraphQLBehaviorExtended { + APICategoryGraphQLBehavior { var authProviderFactory: APIAuthProviderFactory? From 276e13f0d492414260ea3d7076e97fa225b88691 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 9 Apr 2024 23:25:32 -0700 Subject: [PATCH 04/23] change to use Publisher operators for auth type streams --- .../Operation/RetryableGraphQLOperation.swift | 36 ++++++++++++++++++- .../Auth/AWSAuthModeStrategy.swift | 28 ++++----------- .../InitialSync/InitialSyncOperation.swift | 21 ++++++----- ...omingAsyncSubscriptionEventPublisher.swift | 24 +++++++------ .../Sync/Support/AsyncStream+Extensions.swift | 21 ----------- 5 files changed, 68 insertions(+), 62 deletions(-) delete mode 100644 AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift diff --git a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift index 86281585b0..0507431db1 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -16,6 +16,7 @@ public final class RetryableGraphQLOperation { public let requestFactory: AsyncStream<() -> GraphQLRequest> public weak var api: APICategoryGraphQLBehavior? private var task: Task? + private var cancellables = Set() public init( requestFactory: T, @@ -25,6 +26,21 @@ public final class RetryableGraphQLOperation { self.api = api } + public convenience init( + requestStream: AnyPublisher<() -> GraphQLRequest, Never>, + api: APICategoryGraphQLBehavior + ) { + var cancellables = Set() + self.init(requestFactory: AsyncStream { continuation in + requestStream.sink { completion in + continuation.finish() + } receiveValue: { value in + continuation.yield(value) + }.store(in: &cancellables) + }, api: api) + self.cancellables = cancellables + } + deinit { cancel() } @@ -60,7 +76,7 @@ public final class RetryableGraphQLOperation { } switch authError { - case .signedOut, .notAuthorized: break; + case .signedOut, .notAuthorized: break default: return .failure(error) } } @@ -70,6 +86,7 @@ public final class RetryableGraphQLOperation { public func cancel() { task?.cancel() + cancellables = Set() } } @@ -81,6 +98,7 @@ public final class RetryableGraphQLSubscriptionOperation { public let requestFactory: AsyncStream<() async -> GraphQLRequest> public weak var api: APICategoryGraphQLBehavior? private var task: Task? + private var cancellables = Set() public init( requestFactory: T, @@ -90,6 +108,21 @@ public final class RetryableGraphQLSubscriptionOperation { self.api = api } + public convenience init( + requestStream: AnyPublisher<() async -> GraphQLRequest, Never>, + api: APICategoryGraphQLBehavior + ) { + var cancellables = Set() + self.init(requestFactory: AsyncStream { continuation in + requestStream.sink { completion in + continuation.finish() + } receiveValue: { value in + continuation.yield(value) + }.store(in: &cancellables) + }, api: api) + self.cancellables = cancellables + } + deinit { cancel() } @@ -133,6 +166,7 @@ public final class RetryableGraphQLSubscriptionOperation { public func cancel() { self.task?.cancel() + self.cancellables = Set() } } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift index eeeea09ff1..91d38c2855 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift @@ -97,31 +97,15 @@ public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator { } extension AuthorizationTypeIterator { - public var asyncStream: AsyncStream { + public func publisher() -> AnyPublisher { var it = self - return AsyncStream { continuation in + return Deferred { + var authTypes = [AuthorizationType]() while let authType = it.next() { - continuation.yield(authType) + authTypes.append(authType) } - continuation.finish() - } - } - - public var optionalAsyncStream: AsyncStream { - var it = self - if it.hasNext { - return AsyncStream { continuation in - while let authType = it.next() { - continuation.yield(authType) - } - continuation.finish() - } - } else { - return AsyncStream { continuation in - continuation.yield(nil) - continuation.finish() - } - } + return Publishers.MergeMany(authTypes.map { Just($0) }) + }.eraseToAnyPublisher() } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift index 80b29e2245..6cf1e6b77a 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift @@ -170,19 +170,24 @@ final class InitialSyncOperation: AsynchronousOperation { let minSyncPageSize = Int(min(syncMaxRecords - recordsReceived, syncPageSize)) let limit = minSyncPageSize < 0 ? Int(syncPageSize) : minSyncPageSize let authTypes = await authModeStrategy.authTypesFor(schema: modelSchema, operation: .read) - .optionalAsyncStream.map { authType in { - GraphQLRequest.syncQuery(modelSchema: self.modelSchema, - where: self.syncPredicate, - limit: limit, - nextToken: nextToken, - lastSync: lastSyncTime, - authType: authType + .publisher() + .map { Optional.some($0) } // map to optional to have nil as element + .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided + .map { authType in { + GraphQLRequest.syncQuery( + modelSchema: self.modelSchema, + where: self.syncPredicate, + limit: limit, + nextToken: nextToken, + lastSync: lastSyncTime, + authType: authType ) }} + .eraseToAnyPublisher() let result: Result, APIError> = await RetryableGraphQLOperation( - requestFactory: authTypes, + requestStream: authTypes, api: api ).run(.query) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift index b125e239a1..81171e8f90 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift @@ -124,16 +124,20 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { ) return RetryableGraphQLSubscriptionOperation( - requestFactory: authTypeProvider.optionalAsyncStream.map { authType in { - await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( - for: modelSchema, - subscriptionType: subscriptionType.subscriptionType, - api: api, - auth: auth, - authType: authType, - awsAuthService: self.awsAuthService - ) - }}, + requestStream: authTypeProvider.publisher() + .map { Optional.some($0) } // map to optional to have nil as element + .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided + .map { authType in { + await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( + for: modelSchema, + subscriptionType: subscriptionType.subscriptionType, + api: api, + auth: auth, + authType: authType, + awsAuthService: self.awsAuthService + ) + }} + .eraseToAnyPublisher(), api: api ) } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift deleted file mode 100644 index 0dee36be66..0000000000 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/Support/AsyncStream+Extensions.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - - -import Foundation -import Combine - -extension AsyncStream { - static func from(seq: any Sequence) -> AsyncStream { - AsyncStream { continuation in - for ele in seq { - continuation.yield(ele) - } - continuation.finish() - } - } -} From c03a50b5367a7b48fbde79d4a92a98672b0ab7b3 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 10 Apr 2024 19:51:41 -0700 Subject: [PATCH 05/23] add nondeterminsitc operation for better testability --- .../Operation/NondeterminsticOperation.swift | 99 ++++++++++ .../Operation/RetryableGraphQLOperation.swift | 164 ++++++---------- .../InitialSync/InitialSyncOperation.swift | 18 +- ...omingAsyncSubscriptionEventPublisher.swift | 13 +- .../API/NondeterminsticOperationTests.swift | 126 ++++++++++++ .../API/RetryableGraphQLOperationTests.swift | 184 ++++++------------ 6 files changed, 363 insertions(+), 241 deletions(-) create mode 100644 Amplify/Categories/API/Operation/NondeterminsticOperation.swift create mode 100644 AmplifyTests/CategoryTests/API/NondeterminsticOperationTests.swift diff --git a/Amplify/Categories/API/Operation/NondeterminsticOperation.swift b/Amplify/Categories/API/Operation/NondeterminsticOperation.swift new file mode 100644 index 0000000000..7c930b1924 --- /dev/null +++ b/Amplify/Categories/API/Operation/NondeterminsticOperation.swift @@ -0,0 +1,99 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Combine +/** + A non-deterministic operation offers multiple paths to accomplish its task. + It attempts the next path if all preceding paths have failed with an error that allows for continuation. + */ +enum NondeterminsticOperationError: Error { + case totalFailure + case cancelled +} + +final class NondeterminsticOperation { + /// operation that to be eval + typealias Operation = () async throws -> T + typealias OnError = (Error) -> Bool + + private let operations: AsyncStream + private var shouldTryNextOnError: OnError = { _ in true } + private var cancellables = Set() + private var task: Task? + + deinit { + cancel() + } + + init(operations: AsyncStream, shouldTryNextOnError: OnError? = nil) { + self.operations = operations + if let shouldTryNextOnError { + self.shouldTryNextOnError = shouldTryNextOnError + } + } + + convenience init( + operationStream: AnyPublisher, + shouldTryNextOnError: OnError? = nil + ) { + var cancellables = Set() + self.init( + operations: AsyncStream { continuation in + operationStream.sink { _ in + continuation.finish() + } receiveValue: { operation in + continuation.yield(operation) + }.store(in: &cancellables) + }, + shouldTryNextOnError: shouldTryNextOnError + ) + self.cancellables = cancellables + } + + /// Synchronous version of executing the operations + func execute() -> Future { + Future { [weak self] promise in + self?.task = Task { [weak self] in + do { + if let self { + promise(.success(try await self.run())) + } else { + promise(.failure(NondeterminsticOperationError.cancelled)) + } + } catch { + promise(.failure(error)) + } + } + } + } + + /// Asynchronous version of executing the operations + func run() async throws -> T { + for await operation in operations { + if Task.isCancelled { + throw NondeterminsticOperationError.cancelled + } + do { + return try await operation() + } catch { + if shouldTryNextOnError(error) { + continue + } else { + throw error + } + } + } + throw NondeterminsticOperationError.totalFailure + } + + /// Cancel the operation + func cancel() { + task?.cancel() + cancellables = Set() + } +} diff --git a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift index 0507431db1..cc4791f5f3 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -13,80 +13,56 @@ import Combine public final class RetryableGraphQLOperation { public typealias Payload = Payload - public let requestFactory: AsyncStream<() -> GraphQLRequest> - public weak var api: APICategoryGraphQLBehavior? - private var task: Task? - private var cancellables = Set() - - public init( - requestFactory: T, - api: APICategoryGraphQLBehavior - ) where T.Element == () -> GraphQLRequest { - self.requestFactory = requestFactory.asyncStream - self.api = api - } + private let nondeterminsticOperation: NondeterminsticOperation.Success> - public convenience init( - requestStream: AnyPublisher<() -> GraphQLRequest, Never>, - api: APICategoryGraphQLBehavior + public init( + requestStream: AnyPublisher<() async throws -> GraphQLTask.Success, Never> ) { - var cancellables = Set() - self.init(requestFactory: AsyncStream { continuation in - requestStream.sink { completion in - continuation.finish() - } receiveValue: { value in - continuation.yield(value) - }.store(in: &cancellables) - }, api: api) - self.cancellables = cancellables + self.nondeterminsticOperation = NondeterminsticOperation( + operationStream: requestStream, + shouldTryNextOnError: Self.onError(_:) + ) } deinit { cancel() } - public func execute( - _ operationType: GraphQLOperationType - ) -> Future.Success, APIError> { - Future() { promise in - self.task = Task { promise(await self.run(operationType)) } + static func onError(_ error: Error) -> Bool { + guard let error = error as? APIError, + let authError = error.underlyingError as? AuthError + else { + return false } - } - public func run(_ operationType: GraphQLOperationType) async -> Result.Success, APIError> { - for await request in requestFactory { - do { - try Task.checkCancellation() - switch (self.api, operationType) { - case (.some(let api), .query): - return .success(try await api.query(request: request())) - case (.some(let api), .mutation): - return .success(try await api.mutate(request: request())) - default: - return .failure(.operationError("Unable to run GraphQL operation with type \(operationType)", "")) - } - - } catch is CancellationError { - return .failure(.operationError("GraphQL operation cancelled", "")) - } catch { - guard let error = error as? APIError, - let authError = error.underlyingError as? AuthError - else { - return .failure(.operationError("Failed to send \(operationType) GraphQL request", "", error)) - } + switch authError { + case .signedOut, .notAuthorized: return true + default: return false + } + } - switch authError { - case .signedOut, .notAuthorized: break - default: return .failure(error) - } + public func execute( + _ operationType: GraphQLOperationType + ) -> AnyPublisher.Success, APIError> { + nondeterminsticOperation.execute().mapError { + if let apiError = $0 as? APIError { + return apiError + } else { + return APIError.operationError("Failed to execute GraphQL operation", "", $0) } + }.eraseToAnyPublisher() + } + + public func run() async -> Result.Success, APIError> { + do { + return .success(try await nondeterminsticOperation.run()) + } catch { + return .failure(.operationError("Failed to execute GraphQL operation", "", error)) } - return .failure(APIError.operationError("Failed to execute GraphQL operation \(operationType)", "", nil)) } public func cancel() { - task?.cancel() - cancellables = Set() + nondeterminsticOperation.cancel() } } @@ -94,69 +70,45 @@ public final class RetryableGraphQLOperation { public final class RetryableGraphQLSubscriptionOperation { public typealias Payload = Payload + public typealias SubscriptionEvents = GraphQLSubscriptionEvent + private var task: Task? + private let nondeterminsticOperation: NondeterminsticOperation> - public let requestFactory: AsyncStream<() async -> GraphQLRequest> - public weak var api: APICategoryGraphQLBehavior? - private var task: Task? - private var cancellables = Set() - - public init( - requestFactory: T, - api: APICategoryGraphQLBehavior - ) where T.Element == () async -> GraphQLRequest { - self.requestFactory = requestFactory.asyncStream - self.api = api - } - - public convenience init( - requestStream: AnyPublisher<() async -> GraphQLRequest, Never>, - api: APICategoryGraphQLBehavior + public init( + requestStream: AnyPublisher<() async throws -> AmplifyAsyncThrowingSequence, Never> ) { - var cancellables = Set() - self.init(requestFactory: AsyncStream { continuation in - requestStream.sink { completion in - continuation.finish() - } receiveValue: { value in - continuation.yield(value) - }.store(in: &cancellables) - }, api: api) - self.cancellables = cancellables + self.nondeterminsticOperation = NondeterminsticOperation(operationStream: requestStream) } deinit { cancel() } - public func subscribe() -> AnyPublisher, APIError> { - let subject = PassthroughSubject, APIError>() + public func subscribe() -> AnyPublisher { + let subject = PassthroughSubject() self.task = Task { await self.trySubscribe(subject) } return subject.eraseToAnyPublisher() } - private func trySubscribe(_ subject: PassthroughSubject, APIError>) async { + private func trySubscribe(_ subject: PassthroughSubject) async { var apiError: APIError? - for await request in requestFactory { - guard let sequence = self.api?.subscribe(request: await request()) else { - continue - } - do { + do { + try Task.checkCancellation() + let sequence = try await self.nondeterminsticOperation.run() + defer { sequence.cancel() } + for try await event in sequence { try Task.checkCancellation() - - for try await event in sequence { - try Task.checkCancellation() - Self.log.debug("Subscribe event \(event)") - subject.send(event) - } - } catch is CancellationError { - subject.send(completion: .finished) - } catch { - if let error = error as? APIError { - apiError = error - } - Self.log.debug("Failed with subscription request: \(error)") + subject.send(event) } - sequence.cancel() + } catch is CancellationError { + subject.send(completion: .finished) + } catch { + if let error = error as? APIError { + apiError = error + } + Self.log.debug("Failed with subscription request: \(error)") } + if apiError != nil { subject.send(completion: .failure(apiError!)) } else { @@ -166,7 +118,7 @@ public final class RetryableGraphQLSubscriptionOperation { public func cancel() { self.task?.cancel() - self.cancellables = Set() + self.nondeterminsticOperation.cancel() } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift index 6cf1e6b77a..0365b91862 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift @@ -173,25 +173,23 @@ final class InitialSyncOperation: AsynchronousOperation { .publisher() .map { Optional.some($0) } // map to optional to have nil as element .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided - .map { authType in { - GraphQLRequest.syncQuery( + .map { authType in { [weak self] in + guard let self, let api = self.api else { + throw APIError.operationError("Operation cancelled", "") + } + + return try await api.query(request: GraphQLRequest.syncQuery( modelSchema: self.modelSchema, where: self.syncPredicate, limit: limit, nextToken: nextToken, lastSync: lastSyncTime, authType: authType - ) + )) }} .eraseToAnyPublisher() - - let result: Result, APIError> = await RetryableGraphQLOperation( - requestStream: authTypes, - api: api - ).run(.query) - - switch result { + switch await RetryableGraphQLOperation(requestStream: authTypes).run() { case .success(let graphQLResult): await handleQueryResults(lastSyncTime: lastSyncTime, graphQLResult: graphQLResult) case .failure(let apiError): diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift index 81171e8f90..a342d12c00 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift @@ -127,18 +127,21 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { requestStream: authTypeProvider.publisher() .map { Optional.some($0) } // map to optional to have nil as element .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided - .map { authType in { - await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( + .map { authType in { [weak self] in + guard let self else { + throw APIError.operationError("GraphQL subscription cancelled", "") + } + + return api.subscribe(request: await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( for: modelSchema, subscriptionType: subscriptionType.subscriptionType, api: api, auth: auth, authType: authType, awsAuthService: self.awsAuthService - ) + )) }} - .eraseToAnyPublisher(), - api: api + .eraseToAnyPublisher() ) } diff --git a/AmplifyTests/CategoryTests/API/NondeterminsticOperationTests.swift b/AmplifyTests/CategoryTests/API/NondeterminsticOperationTests.swift new file mode 100644 index 0000000000..2923117710 --- /dev/null +++ b/AmplifyTests/CategoryTests/API/NondeterminsticOperationTests.swift @@ -0,0 +1,126 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import XCTest +@testable import Amplify + +class NondeterminsticOperationTests: XCTestCase { + enum TestError: Error { + case error + } + /** + Given: A nondeterminstic operation with all operation candidates would success + When: execute the nondeterminstic operation + Then: only first succeed operation will be executed + */ + func test_withAllSucceedOperations_onlyFirstOneExecuted() async throws { + let expectation1 = expectation(description: "opeartion 1 executed") + let operation1: () async throws -> Void = { + expectation1.fulfill() + } + let expectation2 = expectation(description: "opeartion 2 executed") + expectation2.isInverted = true + let operation2: () async throws -> Void = { + expectation2.fulfill() + } + let expectation3 = expectation(description: "opeartion 3 executed") + expectation3.isInverted = true + let operation3: () async throws -> Void = { + expectation3.fulfill() + } + + let nondeterminsticOperation = NondeterminsticOperation(operations: AsyncStream { continuation in + for operation in [operation1, operation2, operation3] { + continuation.yield(operation) + } + continuation.finish() + }) + + try await nondeterminsticOperation.run() + await fulfillment(of: [expectation1, expectation2, expectation3], timeout: 0.2) + } + + /** + Given: A nondeterminstic operation with all operation candidates would fail + When: execute the nondeterminstic operation + Then: a totoal failure error is throwed and all operations are executed + */ + func test_withAllFailedOperations_throwsTotoalFailureAndAllOperationsAreExecuted() async throws { + let expectation1 = expectation(description: "opeartion 1 executed") + let operation1: () async throws -> Void = { + expectation1.fulfill() + throw TestError.error + } + let expectation2 = expectation(description: "opeartion 2 executed") + let operation2: () async throws -> Void = { + expectation2.fulfill() + throw TestError.error + } + let expectation3 = expectation(description: "opeartion 3 executed") + let operation3: () async throws -> Void = { + expectation3.fulfill() + throw TestError.error + } + + let nondeterminsticOperation = NondeterminsticOperation(operations: AsyncStream { continuation in + for operation in [operation1, operation2, operation3] { + continuation.yield(operation) + } + continuation.finish() + }) + do { + try await nondeterminsticOperation.run() + } catch { + XCTAssert(error is NondeterminsticOperationError) + XCTAssertEqual(error as! NondeterminsticOperationError, NondeterminsticOperationError.totalFailure) + } + await fulfillment(of: [expectation1, expectation2, expectation3], timeout: 0.2) + } + + /** + Given: A nondeterminstic operation with some operation candidates would succeed + When: execute the nondeterminstic operation + Then: all operations until the first success operation will be executed + */ + func test_withSomeSuccessOperations_AllOperationsUntilSuccessOperationAreExecuted() async throws { + let expectation1 = expectation(description: "opeartion 1 executed") + let operation1: () async throws -> Void = { + expectation1.fulfill() + throw TestError.error + } + let expectation2 = expectation(description: "opeartion 2 executed") + let operation2: () async throws -> Void = { + expectation2.fulfill() + throw TestError.error + } + let expectation3 = expectation(description: "opeartion 3 executed") + let operation3: () async throws -> Void = { + expectation3.fulfill() + } + let expectation4 = expectation(description: "opeartion executed") + expectation4.isInverted = true + let operation4: () async throws -> Void = { + expectation4.fulfill() + throw TestError.error + } + + let nondeterminsticOperation = NondeterminsticOperation(operations: AsyncStream { continuation in + for operation in [operation1, operation2, operation3, operation4] { + continuation.yield(operation) + } + continuation.finish() + }) + do { + try await nondeterminsticOperation.run() + } catch { + XCTAssert(error is NondeterminsticOperationError) + XCTAssertEqual(error as! NondeterminsticOperationError, NondeterminsticOperationError.totalFailure) + } + await fulfillment(of: [expectation1, expectation2, expectation3, expectation4], timeout: 0.2) + } +} diff --git a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift index c76e6ec0cd..897e7303c3 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -7,150 +7,94 @@ import Foundation import XCTest - +import Combine @testable import Amplify @testable import AmplifyTestCommon class RetryableGraphQLOperationTests: XCTestCase { let testApiName = "apiName" - /// Given: a RetryableGraphQLOperation with a maxRetries of 2 - /// When: the request fails the first attempt with a .signedOut error - /// Then: the request is re-tried and resultListener called - func testShouldRetryOperation() { - let maxRetries = 2 - var attempt = 0 - - let requestFactoryExpectation = expectation(description: "Retry factory called \(maxRetries) times") - requestFactoryExpectation.expectedFulfillmentCount = maxRetries - let resultExpectation = expectation(description: "Result called") - - let resultListener: ResultListener = { _ in - resultExpectation.fulfill() + /// Given: a RetryableGraphQLOperation with 2 operations + /// When: the first one fails with a .signedOut error, the next one succeed with response + /// Then: return the success response + func testShouldRetryOperationWithSignedOutAuthError() async throws { + let expectation1 = expectation(description: "Operation 1 throws signed out auth error") + let operation1: () async throws -> GraphQLResponse = { + expectation1.fulfill() + throw APIError.operationError("", "", AuthError.signedOut("", "")) } - let requestFactory: RequestFactory = { - requestFactoryExpectation.fulfill() - return self.makeTestRequest() - + let expectation2 = expectation(description: "Operation 2 successfully finished") + let operation2: () async throws -> GraphQLResponse = { + expectation2.fulfill() + return .success("operation 2") } - let operation = RetryableGraphQLOperation(requestFactory: requestFactory, - maxRetries: maxRetries, - resultListener: resultListener) { _, wrappedListener in - - // simulate an error at first attempt - if attempt == 0 { - wrappedListener( - .failure(self.makeSignedOutAuthError()) - ) - } else { - wrappedListener(.success(.success(""))) - } - attempt += 1 - return self.makeTestOperation() + let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() + let result = await RetryableGraphQLOperation(requestStream: publisher).run() + if case .success(.success(let string)) = result { + XCTAssertEqual(string, "operation 2") + } else { + XCTFail("Wrong result") } - operation.main() - - wait(for: [requestFactoryExpectation, resultExpectation], timeout: 10) + await fulfillment(of: [expectation1, expectation2], timeout: 1) } - /// Given: a RetryableGraphQLOperation with a maxRetries of 1 - /// When: the request fails the first attempt with a .signedOut error - /// Then: the request is not re-tried - func testShouldNotRetryOperationWithMaxRetriesOne() { - let maxRetries = 1 - - let requestFactoryExpectation = expectation(description: "Retry factory called \(maxRetries) times") - requestFactoryExpectation.expectedFulfillmentCount = maxRetries - let resultExpectation = expectation(description: "Result called") - - let resultListener: ResultListener = { _ in - resultExpectation.fulfill() + /// Given: a RetryableGraphQLOperation with 2 operations + /// When: the first one fails with a .notAuthorized error, the next one succeed with response + /// Then: return the success response + func testShouldRetryOperationWithNotAuthorizedAuthError() async throws { + let expectation1 = expectation(description: "Operation 1 throws signed out auth error") + let operation1: () async throws -> GraphQLResponse = { + expectation1.fulfill() + throw APIError.operationError("", "", AuthError.notAuthorized("", "")) } - let requestFactory: RequestFactory = { - requestFactoryExpectation.fulfill() - return self.makeTestRequest() - + let expectation2 = expectation(description: "Operation 2 successfully finished") + let operation2: () async throws -> GraphQLResponse = { + expectation2.fulfill() + return .success("operation 2") } - let operation = RetryableGraphQLOperation(requestFactory: requestFactory, - maxRetries: maxRetries, - resultListener: resultListener) { _, wrappedListener in - - wrappedListener( - .failure(self.makeSignedOutAuthError()) - ) - return self.makeTestOperation() + let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() + let result = await RetryableGraphQLOperation(requestStream: publisher).run() + if case .success(.success(let string)) = result { + XCTAssertEqual(string, "operation 2") + } else { + XCTFail("Wrong result") } - operation.main() - - wait(for: [requestFactoryExpectation, resultExpectation], timeout: 10) + await fulfillment(of: [expectation1, expectation2], timeout: 1) } - /// Given: a RetryableGraphQLOperation with a maxRetries of 2 - /// When: the request fails both attempts - /// Then: the request is re-tried only twice and resultListener called - func testNotShouldRetryOperation() { - let maxRetries = 2 - - let requestFactoryExpectation = expectation(description: "Retry factory called \(maxRetries) times") - requestFactoryExpectation.expectedFulfillmentCount = maxRetries - let resultExpectation = expectation(description: "Result called") - - let resultListener: ResultListener = { _ in - resultExpectation.fulfill() + /// Given: a RetryableGraphQLOperation with 2 operations + /// When: the first one fails with a .notAuthorized error, the next one succeed with response + /// Then: return the success response + func testShouldNotRetryOperationWithUnknownError() async throws { + let expectation1 = expectation(description: "Operation 1 throws signed out auth error") + let operation1: () async throws -> GraphQLResponse = { + expectation1.fulfill() + throw APIError.unknown("~Unknown~", "") } - let requestFactory: RequestFactory = { - requestFactoryExpectation.fulfill() - return self.makeTestRequest() - + let expectation2 = expectation(description: "Operation 2 successfully finished") + expectation2.isInverted = true + let operation2: () async throws -> GraphQLResponse = { + expectation2.fulfill() + return .success("operation 2") } - let operation = RetryableGraphQLOperation(requestFactory: requestFactory, - maxRetries: maxRetries, - resultListener: resultListener) { _, wrappedListener in - - // simulate an error for both attempts - wrappedListener( - .failure(self.makeSignedOutAuthError()) - ) - return self.makeTestOperation() + let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() + let result = await RetryableGraphQLOperation(requestStream: publisher).run() + if case .failure(.operationError(_, _, let error)) = result { + XCTAssert(error is APIError) + if case .unknown(let description, _, _) = error as! APIError { + XCTAssertEqual(description, "~Unknown~") + } else { + XCTFail("Wrong result") + } + } else { + XCTFail("Wrong result") } - operation.main() - - wait(for: [requestFactoryExpectation, resultExpectation], timeout: 10) - } -} - -// MARK: - Test helpers -extension RetryableGraphQLOperationTests { - private func makeTestRequest() -> GraphQLRequest { - GraphQLRequest(apiName: testApiName, - document: "", - responseType: Payload.self) - } - - private func makeTestOperation() -> GraphQLOperation { - let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) - let operationRequest = GraphQLOperationRequest(apiName: testApiName, - operationType: .subscription, - document: "", - responseType: Payload.self, - options: requestOptions) - return GraphQLOperation(categoryType: .dataStore, - eventName: "eventName", - request: operationRequest) + await fulfillment(of: [expectation1, expectation2], timeout: 0.3) } - - func makeSignedOutAuthError() -> APIError { - return APIError.operationError("Error", "", AuthError.signedOut("AuthError", "")) - } - - /// Convenience type alias - private typealias Payload = String - private typealias ResultListener = RetryableGraphQLOperation.OperationResultListener - private typealias RequestFactory = RetryableGraphQLOperation.RequestFactory } From ccbfd660bcdb7955d64ddbd16d41b9d3e7b8a258 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 15 Apr 2024 23:45:35 -0700 Subject: [PATCH 06/23] fix unit test cases --- .../Operation/RetryableGraphQLOperation.swift | 11 +- ...tialSyncOperationSyncExpressionTests.swift | 38 +- .../InitialSyncOperationTests.swift | 89 +- .../InitialSyncOrchestratorTests.swift | 46 +- .../OutgoingMutationQueueNetworkTests.swift | 41 +- .../OutgoingMutationQueueTests.swift | 40 +- ...tationQueueTestsWithMockStateMachine.swift | 75 +- ...MutationErrorFromCloudOperationTests.swift | 2558 ++++++++--------- .../SyncMutationToCloudOperationTests.swift | 966 +++---- .../ModelReconciliationDeleteTests.swift | 47 +- .../Mocks/MockAPICategoryPlugin.swift | 170 +- .../Mocks/MockAPIResponders.swift | 34 +- .../Mocks/MockDataStoreCategoryPlugin.swift | 104 +- AmplifyTestCommon/Mocks/MockResponder.swift | 16 + .../API/RetryableGraphQLOperationTests.swift | 50 +- 15 files changed, 2169 insertions(+), 2116 deletions(-) diff --git a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift index cc4791f5f3..f40229d9f9 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -36,7 +36,7 @@ public final class RetryableGraphQLOperation { } switch authError { - case .signedOut, .notAuthorized: return true + case .notAuthorized: return true default: return false } } @@ -55,9 +55,14 @@ public final class RetryableGraphQLOperation { public func run() async -> Result.Success, APIError> { do { - return .success(try await nondeterminsticOperation.run()) + let result = try await nondeterminsticOperation.run() + return .success(result) } catch { - return .failure(.operationError("Failed to execute GraphQL operation", "", error)) + if let apiError = error as? APIError { + return .failure(apiError) + } else { + return .failure(.operationError("Failed to execute GraphQL operation", "", error)) + } } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationSyncExpressionTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationSyncExpressionTests.swift index d2e0d60242..cc7e5e9144 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationSyncExpressionTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationSyncExpressionTests.swift @@ -15,7 +15,7 @@ import Combine @testable import AWSPluginsCore class InitialSyncOperationSyncExpressionTests: XCTestCase { - typealias APIPluginQueryResponder = QueryRequestListenerResponder> + typealias APIPluginQueryResponder = QueryRequestResponder> var storageAdapter: StorageEngineAdapter! var apiPlugin = MockAPICategoryPlugin() @@ -36,7 +36,7 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { func initialSyncOperation(withSyncExpression syncExpression: DataStoreSyncExpression, responder: APIPluginQueryResponder) -> InitialSyncOperation { - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder #if os(watchOS) let configuration = DataStoreConfiguration.custom(syncPageSize: 10, syncExpressions: [syncExpression], @@ -55,7 +55,7 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { } func testBaseQueryWithBasicSyncExpression() throws { - let responder = APIPluginQueryResponder { request, listener in + let responder = APIPluginQueryResponder { request in XCTAssertEqual(request.document, """ query SyncMockSynceds($filter: ModelMockSyncedFilterInput, $limit: Int) { syncMockSynceds(filter: $filter, limit: $limit) { @@ -73,28 +73,26 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { """) guard let filter = request.variables?["filter"] as? [String: Any?] else { XCTFail("Unable to get filter") - return nil + return .failure(.unknown("Unable to get filter", "", nil)) } guard let group = filter["and"] as? [[String: Any?]] else { XCTFail("Unable to find 'and' group") - return nil + return .failure(.unknown("Unable to find 'and' group", "", nil)) } guard let key = group[0]["id"] as? [String: Any?] else { XCTFail("Unable to get id from filter") - return nil + return .failure(.unknown("Unable to get id from filter", "", nil)) } guard let value = key["eq"] as? String else { XCTFail("Unable to get eq from key") - return nil + return .failure(.unknown("Unable to get eq from key", "", nil)) } XCTAssertEqual(value, "id-123") let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) self.apiWasQueried.fulfill() - return nil + return .success(list) } let syncExpression = DataStoreSyncExpression.syncExpression(MockSynced.schema, where: { @@ -127,7 +125,7 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { } func testBaseQueryWithFilterSyncExpression() throws { - let responder = APIPluginQueryResponder { request, listener in + let responder = APIPluginQueryResponder { request in XCTAssertEqual(request.document, """ query SyncMockSynceds($filter: ModelMockSyncedFilterInput, $limit: Int) { syncMockSynceds(filter: $filter, limit: $limit) { @@ -145,28 +143,26 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { """) guard let filter = request.variables?["filter"] as? [String: Any?] else { XCTFail("Unable to get filter") - return nil + return .failure(.unknown("Unable to get filter", "", nil)) } guard let group = filter["or"] as? [[String: Any?]] else { XCTFail("Unable to find 'or' group") - return nil + return .failure(.unknown("Unable to find 'or' group", "", nil)) } guard let key = group[0]["id"] as? [String: Any?] else { XCTFail("Unable to get id from filter") - return nil + return .failure(.unknown("Unable to get id from filter", "", nil)) } guard let value = key["eq"] as? String else { XCTFail("Unable to get eq from key") - return nil + return .failure(.unknown("Unable to get eq from key", "", nil)) } XCTAssertEqual(value, "id-123") let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) self.apiWasQueried.fulfill() - return nil + return .success(list) } let syncExpression = DataStoreSyncExpression.syncExpression(MockSynced.schema, where: { @@ -199,7 +195,7 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { } func testBaseQueryWithSyncExpressionConstantAll() throws { - let responder = APIPluginQueryResponder { request, listener in + let responder = APIPluginQueryResponder { request in XCTAssertEqual(request.document, """ query SyncMockSynceds($limit: Int) { syncMockSynceds(limit: $limit) { @@ -218,10 +214,8 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { XCTAssertNil(request.variables?["filter"]) let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) self.apiWasQueried.fulfill() - return nil + return .success(list) } let syncExpression = DataStoreSyncExpression.syncExpression(MockSynced.schema, where: { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift index d1a8a464aa..58bb5c4bea 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationTests.swift @@ -247,16 +247,14 @@ class InitialSyncOperationTests: XCTestCase { /// - Then: /// - It reads sync metadata from storage func testReadsMetadata() { - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startDateMilliseconds = Int64(Date().timeIntervalSince1970) * 1_000 let list = PaginatedList(items: [], nextToken: nil, startedAt: startDateMilliseconds) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() let metadataQueryReceived = expectation(description: "Metadata query received by storage adapter") @@ -308,17 +306,15 @@ class InitialSyncOperationTests: XCTestCase { /// - It performs a sync query against the API category func testQueriesAPI() { let apiWasQueried = expectation(description: "API was queried for a PaginatedList of AnyModel") - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startDateMilliseconds = Int64(Date().timeIntervalSince1970) * 1_000 let list = PaginatedList(items: [], nextToken: nil, startedAt: startDateMilliseconds) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) apiWasQueried.fulfill() - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -366,16 +362,14 @@ class InitialSyncOperationTests: XCTestCase { /// - Then: /// - The method invokes a completion callback when complete func testInvokesPublisherCompletion() { - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startDateMilliseconds = Int64(Date().timeIntervalSince1970) * 1_000 let list = PaginatedList(items: [], nextToken: nil, startedAt: startDateMilliseconds) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -421,18 +415,16 @@ class InitialSyncOperationTests: XCTestCase { var nextTokens = ["token1", "token2"] - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startedAt = Int64(Date().timeIntervalSince1970) let nextToken = nextTokens.isEmpty ? nil : nextTokens.removeFirst() let list = PaginatedList(items: [], nextToken: nextToken, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) apiWasQueried.fulfill() - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -482,15 +474,13 @@ class InitialSyncOperationTests: XCTestCase { lastChangedAt: Int64(Date().timeIntervalSince1970), version: 1) let mutationSync = MutationSync(model: anyModel, syncMetadata: metadata) - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let list = PaginatedList(items: [mutationSync], nextToken: nil, startedAt: startedAtMilliseconds) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -551,16 +541,14 @@ class InitialSyncOperationTests: XCTestCase { /// - The method submits the returned data to the reconciliation queue func testUpdatesSyncMetadata() throws { let startDateMilliseconds = Int64(Date().timeIntervalSince1970) * 1_000 - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startedAt = startDateMilliseconds let list = PaginatedList(items: [], nextToken: nil, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = try SQLiteStorageEngineAdapter(connection: Connection(.inMemory)) try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas + [MockSynced.schema]) @@ -614,17 +602,15 @@ class InitialSyncOperationTests: XCTestCase { /// - I invoke main() against an API that returns .signedOut error /// - Then: /// - The method completes with a failure result, error handler is called. - func testQueriesAPIReturnSignedOutError() throws { - let responder = QueryRequestListenerResponder> { _, listener in + func testQueriesAPIReturnSignedOutError() async throws { + let responder = QueryRequestResponder> { _ in let authError = AuthError.signedOut("", "", nil) let apiError = APIError.operationError("", "", authError) - let event: GraphQLOperation>.OperationResult = .failure(apiError) - listener?(event) - return nil + throw apiError } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = try SQLiteStorageEngineAdapter(connection: Connection(.inMemory)) @@ -704,7 +690,12 @@ class InitialSyncOperationTests: XCTestCase { operation.main() - waitForExpectations(timeout: 1) + await fulfillment(of: [ + expectErrorHandlerCalled, + syncStartedReceived, + syncCompletionReceived, + finishedReceived + ], timeout: 1) sink.cancel() } @@ -734,19 +725,17 @@ class InitialSyncOperationTests: XCTestCase { wait(for: [syncMetadataSaved], timeout: 1.0) let apiWasQueried = expectation(description: "API was queried for a PaginatedList of AnyModel") - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in let lastSync = request.variables?["lastSync"] as? Int64 XCTAssertEqual(lastSync, startDateMilliseconds) let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) apiWasQueried.fulfill() - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let reconciliationQueue = MockReconciliationQueue() let operation = InitialSyncOperation( @@ -805,19 +794,17 @@ class InitialSyncOperationTests: XCTestCase { wait(for: [syncMetadataSaved], timeout: 1.0) let apiWasQueried = expectation(description: "API was queried for a PaginatedList of AnyModel") - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in let lastSync = request.variables?["lastSync"] as? Int XCTAssertNil(lastSync) let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) apiWasQueried.fulfill() - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let reconciliationQueue = MockReconciliationQueue() #if os(watchOS) @@ -866,7 +853,7 @@ class InitialSyncOperationTests: XCTestCase { try storageAdapter.setUp(modelSchemas: StorageEngine.systemModelSchemas + [MockSynced.schema]) let apiWasQueried = expectation(description: "API was queried for a PaginatedList of AnyModel") - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in let lastSync = request.variables?["lastSync"] as? Int XCTAssertNil(lastSync) XCTAssert(request.document.contains("limit: Int")) @@ -874,14 +861,12 @@ class InitialSyncOperationTests: XCTestCase { XCTAssertEqual(10, limitValue) let list = PaginatedList(items: [], nextToken: nil, startedAt: nil) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) apiWasQueried.fulfill() - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let reconciliationQueue = MockReconciliationQueue() #if os(watchOS) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift index fbe3f5b6af..429e955cf6 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOrchestratorTests.swift @@ -27,16 +27,14 @@ class InitialSyncOrchestratorTests: XCTestCase { func testInvokesCompletionCallback() async throws { ModelRegistry.reset() PostCommentModelRegistration().registerModels(registry: ModelRegistry.self) - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startedAt = Int64(Date().timeIntervalSince1970) let list = PaginatedList(items: [], nextToken: nil, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -120,23 +118,19 @@ class InitialSyncOrchestratorTests: XCTestCase { func testFinishWithAPIError() async throws { ModelRegistry.reset() PostCommentModelRegistration().registerModels(registry: ModelRegistry.self) - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in if request.document.contains("SyncPosts") { - let event: GraphQLOperation>.OperationResult = - .failure(APIError.operationError("", "", nil)) - listener?(event) + throw APIError.operationError("", "", nil) } else if request.document.contains("SyncComments") { let startedAt = Int64(Date().timeIntervalSince1970) let list = PaginatedList(items: [], nextToken: nil, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) + return .success(list) } - - return nil + return .failure(.unknown("", "", nil)) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -238,16 +232,14 @@ class InitialSyncOrchestratorTests: XCTestCase { } TestModelsWithNoAssociations().registerModels(registry: ModelRegistry.self) - let responder = QueryRequestListenerResponder> { _, listener in + let responder = QueryRequestResponder> { _ in let startedAt = Int64(Date().timeIntervalSince1970) let list = PaginatedList(items: [], nextToken: nil, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -297,7 +289,7 @@ class InitialSyncOrchestratorTests: XCTestCase { PostCommentModelRegistration().registerModels(registry: ModelRegistry.self) let postWasQueried = expectation(description: "Post was queried") let commentWasQueried = expectation(description: "Comment was queried") - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in if request.document.hasPrefix("query SyncPosts") { postWasQueried.fulfill() } @@ -308,13 +300,11 @@ class InitialSyncOrchestratorTests: XCTestCase { let startedAt = Int64(Date().timeIntervalSince1970) let list = PaginatedList(items: [], nextToken: nil, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) @@ -371,7 +361,7 @@ class InitialSyncOrchestratorTests: XCTestCase { var nextTokens = Array(repeating: "token", count: pageCount - 1) - let responder = QueryRequestListenerResponder> { request, listener in + let responder = QueryRequestResponder> { request in if request.document.hasPrefix("query SyncPosts") { postWasQueried.fulfill() } @@ -383,13 +373,11 @@ class InitialSyncOrchestratorTests: XCTestCase { let startedAt = Int64(Date().timeIntervalSince1970) let nextToken = nextTokens.isEmpty ? nil : nextTokens.removeFirst() let list = PaginatedList(items: [], nextToken: nextToken, startedAt: startedAt) - let event: GraphQLOperation>.OperationResult = .success(.success(list)) - listener?(event) - return nil + return .success(list) } let apiPlugin = MockAPICategoryPlugin() - apiPlugin.responders[.queryRequestListener] = responder + apiPlugin.responders[.queryRequestResponse] = responder let storageAdapter = MockSQLiteStorageEngineAdapter() storageAdapter.returnOnQueryModelSyncMetadata(nil) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift index 035918e8bf..f5ccca5161 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift @@ -113,7 +113,7 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { ) // Start by accepting the initial "create" mutation - apiPlugin.responders = [.mutateRequestListener: acceptInitialMutation] + apiPlugin.responders = [.mutateRequestResponse: acceptInitialMutation] try await startAmplifyAndWaitForSync() @@ -129,7 +129,7 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { // Set the responder to reject the mutation. Make sure to push a retry advice before sending // a new mutation. - apiPlugin.responders = [.mutateRequestListener: rejectMutationsWithRetriableError] + apiPlugin.responders = [.mutateRequestResponse: rejectMutationsWithRetriableError] // NOTE: This policy is not used by the SyncMutationToCloudOperation, only by the // RemoteSyncEngine. @@ -248,7 +248,7 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { fulfillingWhenNetworkAvailableAgain: networkAvailableAgain ) - apiPlugin.responders = [.mutateRequestListener: acceptSubsequentMutations] + apiPlugin.responders = [.mutateRequestResponse: acceptSubsequentMutations] reachabilitySubject.send(ReachabilityUpdate(isOnline: true)) await fulfillment(of: [networkAvailableAgain, syncStarted, expectedFinalContentReceived, outboxEmpty], timeout: 5.0) @@ -260,8 +260,8 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { for model: AnyModel, fulfilling expectation: XCTestExpectation, incrementing version: AtomicValue - ) -> MutateRequestListenerResponder> { - MutateRequestListenerResponder> { _, eventListener in + ) -> MutateRequestResponder> { + MutateRequestResponder> { _ in let mockResponse = MutationSync( model: model, syncMetadata: MutationSyncMetadata( @@ -273,24 +273,19 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { ) ) - DispatchQueue.global().async { - eventListener?(.success(.success(mockResponse))) - expectation.fulfill() - } - - return nil + try! await Task.sleep(seconds: 0.01) + expectation.fulfill() + return .success(mockResponse) } } /// Returns a responder that executes the eventListener after a delay, to simulate network lag private func setUpRetriableErrorRequestResponder( listenerDelay: TimeInterval - ) -> MutateRequestListenerResponder> { - MutateRequestListenerResponder> { _, eventListener in - DispatchQueue.global().asyncAfter(deadline: .now() + listenerDelay) { - eventListener?(.failure(self.connectionError)) - } - return nil + ) -> MutateRequestResponder> { + MutateRequestResponder> { _ in + try! await Task.sleep(seconds: listenerDelay) + return .failure(.unknown("", "", self.connectionError)) } } @@ -299,12 +294,12 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { fulfilling expectation: XCTestExpectation, whenContentContains expectedFinalContent: String, incrementing version: AtomicValue - ) -> MutateRequestListenerResponder> { - MutateRequestListenerResponder> { request, eventListener in + ) -> MutateRequestResponder> { + MutateRequestResponder> { request in guard let input = request.variables?["input"] as? [String: Any], let content = input["content"] as? String else { XCTFail("Unexpected request structure: no `content` in variables.") - return nil + return .failure(.unknown("Unexpected request structure: no `content` in variables.", "", nil)) } let mockResponse = MutationSync( @@ -317,14 +312,12 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { version: version.increment() ) ) - - eventListener?(.success(.success(mockResponse))) - + if content == expectedFinalContent { expectation.fulfill() } - return nil + return .success(mockResponse) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift index 41f3244351..915c493072 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift @@ -28,10 +28,22 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { dataStoreConfiguration: .testDefault(), authModeStrategy: AWSDefaultAuthModeStrategy())) } + let post = Post(title: "Post title", content: "Post content", createdAt: .now()) + apiPlugin.responders[.mutateRequestResponse] = MutateRequestResponder { request in + let anyModel = try! post.eraseToAnyModel() + let remoteSyncMetadata = MutationSyncMetadata(modelId: post.id, + modelName: Post.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) + } + let outboxStatusReceivedCurrentCount = AtomicValue(initialValue: 0) let outboxStatusOnStart = expectation(description: "On DataStore start, outboxStatus received") let outboxStatusOnMutationEnqueued = expectation(description: "Mutation enqueued, outboxStatus received") @@ -48,12 +60,17 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { return } - if outboxStatusReceivedCurrentCount.get() == 1 { + switch outboxStatusReceivedCurrentCount.get() { + case 1: XCTAssertTrue(outboxStatusEvent.isEmpty) outboxStatusOnStart.fulfill() - } else { + case 2: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnMutationEnqueued.fulfill() + case 3: + XCTAssertTrue(outboxStatusEvent.isEmpty) + default: + XCTFail("Should not trigger outbox status event") } } @@ -163,13 +180,28 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { return } - if outboxStatusReceivedCurrentCount == 1 { + switch outboxStatusReceivedCurrentCount { + case 1: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnStart.fulfill() - } else { + case 2: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnMutationEnqueued.fulfill() + case 3, 4: + XCTAssertFalse(outboxStatusEvent.isEmpty) + case 5: + XCTAssertTrue(outboxStatusEvent.isEmpty) + default: + XCTFail("Should not trigger outbox status event") } + +// if outboxStatusReceivedCurrentCount == 1 { +// XCTAssertFalse(outboxStatusEvent.isEmpty) +// outboxStatusOnStart.fulfill() +// } else { +// XCTAssertFalse(outboxStatusEvent.isEmpty) +// outboxStatusOnMutationEnqueued.fulfill() +// } } guard try await HubListenerTestUtilities.waitForListener(with: hubListener, timeout: 5.0) else { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift index d8442e9b96..0ffc4fb99e 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift @@ -69,14 +69,14 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { waitForExpectations(timeout: 1) } - func testRequestingEvent_subscriptionSetup() throws { + func testRequestingEvent_subscriptionSetup() async throws { let receivedSubscription = expectation(description: "state machine received receivedSubscription") stateMachine.pushExpectActionCriteria { action in XCTAssertEqual(action, OutgoingMutationQueue.Action.receivedSubscription) receivedSubscription.fulfill() } stateMachine.state = .starting(apiBehavior, publisher, reconciliationQueue) - wait(for: [receivedSubscription], timeout: 1.0) + await fulfillment(of: [receivedSubscription], timeout: 1.0) let json = "{\"id\":\"1234\",\"title\":\"t\",\"content\":\"c\",\"createdAt\":\"2020-09-03T22:55:13.424Z\"}" let futureResult = MutationEvent(modelId: "1", @@ -92,17 +92,24 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { } let apiMutationReceived = expectation(description: "API call for mutate received") - var listenerFromRequest: GraphQLOperation>.ResultListener! - let responder = MutateRequestListenerResponder> { _, eventListener in - listenerFromRequest = eventListener + let responder = MutateRequestResponder> { _ in apiMutationReceived.fulfill() - return nil + try! await Task.sleep(seconds: 0.5) + let model = MockSynced(id: "id-1") + let anyModel = try! model.eraseToAnyModel() + let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, + modelName: MockSynced.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) } - apiBehavior.responders[.mutateRequestListener] = responder + apiBehavior.responders[.mutateRequestResponse] = responder stateMachine.state = .requestingEvent - wait(for: [enqueueEvent, apiMutationReceived], timeout: 1) + await fulfillment(of: [enqueueEvent, apiMutationReceived], timeout: 1) let processEvent = expectation(description: "state requestingEvent, processedEvent") stateMachine.pushExpectActionCriteria { action in @@ -110,17 +117,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { processEvent.fulfill() } - let model = MockSynced(id: "id-1") - let anyModel = try model.eraseToAnyModel() - let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, - modelName: MockSynced.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 2) - let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - listenerFromRequest(.success(.success(remoteMutationSync))) - - wait(for: [processEvent], timeout: 1) + await fulfillment(of: [processEvent], timeout: 1) } func testRequestingEvent_nosubscription() { @@ -135,7 +132,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { waitForExpectations(timeout: 1) } - func testReceivedStartActionWhileExpectingEventProcessedAction() throws { + func testReceivedStartActionWhileExpectingEventProcessedAction() async throws { // Ensure subscription is setup let receivedSubscription = expectation(description: "receivedSubscription") stateMachine.pushExpectActionCriteria { action in @@ -143,7 +140,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { receivedSubscription.fulfill() } stateMachine.state = .starting(apiBehavior, publisher, reconciliationQueue) - wait(for: [receivedSubscription], timeout: 0.1) + await fulfillment(of: [receivedSubscription], timeout: 0.1) // Mock incoming mutation event let post = Post(title: "title", @@ -160,16 +157,24 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { enqueueEvent.fulfill() } let mutateAPICallExpecation = expectation(description: "Call to api category for mutate") - var listenerFromRequest: GraphQLOperation>.ResultListener! - let responder = MutateRequestListenerResponder> { _, eventListener in - listenerFromRequest = eventListener + + let responder = MutateRequestResponder> { _ in mutateAPICallExpecation.fulfill() - return nil + try! await Task.sleep(seconds: 0.3) + let model = MockSynced(id: "id-1") + let anyModel = try! model.eraseToAnyModel() + let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, + modelName: MockSynced.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) } - apiBehavior.responders[.mutateRequestListener] = responder + apiBehavior.responders[.mutateRequestResponse] = responder stateMachine.state = .requestingEvent - wait(for: [enqueueEvent, mutateAPICallExpecation], timeout: 0.1) + await fulfillment(of: [enqueueEvent, mutateAPICallExpecation], timeout: 0.1) // While we are expecting the mutationEvent to be processed by making an API call, // stop the mutation queue. Note that we are not testing that the operation @@ -181,7 +186,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { mutationQueueStopped.fulfill() } mutationQueue.stopSyncingToCloud { } - wait(for: [mutationQueueStopped], timeout: 0.1) + await fulfillment(of: [mutationQueueStopped], timeout: 0.1) // Re-enable syncing let startReceivedAgain = expectation(description: "Start received again") @@ -196,7 +201,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { mutationEventPublisher: publisher, reconciliationQueue: reconciliationQueue) - wait(for: [startReceivedAgain], timeout: 1) + await fulfillment(of: [startReceivedAgain], timeout: 1) // After - enabling, mock the callback from API to be completed let processEvent = expectation(description: "state requestingEvent, processedEvent") @@ -205,17 +210,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { processEvent.fulfill() } - let model = MockSynced(id: "id-1") - let anyModel = try model.eraseToAnyModel() - let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, - modelName: MockSynced.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 2) - let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - listenerFromRequest(.success(.success(remoteMutationSync))) - - wait(for: [processEvent], timeout: 1) + await fulfillment(of: [processEvent], timeout: 1) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift index 2163442dca..402341d803 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift @@ -1,1280 +1,1280 @@ +//// +//// Copyright Amazon.com Inc. or its affiliates. +//// All Rights Reserved. +//// +//// SPDX-License-Identifier: Apache-2.0 +//// // -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation -import XCTest -import Combine - -@testable import Amplify -@testable import AmplifyTestCommon -@testable import AWSPluginsCore -@testable import AWSDataStorePlugin - -// swiftlint:disable type_body_length -// swiftlint:disable type_name -// swiftlint:disable file_length -class ProcessMutationErrorFromCloudOperationTests: XCTestCase { - // swiftlint:enable type_name - let defaultAsyncWaitTimeout = 10.0 - var mockAPIPlugin: MockAPICategoryPlugin! - var storageAdapter: StorageEngineAdapter! - var localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let queue = OperationQueue() - let reconciliationQueue = MockReconciliationQueue() - - override func setUp() async throws { - await tryOrFail { - try await setUpWithAPI() - } - storageAdapter = MockSQLiteStorageEngineAdapter() - - ModelRegistry.register(modelType: Post.self) - ModelRegistry.register(modelType: Comment.self) - } - - /// - Given: APIError - /// - When: - /// - APIError contains AuthError indicating user is not authenticated - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForAuthError() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - let authError = AuthError.signedOut("User is not authenticated", "Authenticate user", nil) - let apiError = APIError.operationError("not signed in", "Sign In User", authError) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let actualAPIError = amplifyError as? APIError, - case let .operationError(_, _, underlyingError) = actualAPIError, - let authError = underlyingError as? AuthError, - case .signedOut = authError else { - XCTFail("Should be `signedOut` error") - return - } - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - apiError: apiError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: APIError - /// - When: - /// - APIError unrelated to AuthError - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForAPIError() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - let apiError = APIError.operationError("Operation failed", "", nil) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let actualAPIError = amplifyError as? APIError, - case .operationError = actualAPIError else { - XCTFail("Missing APIError.operationError") - return - } - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - apiError: apiError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: GraphQLError with no errors - /// - When: - /// - GraphQLError with no errors - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrors() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.unknown("", "", nil) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .unknown = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: GraphQLError with no error - /// - When: - /// - GraphQLError with no error - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrorsArray() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.error([]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - XCTAssertEqual(errors.count, 0) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: GraphQLError more than one error to handle - /// - When: - /// - GraphQLError with multiple errors - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithMultipleErrors() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let error = GraphQLError(message: "error message") - let graphQLResponseError = GraphQLResponseError>.error([error, error]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - XCTAssertEqual(errors.count, 2) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: GraphQLError ConditionalCheck - /// - When: - /// - GraphQLError with errors containing type ConditionalCheck - /// - Then: - /// - `DataStoreErrorHandler` is called - func testProcessMutationErrorFromCloudOperationSuccessForConditionalCheck() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.conditionalCheck)]) - - let expectHubEvent = expectation(description: "Hub is notified") - let expectCompletion = expectation(description: "Expect to complete error processing") - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in - if payload.eventName == "DataStore.conditionalSaveFailed" { - expectHubEvent.fulfill() - } - } - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - XCTAssertEqual(errors.count, 1) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectHubEvent, expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - Amplify.Hub.removeListener(hubListener) - } - - func testProcessMutationErrorFromCloudOperationSuccessForUnauthorized() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unauthorized)]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - XCTAssertEqual(errors.count, 1) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - func testProcessMutationErrorFromCloudOperationSuccessForOperationDisabled() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.operationDisabled)]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError") - return - } - XCTAssertEqual(errors.count, 1) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - func testProcessMutationErrorFromCloudOperationSuccessForUnknownError() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unknown("unknownErrorType"))]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let configuration = custom(errorHandler: { error in - guard let dataStoreError = error as? DataStoreError, - case let .api(amplifyError, mutationEventOptional) = dataStoreError else { - XCTFail("Expected API error with mutationEvent") - return - } - guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, - case .error(let errors) = graphQLResponseError else { - XCTFail("Missing GraphQLResponseError.unknown") - return - } - XCTAssertEqual(errors.count, 1) - guard let actualMutationEvent = mutationEventOptional else { - XCTFail("Missing mutationEvent for api error") - return - } - XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) - expectErrorHandlerCalled.fulfill() - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - Error does not contain the remote model - /// - Then: - /// - Unexpected scenario, there should never be an conflict unhandled error without error.data - func testConflictUnhandledReturnsErrorForMissingRemoteModel() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) - let graphQLError = GraphQLError(message: "conflict unhandled", - extensions: ["errorType": .string(AppSyncErrorType.conflictUnhandled.rawValue)]) - let graphQLResponseError = GraphQLResponseError>.error([graphQLError]) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case let .failure(error) = result, - let dataStoreError = error as? DataStoreError, - case .unknown = dataStoreError else { - XCTFail("Should have failed with DataStoreError.unknown") - return - } - - XCTAssertEqual(dataStoreError.errorDescription, "Missing remote model from the response from AppSync.") - expectCompletion.fulfill() - } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `create` - /// - Then: - /// - Unexpected scenario, there should never get a conflict for create mutations - func testConflictUnhandledReturnsErrorForCreateMutation() throws { - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) - let remotePost = Post(title: "remoteTitle", content: "remoteContent", createdAt: .now()) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 1) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case let .failure(error) = result, - let dataStoreError = error as? DataStoreError, - case .unknown = dataStoreError else { - XCTFail("Should have failed with DataStoreError.unknown") - return - } - - XCTAssertEqual(dataStoreError.errorDescription, "Should never get conflict unhandled for create mutation") - expectCompletion.fulfill() - } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `delete`, remote model is deleted. - /// - Then: - /// - No-op, operation finishes successfully - func testConflictUnhandledForDeleteMutationAndDeletedRemoteModel() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: true, - version: 1) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retryLocal` - /// - Then: - /// - API is called to delete with local model - func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryLocal() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - - var eventListenerOptional: GraphQLOperation>.ResultListener? - let apiMutateCalled = expectation(description: "API was called") - mockAPIPlugin.responders[.mutateRequestListener] = - MutateRequestListenerResponder> { request, eventListener in - guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { - XCTFail("The document variables property doesn't contain a valid input") - return nil - } - XCTAssert(input["id"] as? String == localPost.id) - XCTAssert(request.document.contains("DeletePost")) - eventListenerOptional = eventListener - apiMutateCalled.fulfill() - return nil - } - - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let configuration = custom(conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.retryLocal) - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) - - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) - guard let eventListener = eventListenerOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, - modelName: remotePost.modelName, - deleted: true, - lastChangedAt: 0, - version: 3) - let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) - eventListener(.success(.success(mockResponse))) - - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retry(model)` - /// - Then: - /// - API is called with the model from the conflict handler result - func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryModel() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - self.assertSuccessfulNil(result) - expectCompletion.fulfill() - } - - let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) - var eventListenerOptional: GraphQLOperation>.ResultListener? - let apiMutateCalled = expectation(description: "API was called") - mockAPIPlugin.responders[.mutateRequestListener] = - MutateRequestListenerResponder> { request, eventListener in - guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { - XCTFail("The document variables property doesn't contain a valid input") - return nil - } - XCTAssert(input["title"] as? String == retryModel.title) - XCTAssertTrue(request.document.contains("UpdatePost")) - eventListenerOptional = eventListener - apiMutateCalled.fulfill() - return nil - } - - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let configuration = custom(conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.retry(retryModel)) - }) - - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) - - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) - guard let eventListener = eventListenerOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, - modelName: remotePost.modelName, - deleted: false, - lastChangedAt: 0, - version: 3) - let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) - eventListener(.success(.success(mockResponse))) - - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `delete`, remote model is an update, conflict handler returns `.applyRemote` - /// - Then: - /// - Local Store is reconciled(recreated) to remote model, result mutationEvent is `update` - func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsApplyRemote() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result, - let mutationEvent = mutationEventOptional else { - XCTFail("Should have been successful") - return - } - XCTAssertEqual(mutationEvent.mutationType, "update") - XCTAssertEqual(mutationEvent.modelId, remotePost.id) - expectCompletion.fulfill() - } - - let modelSavedEvent = expectation(description: "model saved event") - modelSavedEvent.expectedFulfillmentCount = 2 - let storageAdapter = MockSQLiteStorageEngineAdapter() - storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in - guard let savedPost = model as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - XCTAssertEqual(savedPost.title, remotePost.title) - modelSavedEvent.fulfill() - completion(.success(model)) - } - - storageAdapter.responders[.saveModelCompletion] = - SaveModelCompletionResponder { metadata, completion in - XCTAssertEqual(metadata.deleted, false) - XCTAssertEqual(metadata.version, 2) - modelSavedEvent.fulfill() - completion(.success(metadata)) - } - - let expectHubEvent = expectation(description: "Hub is notified") - let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in - if payload.eventName == "DataStore.syncReceived" { - expectHubEvent.fulfill() - } - } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - - wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - Amplify.Hub.removeListener(hubListener) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `update`, remote model is deleted - /// - Then: - /// - Local model is deleted, result mutationEvent is `delete` - func testConflictUnhandledForUpdateMutationAndDeletedRemoteModel() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: true, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result, - let mutationEvent = mutationEventOptional else { - XCTFail("Should have been successful") - return - } - XCTAssertEqual(mutationEvent.mutationType, "delete") - XCTAssertEqual(mutationEvent.modelId, localPost.id) - expectCompletion.fulfill() - } - - let modelDeletedEvent = expectation(description: "model deleted event") - let metadataSavedEvent = expectation(description: "metadata saved event") - let storageAdapter = MockSQLiteStorageEngineAdapter() - storageAdapter.shouldReturnErrorOnDeleteMutation = false - storageAdapter.responders[.deleteUntypedModel] = DeleteUntypedModelCompletionResponder { _ in - modelDeletedEvent.fulfill() - return .emptyResult - } - storageAdapter.responders[.saveModelCompletion] = - SaveModelCompletionResponder { metadata, completion in - XCTAssertEqual(metadata.deleted, true) - XCTAssertEqual(metadata.version, 2) - metadataSavedEvent.fulfill() - completion(.success(metadata)) - } - - let expectHubEvent = expectation(description: "Hub is notified") - let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in - if payload.eventName == "DataStore.syncReceived" { - expectHubEvent.fulfill() - } - } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - - queue.addOperation(operation) - - wait(for: [modelDeletedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - Amplify.Hub.removeListener(hubListener) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `update`, remote model is an update, conflict handler returns `.applyRemote` - /// - Then: - /// - Local model is updated with remote model data, result mutationEvent is `update` - func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsApplyRemote() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result, - let mutationEvent = mutationEventOptional else { - XCTFail("Should have been successful") - return - } - XCTAssertEqual(mutationEvent.mutationType, "update") - XCTAssertEqual(mutationEvent.modelId, remotePost.id) - expectCompletion.fulfill() - } - - let storageAdapter = MockSQLiteStorageEngineAdapter() - let modelSavedEvent = expectation(description: "model saved event") - let metadataSavedEvent = expectation(description: "metadata saved event") - storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in - guard let savedPost = model as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - XCTAssertEqual(savedPost.title, remotePost.title) - modelSavedEvent.fulfill() - completion(.success(model)) - } - storageAdapter.responders[.saveModelCompletion] = - SaveModelCompletionResponder { metadata, completion in - XCTAssertEqual(metadata.deleted, false) - XCTAssertEqual(metadata.version, 2) - metadataSavedEvent.fulfill() - completion(.success(metadata)) - } - - let expectHubEvent = expectation(description: "Hub is notified") - let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in - if payload.eventName == "DataStore.syncReceived" { - expectHubEvent.fulfill() - } - } - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let configuration = custom(conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.applyRemote) - }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - Amplify.Hub.removeListener(hubListener) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` - /// - Then: - /// - API is called to update with the local model - func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryLocal() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result else { - XCTFail("Should have been successful") - return - } - XCTAssertNil(mutationEventOptional) - expectCompletion.fulfill() - } - - var eventListenerOptional: GraphQLOperation>.ResultListener? - let apiMutateCalled = expectation(description: "API was called") - mockAPIPlugin.responders[.mutateRequestListener] = - MutateRequestListenerResponder> { request, eventListener in - guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { - XCTFail("The document variables property doesn't contain a valid input") - return nil - } - XCTAssert(input["title"] as? String == localPost.title) - XCTAssertTrue(request.document.contains("UpdatePost")) - eventListenerOptional = eventListener - apiMutateCalled.fulfill() - return nil - } - - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let configuration = custom(conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.retryLocal) - }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) - - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) - guard let eventListener = eventListenerOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, - modelName: remotePost.modelName, - deleted: false, - lastChangedAt: 0, - version: 3) - let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) - eventListener(.success(.success(mockResponse))) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `update`, remote model is an update, conflict handler returns `.retry(Model)` - /// - Then: - /// - API is called to update the model from the conflict handler result - func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryModel() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result else { - XCTFail("Should have been successful") - return - } - XCTAssertNil(mutationEventOptional) - expectCompletion.fulfill() - } - - let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) - var eventListenerOptional: GraphQLOperation>.ResultListener? - let apiMutateCalled = expectation(description: "API was called") - mockAPIPlugin.responders[.mutateRequestListener] = - MutateRequestListenerResponder> { request, eventListener in - guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { - XCTFail("The document variables property doesn't contain a valid input") - return nil - } - XCTAssert(input["title"] as? String == retryModel.title) - XCTAssertTrue(request.document.contains("UpdatePost")) - eventListenerOptional = eventListener - apiMutateCalled.fulfill() - return nil - } - - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let configuration = custom(conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.retry(retryModel)) - }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) - guard let eventListener = eventListenerOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, - modelName: remotePost.modelName, - deleted: false, - lastChangedAt: 0, - version: 3) - let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) - eventListener(.success(.success(mockResponse))) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// - Given: Conflict Unhandled error - /// - When: - /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` - /// - API is called to update with local model and response contains error - /// - Then: - /// - `DataStoreErrorHandler` is called - func testConflictUnhandledSyncToCloudReturnsError() throws { - let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) - guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, - deleted: false, - version: 2) else { - XCTFail("Couldn't get GraphQL response with remote post") - return - } - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - guard case .success(let mutationEventOptional) = result else { - XCTFail("Should have been successful") - return - } - XCTAssertNil(mutationEventOptional) - expectCompletion.fulfill() - } - - var eventListenerOptional: GraphQLOperation>.ResultListener? - let apiMutateCalled = expectation(description: "API was called") - mockAPIPlugin.responders[.mutateRequestListener] = - MutateRequestListenerResponder> { request, eventListener in - guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { - XCTFail("The document variables property doesn't contain a valid input") - return nil - } - XCTAssert(input["title"] as? String == localPost.title) - XCTAssertTrue(request.document.contains("UpdatePost")) - eventListenerOptional = eventListener - apiMutateCalled.fulfill() - return nil - } - - let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") - let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - let configuration = custom(errorHandler: { _ in - expectErrorHandlerCalled.fulfill() - }, conflictHandler: { data, resolve in - guard let localPost = data.local as? Post, - let remotePost = data.remote as? Post else { - XCTFail("Couldn't get Posts from local and remote data") - return - } - - XCTAssertEqual(localPost.title, "localTitle") - XCTAssertEqual(remotePost.title, "remoteTitle") - expectConflicthandlerCalled.fulfill() - resolve(.retryLocal) - }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) - queue.addOperation(operation) - - wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) - guard let eventListener = eventListenerOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - let error = GraphQLError(message: "some other error") - eventListener(.success(.failure(.error([error])))) - - wait(for: [expectErrorHandlerCalled], timeout: defaultAsyncWaitTimeout) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } - - /// Given: GraphQL "OperationDisabled" error - /// - When: - /// - API is called and response contains an "OperationDisabled" error - /// - Then: - /// - Completion handler is successfully called - func testProcessOperationDisabledError() throws { - let post = Post(title: "localTitle", content: "localContent", createdAt: .now()) - let mutationEvent = try MutationEvent(model: post, modelSchema: Post.schema, mutationType: .create) - let expectCompletion = expectation(description: "Expect to complete error processing") - let completion: (Result) -> Void = { result in - if case .success(let mutationEventOptional) = result { - XCTAssertNil(mutationEventOptional) - expectCompletion.fulfill() - return - } - XCTFail("Should have been successful") - } - - let graphQLError = try getGraphQLResponseError(withRemote: post, - deleted: false, - version: 0, - errorType: .operationDisabled) - - let operation = ProcessMutationErrorFromCloudOperation( - dataStoreConfiguration: DataStoreConfiguration.testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLError, - completion: completion) - - queue.addOperation(operation) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) - } -} - -extension ProcessMutationErrorFromCloudOperationTests { - private func setUpCore() async throws -> AmplifyConfiguration { - await Amplify.reset() - - let dataStorePublisher = DataStorePublisher() - let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), - storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, - dataStorePublisher: dataStorePublisher, - validAPIPluginKey: "MockAPICategoryPlugin", - validAuthPluginKey: "MockAuthCategoryPlugin") - try Amplify.add(plugin: dataStorePlugin) - let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ - "awsDataStorePlugin": true - ]) - - let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) - - return amplifyConfig - } - - private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { - mockAPIPlugin = MockAPICategoryPlugin() - try Amplify.add(plugin: mockAPIPlugin) - - let apiConfig = APICategoryConfiguration(plugins: [ - "MockAPICategoryPlugin": true - ]) - let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) - return amplifyConfig - } - - private func setUpWithAPI() async throws { - let configWithoutAPI = try await setUpCore() - let configWithAPI = try setUpAPICategory(config: configWithoutAPI) - try Amplify.configure(configWithAPI) - } - - private func assertSuccessfulNil(_ result: Result) { - guard case .success(let mutationEventOptional) = result else { - XCTFail("Should have been successful") - return - } - XCTAssertNil(mutationEventOptional) - } - - private func getGraphQLResponseError(withRemote post: Post = Post(title: "remoteTitle", - content: "remoteContent", - createdAt: .now()), - deleted: Bool = false, - version: Int = 1, - errorType: AppSyncErrorType? = .conflictUnhandled) - throws -> GraphQLResponseError>? { - let data = Data(try post.toJSON().utf8) - let decoder = JSONDecoder() - decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy - let remoteData = try decoder.decode(JSONValue.self, from: data) - guard case var .object(remoteDataObject) = remoteData else { - return nil - } - remoteDataObject["_deleted"] = .boolean(deleted) - remoteDataObject["_lastChangedAt"] = .number(123) - remoteDataObject["_version"] = .number(Double(version)) - remoteDataObject["__typename"] = .string(post.modelName) - if let errorType = errorType { - let graphQLError = GraphQLError(message: "error message", - extensions: ["errorType": .string(errorType.rawValue), - "data": .object(remoteDataObject)]) - return GraphQLResponseError>.error([graphQLError]) - } else { - let graphQLError = GraphQLError(message: "error message") - return GraphQLResponseError>.error([graphQLError]) - } - } - - private func graphQLError(_ errorType: AppSyncErrorType) -> GraphQLError { - GraphQLError(message: "message", - locations: nil, - path: nil, - extensions: ["errorType": .string(errorType.rawValue)]) - } - - private func custom(errorHandler: DataStoreErrorHandler? = nil, - conflictHandler: (DataStoreConflictHandler)? = nil) -> DataStoreConfiguration { - if let conflictHandler = conflictHandler, let errorHandler = errorHandler { - #if os(watchOS) - return .custom(errorHandler: errorHandler, - conflictHandler: conflictHandler, - disableSubscriptions: { false }) - #else - return .custom(errorHandler: errorHandler, - conflictHandler: conflictHandler) - #endif - } else if let errorHandler = errorHandler { - #if os(watchOS) - return .custom(errorHandler: errorHandler, - disableSubscriptions: { false }) - #else - return .custom(errorHandler: errorHandler) - #endif - } else if let conflictHandler = conflictHandler { - #if os(watchOS) - return .custom(conflictHandler: conflictHandler, - disableSubscriptions: { false }) - #else - return .custom(conflictHandler: conflictHandler) - #endif - } - return .testDefault() - } -} +//import Foundation +//import XCTest +//import Combine +// +//@testable import Amplify +//@testable import AmplifyTestCommon +//@testable import AWSPluginsCore +//@testable import AWSDataStorePlugin +// +//// swiftlint:disable type_body_length +//// swiftlint:disable type_name +//// swiftlint:disable file_length +//class ProcessMutationErrorFromCloudOperationTests: XCTestCase { +// // swiftlint:enable type_name +// let defaultAsyncWaitTimeout = 10.0 +// var mockAPIPlugin: MockAPICategoryPlugin! +// var storageAdapter: StorageEngineAdapter! +// var localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let queue = OperationQueue() +// let reconciliationQueue = MockReconciliationQueue() +// +// override func setUp() async throws { +// await tryOrFail { +// try await setUpWithAPI() +// } +// storageAdapter = MockSQLiteStorageEngineAdapter() +// +// ModelRegistry.register(modelType: Post.self) +// ModelRegistry.register(modelType: Comment.self) +// } +// +// /// - Given: APIError +// /// - When: +// /// - APIError contains AuthError indicating user is not authenticated +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForAuthError() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// let authError = AuthError.signedOut("User is not authenticated", "Authenticate user", nil) +// let apiError = APIError.operationError("not signed in", "Sign In User", authError) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let actualAPIError = amplifyError as? APIError, +// case let .operationError(_, _, underlyingError) = actualAPIError, +// let authError = underlyingError as? AuthError, +// case .signedOut = authError else { +// XCTFail("Should be `signedOut` error") +// return +// } +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// apiError: apiError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: APIError +// /// - When: +// /// - APIError unrelated to AuthError +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForAPIError() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// let apiError = APIError.operationError("Operation failed", "", nil) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let actualAPIError = amplifyError as? APIError, +// case .operationError = actualAPIError else { +// XCTFail("Missing APIError.operationError") +// return +// } +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// apiError: apiError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: GraphQLError with no errors +// /// - When: +// /// - GraphQLError with no errors +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrors() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.unknown("", "", nil) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .unknown = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: GraphQLError with no error +// /// - When: +// /// - GraphQLError with no error +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrorsArray() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.error([]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// XCTAssertEqual(errors.count, 0) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: GraphQLError more than one error to handle +// /// - When: +// /// - GraphQLError with multiple errors +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithMultipleErrors() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let error = GraphQLError(message: "error message") +// let graphQLResponseError = GraphQLResponseError>.error([error, error]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// XCTAssertEqual(errors.count, 2) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: GraphQLError ConditionalCheck +// /// - When: +// /// - GraphQLError with errors containing type ConditionalCheck +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testProcessMutationErrorFromCloudOperationSuccessForConditionalCheck() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.conditionalCheck)]) +// +// let expectHubEvent = expectation(description: "Hub is notified") +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in +// if payload.eventName == "DataStore.conditionalSaveFailed" { +// expectHubEvent.fulfill() +// } +// } +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// XCTAssertEqual(errors.count, 1) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectHubEvent, expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// Amplify.Hub.removeListener(hubListener) +// } +// +// func testProcessMutationErrorFromCloudOperationSuccessForUnauthorized() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unauthorized)]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// XCTAssertEqual(errors.count, 1) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// func testProcessMutationErrorFromCloudOperationSuccessForOperationDisabled() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.operationDisabled)]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError") +// return +// } +// XCTAssertEqual(errors.count, 1) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// func testProcessMutationErrorFromCloudOperationSuccessForUnknownError() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unknown("unknownErrorType"))]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let configuration = custom(errorHandler: { error in +// guard let dataStoreError = error as? DataStoreError, +// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { +// XCTFail("Expected API error with mutationEvent") +// return +// } +// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, +// case .error(let errors) = graphQLResponseError else { +// XCTFail("Missing GraphQLResponseError.unknown") +// return +// } +// XCTAssertEqual(errors.count, 1) +// guard let actualMutationEvent = mutationEventOptional else { +// XCTFail("Missing mutationEvent for api error") +// return +// } +// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) +// expectErrorHandlerCalled.fulfill() +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - Error does not contain the remote model +// /// - Then: +// /// - Unexpected scenario, there should never be an conflict unhandled error without error.data +// func testConflictUnhandledReturnsErrorForMissingRemoteModel() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) +// let graphQLError = GraphQLError(message: "conflict unhandled", +// extensions: ["errorType": .string(AppSyncErrorType.conflictUnhandled.rawValue)]) +// let graphQLResponseError = GraphQLResponseError>.error([graphQLError]) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case let .failure(error) = result, +// let dataStoreError = error as? DataStoreError, +// case .unknown = dataStoreError else { +// XCTFail("Should have failed with DataStoreError.unknown") +// return +// } +// +// XCTAssertEqual(dataStoreError.errorDescription, "Missing remote model from the response from AppSync.") +// expectCompletion.fulfill() +// } +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `create` +// /// - Then: +// /// - Unexpected scenario, there should never get a conflict for create mutations +// func testConflictUnhandledReturnsErrorForCreateMutation() throws { +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) +// let remotePost = Post(title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 1) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case let .failure(error) = result, +// let dataStoreError = error as? DataStoreError, +// case .unknown = dataStoreError else { +// XCTFail("Should have failed with DataStoreError.unknown") +// return +// } +// +// XCTAssertEqual(dataStoreError.errorDescription, "Should never get conflict unhandled for create mutation") +// expectCompletion.fulfill() +// } +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `delete`, remote model is deleted. +// /// - Then: +// /// - No-op, operation finishes successfully +// func testConflictUnhandledForDeleteMutationAndDeletedRemoteModel() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: true, +// version: 1) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retryLocal` +// /// - Then: +// /// - API is called to delete with local model +// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryLocal() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// +// var eventListenerOptional: GraphQLOperation>.ResultListener? +// let apiMutateCalled = expectation(description: "API was called") +// mockAPIPlugin.responders[.mutateRequestListener] = +// MutateRequestListenerResponder> { request, eventListener in +// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { +// XCTFail("The document variables property doesn't contain a valid input") +// return nil +// } +// XCTAssert(input["id"] as? String == localPost.id) +// XCTAssert(request.document.contains("DeletePost")) +// eventListenerOptional = eventListener +// apiMutateCalled.fulfill() +// return nil +// } +// +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let configuration = custom(conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.retryLocal) +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// reconciliationQueue: reconciliationQueue, +// completion: completion) +// +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) +// guard let eventListener = eventListenerOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, +// modelName: remotePost.modelName, +// deleted: true, +// lastChangedAt: 0, +// version: 3) +// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) +// eventListener(.success(.success(mockResponse))) +// +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retry(model)` +// /// - Then: +// /// - API is called with the model from the conflict handler result +// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryModel() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// self.assertSuccessfulNil(result) +// expectCompletion.fulfill() +// } +// +// let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) +// var eventListenerOptional: GraphQLOperation>.ResultListener? +// let apiMutateCalled = expectation(description: "API was called") +// mockAPIPlugin.responders[.mutateRequestListener] = +// MutateRequestListenerResponder> { request, eventListener in +// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { +// XCTFail("The document variables property doesn't contain a valid input") +// return nil +// } +// XCTAssert(input["title"] as? String == retryModel.title) +// XCTAssertTrue(request.document.contains("UpdatePost")) +// eventListenerOptional = eventListener +// apiMutateCalled.fulfill() +// return nil +// } +// +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let configuration = custom(conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.retry(retryModel)) +// }) +// +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// reconciliationQueue: reconciliationQueue, +// completion: completion) +// +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) +// guard let eventListener = eventListenerOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, +// modelName: remotePost.modelName, +// deleted: false, +// lastChangedAt: 0, +// version: 3) +// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) +// eventListener(.success(.success(mockResponse))) +// +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.applyRemote` +// /// - Then: +// /// - Local Store is reconciled(recreated) to remote model, result mutationEvent is `update` +// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsApplyRemote() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result, +// let mutationEvent = mutationEventOptional else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertEqual(mutationEvent.mutationType, "update") +// XCTAssertEqual(mutationEvent.modelId, remotePost.id) +// expectCompletion.fulfill() +// } +// +// let modelSavedEvent = expectation(description: "model saved event") +// modelSavedEvent.expectedFulfillmentCount = 2 +// let storageAdapter = MockSQLiteStorageEngineAdapter() +// storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in +// guard let savedPost = model as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// XCTAssertEqual(savedPost.title, remotePost.title) +// modelSavedEvent.fulfill() +// completion(.success(model)) +// } +// +// storageAdapter.responders[.saveModelCompletion] = +// SaveModelCompletionResponder { metadata, completion in +// XCTAssertEqual(metadata.deleted, false) +// XCTAssertEqual(metadata.version, 2) +// modelSavedEvent.fulfill() +// completion(.success(metadata)) +// } +// +// let expectHubEvent = expectation(description: "Hub is notified") +// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in +// if payload.eventName == "DataStore.syncReceived" { +// expectHubEvent.fulfill() +// } +// } +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// +// wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// Amplify.Hub.removeListener(hubListener) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `update`, remote model is deleted +// /// - Then: +// /// - Local model is deleted, result mutationEvent is `delete` +// func testConflictUnhandledForUpdateMutationAndDeletedRemoteModel() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: true, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result, +// let mutationEvent = mutationEventOptional else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertEqual(mutationEvent.mutationType, "delete") +// XCTAssertEqual(mutationEvent.modelId, localPost.id) +// expectCompletion.fulfill() +// } +// +// let modelDeletedEvent = expectation(description: "model deleted event") +// let metadataSavedEvent = expectation(description: "metadata saved event") +// let storageAdapter = MockSQLiteStorageEngineAdapter() +// storageAdapter.shouldReturnErrorOnDeleteMutation = false +// storageAdapter.responders[.deleteUntypedModel] = DeleteUntypedModelCompletionResponder { _ in +// modelDeletedEvent.fulfill() +// return .emptyResult +// } +// storageAdapter.responders[.saveModelCompletion] = +// SaveModelCompletionResponder { metadata, completion in +// XCTAssertEqual(metadata.deleted, true) +// XCTAssertEqual(metadata.version, 2) +// metadataSavedEvent.fulfill() +// completion(.success(metadata)) +// } +// +// let expectHubEvent = expectation(description: "Hub is notified") +// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in +// if payload.eventName == "DataStore.syncReceived" { +// expectHubEvent.fulfill() +// } +// } +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// +// queue.addOperation(operation) +// +// wait(for: [modelDeletedEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// Amplify.Hub.removeListener(hubListener) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `update`, remote model is an update, conflict handler returns `.applyRemote` +// /// - Then: +// /// - Local model is updated with remote model data, result mutationEvent is `update` +// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsApplyRemote() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result, +// let mutationEvent = mutationEventOptional else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertEqual(mutationEvent.mutationType, "update") +// XCTAssertEqual(mutationEvent.modelId, remotePost.id) +// expectCompletion.fulfill() +// } +// +// let storageAdapter = MockSQLiteStorageEngineAdapter() +// let modelSavedEvent = expectation(description: "model saved event") +// let metadataSavedEvent = expectation(description: "metadata saved event") +// storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in +// guard let savedPost = model as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// XCTAssertEqual(savedPost.title, remotePost.title) +// modelSavedEvent.fulfill() +// completion(.success(model)) +// } +// storageAdapter.responders[.saveModelCompletion] = +// SaveModelCompletionResponder { metadata, completion in +// XCTAssertEqual(metadata.deleted, false) +// XCTAssertEqual(metadata.version, 2) +// metadataSavedEvent.fulfill() +// completion(.success(metadata)) +// } +// +// let expectHubEvent = expectation(description: "Hub is notified") +// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in +// if payload.eventName == "DataStore.syncReceived" { +// expectHubEvent.fulfill() +// } +// } +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let configuration = custom(conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.applyRemote) +// }) +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// Amplify.Hub.removeListener(hubListener) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` +// /// - Then: +// /// - API is called to update with the local model +// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryLocal() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertNil(mutationEventOptional) +// expectCompletion.fulfill() +// } +// +// var eventListenerOptional: GraphQLOperation>.ResultListener? +// let apiMutateCalled = expectation(description: "API was called") +// mockAPIPlugin.responders[.mutateRequestListener] = +// MutateRequestListenerResponder> { request, eventListener in +// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { +// XCTFail("The document variables property doesn't contain a valid input") +// return nil +// } +// XCTAssert(input["title"] as? String == localPost.title) +// XCTAssertTrue(request.document.contains("UpdatePost")) +// eventListenerOptional = eventListener +// apiMutateCalled.fulfill() +// return nil +// } +// +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let configuration = custom(conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.retryLocal) +// }) +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// reconciliationQueue: reconciliationQueue, +// completion: completion) +// +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) +// guard let eventListener = eventListenerOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, +// modelName: remotePost.modelName, +// deleted: false, +// lastChangedAt: 0, +// version: 3) +// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) +// eventListener(.success(.success(mockResponse))) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retry(Model)` +// /// - Then: +// /// - API is called to update the model from the conflict handler result +// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryModel() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertNil(mutationEventOptional) +// expectCompletion.fulfill() +// } +// +// let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) +// var eventListenerOptional: GraphQLOperation>.ResultListener? +// let apiMutateCalled = expectation(description: "API was called") +// mockAPIPlugin.responders[.mutateRequestListener] = +// MutateRequestListenerResponder> { request, eventListener in +// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { +// XCTFail("The document variables property doesn't contain a valid input") +// return nil +// } +// XCTAssert(input["title"] as? String == retryModel.title) +// XCTAssertTrue(request.document.contains("UpdatePost")) +// eventListenerOptional = eventListener +// apiMutateCalled.fulfill() +// return nil +// } +// +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let configuration = custom(conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.retry(retryModel)) +// }) +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// reconciliationQueue: reconciliationQueue, +// completion: completion) +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) +// guard let eventListener = eventListenerOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, +// modelName: remotePost.modelName, +// deleted: false, +// lastChangedAt: 0, +// version: 3) +// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) +// eventListener(.success(.success(mockResponse))) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// - Given: Conflict Unhandled error +// /// - When: +// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` +// /// - API is called to update with local model and response contains error +// /// - Then: +// /// - `DataStoreErrorHandler` is called +// func testConflictUnhandledSyncToCloudReturnsError() throws { +// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) +// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, +// deleted: false, +// version: 2) else { +// XCTFail("Couldn't get GraphQL response with remote post") +// return +// } +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// guard case .success(let mutationEventOptional) = result else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertNil(mutationEventOptional) +// expectCompletion.fulfill() +// } +// +// var eventListenerOptional: GraphQLOperation>.ResultListener? +// let apiMutateCalled = expectation(description: "API was called") +// mockAPIPlugin.responders[.mutateRequestListener] = +// MutateRequestListenerResponder> { request, eventListener in +// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { +// XCTFail("The document variables property doesn't contain a valid input") +// return nil +// } +// XCTAssert(input["title"] as? String == localPost.title) +// XCTAssertTrue(request.document.contains("UpdatePost")) +// eventListenerOptional = eventListener +// apiMutateCalled.fulfill() +// return nil +// } +// +// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") +// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") +// let configuration = custom(errorHandler: { _ in +// expectErrorHandlerCalled.fulfill() +// }, conflictHandler: { data, resolve in +// guard let localPost = data.local as? Post, +// let remotePost = data.remote as? Post else { +// XCTFail("Couldn't get Posts from local and remote data") +// return +// } +// +// XCTAssertEqual(localPost.title, "localTitle") +// XCTAssertEqual(remotePost.title, "remoteTitle") +// expectConflicthandlerCalled.fulfill() +// resolve(.retryLocal) +// }) +// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLResponseError, +// completion: completion) +// queue.addOperation(operation) +// +// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) +// guard let eventListener = eventListenerOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// let error = GraphQLError(message: "some other error") +// eventListener(.success(.failure(.error([error])))) +// +// wait(for: [expectErrorHandlerCalled], timeout: defaultAsyncWaitTimeout) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// /// Given: GraphQL "OperationDisabled" error +// /// - When: +// /// - API is called and response contains an "OperationDisabled" error +// /// - Then: +// /// - Completion handler is successfully called +// func testProcessOperationDisabledError() throws { +// let post = Post(title: "localTitle", content: "localContent", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: post, modelSchema: Post.schema, mutationType: .create) +// let expectCompletion = expectation(description: "Expect to complete error processing") +// let completion: (Result) -> Void = { result in +// if case .success(let mutationEventOptional) = result { +// XCTAssertNil(mutationEventOptional) +// expectCompletion.fulfill() +// return +// } +// XCTFail("Should have been successful") +// } +// +// let graphQLError = try getGraphQLResponseError(withRemote: post, +// deleted: false, +// version: 0, +// errorType: .operationDisabled) +// +// let operation = ProcessMutationErrorFromCloudOperation( +// dataStoreConfiguration: DataStoreConfiguration.testDefault(), +// mutationEvent: mutationEvent, +// api: mockAPIPlugin, +// storageAdapter: storageAdapter, +// graphQLResponseError: graphQLError, +// completion: completion) +// +// queue.addOperation(operation) +// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) +// } +//} +// +//extension ProcessMutationErrorFromCloudOperationTests { +// private func setUpCore() async throws -> AmplifyConfiguration { +// await Amplify.reset() +// +// let dataStorePublisher = DataStorePublisher() +// let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), +// storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, +// dataStorePublisher: dataStorePublisher, +// validAPIPluginKey: "MockAPICategoryPlugin", +// validAuthPluginKey: "MockAuthCategoryPlugin") +// try Amplify.add(plugin: dataStorePlugin) +// let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ +// "awsDataStorePlugin": true +// ]) +// +// let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) +// +// return amplifyConfig +// } +// +// private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { +// mockAPIPlugin = MockAPICategoryPlugin() +// try Amplify.add(plugin: mockAPIPlugin) +// +// let apiConfig = APICategoryConfiguration(plugins: [ +// "MockAPICategoryPlugin": true +// ]) +// let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) +// return amplifyConfig +// } +// +// private func setUpWithAPI() async throws { +// let configWithoutAPI = try await setUpCore() +// let configWithAPI = try setUpAPICategory(config: configWithoutAPI) +// try Amplify.configure(configWithAPI) +// } +// +// private func assertSuccessfulNil(_ result: Result) { +// guard case .success(let mutationEventOptional) = result else { +// XCTFail("Should have been successful") +// return +// } +// XCTAssertNil(mutationEventOptional) +// } +// +// private func getGraphQLResponseError(withRemote post: Post = Post(title: "remoteTitle", +// content: "remoteContent", +// createdAt: .now()), +// deleted: Bool = false, +// version: Int = 1, +// errorType: AppSyncErrorType? = .conflictUnhandled) +// throws -> GraphQLResponseError>? { +// let data = Data(try post.toJSON().utf8) +// let decoder = JSONDecoder() +// decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy +// let remoteData = try decoder.decode(JSONValue.self, from: data) +// guard case var .object(remoteDataObject) = remoteData else { +// return nil +// } +// remoteDataObject["_deleted"] = .boolean(deleted) +// remoteDataObject["_lastChangedAt"] = .number(123) +// remoteDataObject["_version"] = .number(Double(version)) +// remoteDataObject["__typename"] = .string(post.modelName) +// if let errorType = errorType { +// let graphQLError = GraphQLError(message: "error message", +// extensions: ["errorType": .string(errorType.rawValue), +// "data": .object(remoteDataObject)]) +// return GraphQLResponseError>.error([graphQLError]) +// } else { +// let graphQLError = GraphQLError(message: "error message") +// return GraphQLResponseError>.error([graphQLError]) +// } +// } +// +// private func graphQLError(_ errorType: AppSyncErrorType) -> GraphQLError { +// GraphQLError(message: "message", +// locations: nil, +// path: nil, +// extensions: ["errorType": .string(errorType.rawValue)]) +// } +// +// private func custom(errorHandler: DataStoreErrorHandler? = nil, +// conflictHandler: (DataStoreConflictHandler)? = nil) -> DataStoreConfiguration { +// if let conflictHandler = conflictHandler, let errorHandler = errorHandler { +// #if os(watchOS) +// return .custom(errorHandler: errorHandler, +// conflictHandler: conflictHandler, +// disableSubscriptions: { false }) +// #else +// return .custom(errorHandler: errorHandler, +// conflictHandler: conflictHandler) +// #endif +// } else if let errorHandler = errorHandler { +// #if os(watchOS) +// return .custom(errorHandler: errorHandler, +// disableSubscriptions: { false }) +// #else +// return .custom(errorHandler: errorHandler) +// #endif +// } else if let conflictHandler = conflictHandler { +// #if os(watchOS) +// return .custom(conflictHandler: conflictHandler, +// disableSubscriptions: { false }) +// #else +// return .custom(conflictHandler: conflictHandler) +// #endif +// } +// return .testDefault() +// } +//} diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift index 71baed1a34..256295250e 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift @@ -1,484 +1,484 @@ +//// +//// Copyright Amazon.com Inc. or its affiliates. +//// All Rights Reserved. +//// +//// SPDX-License-Identifier: Apache-2.0 +//// // -// Copyright Amazon.com Inc. or its affiliates. -// All Rights Reserved. -// -// SPDX-License-Identifier: Apache-2.0 -// - -import Foundation -import XCTest -import Combine - -@testable import Amplify -@testable import AmplifyTestCommon -@testable import AWSPluginsCore -@testable import AWSDataStorePlugin - -class SyncMutationToCloudOperationTests: XCTestCase { - let defaultAsyncWaitTimeout = 2.0 - let secondsInADay = 60 * 60 * 24 - var mockAPIPlugin: MockAPICategoryPlugin! - - var reachabilityPublisher: CurrentValueSubject! - var publisher: AnyPublisher { - return reachabilityPublisher.eraseToAnyPublisher() - } - - override func setUp() async throws { - reachabilityPublisher = CurrentValueSubject(ReachabilityUpdate(isOnline: false)) - await tryOrFail { - try await setUpWithAPI() - } - ModelRegistry.register(modelType: Post.self) - ModelRegistry.register(modelType: Comment.self) - } - - func testRetryOnTimeoutOfWaiting() async throws { - let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") - let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") - let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") - - let post1 = Post(title: "post1", content: "content1", createdAt: .now()) - let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) - - var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? - var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? - - var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { request, eventListener in - if numberOfTimesEntered == 0 { - let requestInputVersion = request.variables.flatMap { $0["input"] as? [String: Any] }.flatMap { $0["_version"] as? Int } - XCTAssertEqual(requestInputVersion, 10) - listenerFromFirstRequestOptional = eventListener - expectFirstCallToAPIMutate.fulfill() - } else if numberOfTimesEntered == 1 { - listenerFromSecondRequestOptional = eventListener - expectSecondCallToAPIMutate.fulfill() - } else { - XCTFail("This should not be called more than once") - } - numberOfTimesEntered += 1 - // We could return an operation here, but we don't need to. - // The main reason for having this responder is to get the eventListener. - // the eventListener block will execute the the call to validateResponseFromCloud - return nil - } - mockAPIPlugin.responders[.mutateRequestListener] = responder - - let completion: GraphQLOperation>.ResultListener = { _ in - expectMutationRequestCompletion.fulfill() - } - - let model = MockSynced(id: "id-1") - let operation = await SyncMutationToCloudOperation( - mutationEvent: mutationEvent, - getLatestSyncMetadata: { - MutationSyncMetadata( - modelId: model.id, - modelName: model.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 10 - ) - }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: completion - ) - let queue = OperationQueue() - queue.addOperation(operation) - await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) - guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - let urlError = URLError(URLError.notConnectedToInternet) - listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) - await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) - - guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - - let anyModel = try model.eraseToAnyModel() - let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, - modelName: model.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 2) - let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - listenerFromSecondRequest(.success(.success(remoteMutationSync))) - // waitForExpectations(timeout: 1) - await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) - } - - func testRetryOnChangeReachability() async throws { - let mockRequestRetryPolicy = MockRequestRetryablePolicy() - let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) - mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) - - let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") - let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") - let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") - let post1 = Post(title: "post1", content: "content1", createdAt: .now()) - let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) - - var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? - var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? - - var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { _, eventListener in - if numberOfTimesEntered == 0 { - listenerFromFirstRequestOptional = eventListener - expectFirstCallToAPIMutate.fulfill() - } else if numberOfTimesEntered == 1 { - listenerFromSecondRequestOptional = eventListener - expectSecondCallToAPIMutate.fulfill() - } else { - XCTFail("This should not be called more than once") - } - numberOfTimesEntered += 1 - // We could return an operation here, but we don't need to. - // The main reason for having this responder is to get the eventListener. - // the eventListener block will execute the the call to validateResponseFromCloud - return nil - } - mockAPIPlugin.responders[.mutateRequestListener] = responder - - let completion: GraphQLOperation>.ResultListener = { _ in - expectMutationRequestCompletion.fulfill() - } - let operation = await SyncMutationToCloudOperation( - mutationEvent: mutationEvent, - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - requestRetryablePolicy: mockRequestRetryPolicy, - completion: completion - ) - let queue = OperationQueue() - queue.addOperation(operation) - await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) - guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - let urlError = URLError(URLError.notConnectedToInternet) - listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) - reachabilityPublisher.send(ReachabilityUpdate(isOnline: true)) - - await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) - guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - let model = MockSynced(id: "id-1") - let anyModel = try model.eraseToAnyModel() - let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, - modelName: model.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 2) - let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - listenerFromSecondRequest(.success(.success(remoteMutationSync))) - await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) - } - - func testAbilityToCancel() async throws { - let mockRequestRetryPolicy = MockRequestRetryablePolicy() - let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) - mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) - - let expectMutationRequestFailed = expectation(description: "Expect to fail mutation request") - let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") - let post1 = Post(title: "post1", content: "content1", createdAt: .now()) - let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) - - var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? - - var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { _, eventListener in - if numberOfTimesEntered == 0 { - listenerFromFirstRequestOptional = eventListener - expectFirstCallToAPIMutate.fulfill() - } else { - XCTFail("This should not be called more than once") - } - numberOfTimesEntered += 1 - // We could return an operation here, but we don't need to. - // The main reason for having this responder is to get the eventListener. - // the eventListener block will execute the the call to validateResponseFromCloud - return nil - } - mockAPIPlugin.responders[.mutateRequestListener] = responder - - let completion: GraphQLOperation>.ResultListener = { asyncEvent in - switch asyncEvent { - case .failure: - expectMutationRequestFailed.fulfill() - default: - break - } - } - let operation = await SyncMutationToCloudOperation( - mutationEvent: mutationEvent, - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - requestRetryablePolicy: mockRequestRetryPolicy, - completion: completion - ) - let queue = OperationQueue() - queue.addOperation(operation) - await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) - guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { - XCTFail("Listener was not called through MockAPICategoryPlugin") - return - } - - let urlError = URLError(URLError.notConnectedToInternet) - listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) - - // At this point, we will be "waiting forever" to retry our request or until the operation is canceled - operation.cancel() - await fulfillment(of: [expectMutationRequestFailed], timeout: defaultAsyncWaitTimeout) - } - - // MARK: - GetRetryAdviceIfRetryableTests - - func testGetRetryAdvice_NetworkError_RetryTrue() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - - let error = APIError.networkError("", nil, URLError(.userAuthenticationRequired)) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertTrue(advice.shouldRetry) - } - - func testGetRetryAdvice_HTTPStatusError401WithMultiAuth_RetryTrue() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: MockMultiAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - let response = HTTPURLResponse(url: URL(string: "http://localhost")!, - statusCode: 401, - httpVersion: nil, - headerFields: nil)! - let error = APIError.httpStatusError(401, response) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertTrue(advice.shouldRetry) - } - - /// Given: Model with multiple auth types. Mutation requests always fail with 401 error code - /// When: Mutating model fails with 401 - /// Then: DataStore will try again with each auth type and eventually fails - func testGetRetryAdviceForEachModelAuthTypeThenFail_HTTPStatusError401() async throws { - var numberOfTimesEntered = 0 - let mutationEvent = try createMutationEvent() - let authStrategy = MockMultiAuthModeStrategy() - let expectedNumberOfTimesEntered = authStrategy.authTypesFor(schema: mutationEvent.schema, operation: .create).count - - let expectCalllToApiMutateNTimesAndFail = expectation(description: "Call API.mutate \(expectedNumberOfTimesEntered) times and then fail") - - let response = HTTPURLResponse(url: URL(string: "http://localhost")!, - statusCode: 401, - httpVersion: nil, - headerFields: nil)! - let error = APIError.httpStatusError(401, response) - - let operation = await SyncMutationToCloudOperation( - mutationEvent: mutationEvent, - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: authStrategy, - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { result in - if numberOfTimesEntered == expectedNumberOfTimesEntered { - expectCalllToApiMutateNTimesAndFail.fulfill() - - } else { - XCTFail("API.mutate was called incorrect amount of times, expected: \(expectedNumberOfTimesEntered), was : \(numberOfTimesEntered)") - } - } - ) - - let responder = MutateRequestListenerResponder> { request, eventListener in - let requestOptions = GraphQLOperationRequest>.Options(pluginOptions: nil) - let request = GraphQLOperationRequest>(apiName: request.apiName, - operationType: .mutation, - document: request.document, - variables: request.variables, - responseType: request.responseType, - options: requestOptions) - let operation = MockGraphQLOperation(request: request, responseType: request.responseType) - - numberOfTimesEntered += 1 - - DispatchQueue.global().sync { - // Fail with 401 status code - eventListener!(.failure(error)) - } - - return operation - } - - mockAPIPlugin.responders[.mutateRequestListener] = responder - - let queue = OperationQueue() - queue.addOperation(operation) - - await fulfillment(of: [expectCalllToApiMutateNTimesAndFail], timeout: defaultAsyncWaitTimeout) - } - - func testGetRetryAdvice_OperationErrorAuthErrorWithMultiAuth_RetryTrue() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: MockMultiAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - - let authError = AuthError.notAuthorized("", "", nil) - let error = APIError.operationError("", "", authError) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertTrue(advice.shouldRetry) - } - - func testGetRetryAdvice_OperationErrorAuthErrorWithSingleAuth_RetryFalse() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - - let authError = AuthError.notAuthorized("", "", nil) - let error = APIError.operationError("", "", authError) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertFalse(advice.shouldRetry) - } - - func testGetRetryAdvice_OperationErrorAuthErrorSessionExpired_RetryTrue() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - - let authError = AuthError.sessionExpired("", "", nil) - let error = APIError.operationError("", "", authError) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertTrue(advice.shouldRetry) - } - - func testGetRetryAdvice_OperationErrorAuthErrorSignedOut_RetryTrue() async throws { - let operation = await SyncMutationToCloudOperation( - mutationEvent: try createMutationEvent(), - getLatestSyncMetadata: { nil }, - api: mockAPIPlugin, - authModeStrategy: AWSDefaultAuthModeStrategy(), - networkReachabilityPublisher: publisher, - currentAttemptNumber: 1, - completion: { _ in } - ) - - let authError = AuthError.signedOut("", "", nil) - let error = APIError.operationError("", "", authError) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertTrue(advice.shouldRetry) - } - - private func createMutationEvent() throws -> MutationEvent { - let post1 = Post(title: "post1", content: "content1", createdAt: .now()) - return try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) - } - -} - -public class MockMultiAuthModeStrategy: AuthModeStrategy { - public weak var authDelegate: AuthModeStrategyDelegate? - required public init() {} - - public func authTypesFor(schema: ModelSchema, - operation: ModelOperation) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) - } - - public func authTypesFor(schema: ModelSchema, - operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) - } -} - -extension SyncMutationToCloudOperationTests { - private func setUpCore() async throws -> AmplifyConfiguration { - await Amplify.reset() - - let dataStorePublisher = DataStorePublisher() - let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), - storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, - dataStorePublisher: dataStorePublisher, - validAPIPluginKey: "MockAPICategoryPlugin", - validAuthPluginKey: "MockAuthCategoryPlugin") - - try Amplify.add(plugin: dataStorePlugin) - let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ - "awsDataStorePlugin": true - ]) - - let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) - - return amplifyConfig - } - - private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { - mockAPIPlugin = MockAPICategoryPlugin() - try Amplify.add(plugin: mockAPIPlugin) - - let apiConfig = APICategoryConfiguration(plugins: [ - "MockAPICategoryPlugin": true - ]) - let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) - return amplifyConfig - } - - private func setUpWithAPI() async throws { - let configWithoutAPI = try await setUpCore() - let configWithAPI = try setUpAPICategory(config: configWithoutAPI) - try Amplify.configure(configWithAPI) - } -} +//import Foundation +//import XCTest +//import Combine +// +//@testable import Amplify +//@testable import AmplifyTestCommon +//@testable import AWSPluginsCore +//@testable import AWSDataStorePlugin +// +//class SyncMutationToCloudOperationTests: XCTestCase { +// let defaultAsyncWaitTimeout = 2.0 +// let secondsInADay = 60 * 60 * 24 +// var mockAPIPlugin: MockAPICategoryPlugin! +// +// var reachabilityPublisher: CurrentValueSubject! +// var publisher: AnyPublisher { +// return reachabilityPublisher.eraseToAnyPublisher() +// } +// +// override func setUp() async throws { +// reachabilityPublisher = CurrentValueSubject(ReachabilityUpdate(isOnline: false)) +// await tryOrFail { +// try await setUpWithAPI() +// } +// ModelRegistry.register(modelType: Post.self) +// ModelRegistry.register(modelType: Comment.self) +// } +// +// func testRetryOnTimeoutOfWaiting() async throws { +// let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") +// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") +// let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") +// +// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) +// +// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? +// var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? +// +// var numberOfTimesEntered = 0 +// let responder = MutateRequestListenerResponder> { request, eventListener in +// if numberOfTimesEntered == 0 { +// let requestInputVersion = request.variables.flatMap { $0["input"] as? [String: Any] }.flatMap { $0["_version"] as? Int } +// XCTAssertEqual(requestInputVersion, 10) +// listenerFromFirstRequestOptional = eventListener +// expectFirstCallToAPIMutate.fulfill() +// } else if numberOfTimesEntered == 1 { +// listenerFromSecondRequestOptional = eventListener +// expectSecondCallToAPIMutate.fulfill() +// } else { +// XCTFail("This should not be called more than once") +// } +// numberOfTimesEntered += 1 +// // We could return an operation here, but we don't need to. +// // The main reason for having this responder is to get the eventListener. +// // the eventListener block will execute the the call to validateResponseFromCloud +// return nil +// } +// mockAPIPlugin.responders[.mutateRequestListener] = responder +// +// let completion: GraphQLOperation>.ResultListener = { _ in +// expectMutationRequestCompletion.fulfill() +// } +// +// let model = MockSynced(id: "id-1") +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: mutationEvent, +// getLatestSyncMetadata: { +// MutationSyncMetadata( +// modelId: model.id, +// modelName: model.modelName, +// deleted: false, +// lastChangedAt: Date().unixSeconds, +// version: 10 +// ) +// }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: completion +// ) +// let queue = OperationQueue() +// queue.addOperation(operation) +// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) +// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// let urlError = URLError(URLError.notConnectedToInternet) +// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) +// await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) +// +// guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// +// let anyModel = try model.eraseToAnyModel() +// let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, +// modelName: model.modelName, +// deleted: false, +// lastChangedAt: Date().unixSeconds, +// version: 2) +// let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) +// listenerFromSecondRequest(.success(.success(remoteMutationSync))) +// // waitForExpectations(timeout: 1) +// await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// func testRetryOnChangeReachability() async throws { +// let mockRequestRetryPolicy = MockRequestRetryablePolicy() +// let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) +// mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) +// +// let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") +// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") +// let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") +// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) +// +// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? +// var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? +// +// var numberOfTimesEntered = 0 +// let responder = MutateRequestListenerResponder> { _, eventListener in +// if numberOfTimesEntered == 0 { +// listenerFromFirstRequestOptional = eventListener +// expectFirstCallToAPIMutate.fulfill() +// } else if numberOfTimesEntered == 1 { +// listenerFromSecondRequestOptional = eventListener +// expectSecondCallToAPIMutate.fulfill() +// } else { +// XCTFail("This should not be called more than once") +// } +// numberOfTimesEntered += 1 +// // We could return an operation here, but we don't need to. +// // The main reason for having this responder is to get the eventListener. +// // the eventListener block will execute the the call to validateResponseFromCloud +// return nil +// } +// mockAPIPlugin.responders[.mutateRequestListener] = responder +// +// let completion: GraphQLOperation>.ResultListener = { _ in +// expectMutationRequestCompletion.fulfill() +// } +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: mutationEvent, +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// requestRetryablePolicy: mockRequestRetryPolicy, +// completion: completion +// ) +// let queue = OperationQueue() +// queue.addOperation(operation) +// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) +// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// let urlError = URLError(URLError.notConnectedToInternet) +// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) +// reachabilityPublisher.send(ReachabilityUpdate(isOnline: true)) +// +// await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) +// guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// let model = MockSynced(id: "id-1") +// let anyModel = try model.eraseToAnyModel() +// let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, +// modelName: model.modelName, +// deleted: false, +// lastChangedAt: Date().unixSeconds, +// version: 2) +// let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) +// listenerFromSecondRequest(.success(.success(remoteMutationSync))) +// await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) +// } +// +// func testAbilityToCancel() async throws { +// let mockRequestRetryPolicy = MockRequestRetryablePolicy() +// let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) +// mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) +// +// let expectMutationRequestFailed = expectation(description: "Expect to fail mutation request") +// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") +// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) +// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) +// +// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? +// +// var numberOfTimesEntered = 0 +// let responder = MutateRequestListenerResponder> { _, eventListener in +// if numberOfTimesEntered == 0 { +// listenerFromFirstRequestOptional = eventListener +// expectFirstCallToAPIMutate.fulfill() +// } else { +// XCTFail("This should not be called more than once") +// } +// numberOfTimesEntered += 1 +// // We could return an operation here, but we don't need to. +// // The main reason for having this responder is to get the eventListener. +// // the eventListener block will execute the the call to validateResponseFromCloud +// return nil +// } +// mockAPIPlugin.responders[.mutateRequestListener] = responder +// +// let completion: GraphQLOperation>.ResultListener = { asyncEvent in +// switch asyncEvent { +// case .failure: +// expectMutationRequestFailed.fulfill() +// default: +// break +// } +// } +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: mutationEvent, +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// requestRetryablePolicy: mockRequestRetryPolicy, +// completion: completion +// ) +// let queue = OperationQueue() +// queue.addOperation(operation) +// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) +// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { +// XCTFail("Listener was not called through MockAPICategoryPlugin") +// return +// } +// +// let urlError = URLError(URLError.notConnectedToInternet) +// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) +// +// // At this point, we will be "waiting forever" to retry our request or until the operation is canceled +// operation.cancel() +// await fulfillment(of: [expectMutationRequestFailed], timeout: defaultAsyncWaitTimeout) +// } +// +// // MARK: - GetRetryAdviceIfRetryableTests +// +// func testGetRetryAdvice_NetworkError_RetryTrue() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// +// let error = APIError.networkError("", nil, URLError(.userAuthenticationRequired)) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertTrue(advice.shouldRetry) +// } +// +// func testGetRetryAdvice_HTTPStatusError401WithMultiAuth_RetryTrue() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: MockMultiAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// let response = HTTPURLResponse(url: URL(string: "http://localhost")!, +// statusCode: 401, +// httpVersion: nil, +// headerFields: nil)! +// let error = APIError.httpStatusError(401, response) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertTrue(advice.shouldRetry) +// } +// +// /// Given: Model with multiple auth types. Mutation requests always fail with 401 error code +// /// When: Mutating model fails with 401 +// /// Then: DataStore will try again with each auth type and eventually fails +// func testGetRetryAdviceForEachModelAuthTypeThenFail_HTTPStatusError401() async throws { +// var numberOfTimesEntered = 0 +// let mutationEvent = try createMutationEvent() +// let authStrategy = MockMultiAuthModeStrategy() +// let expectedNumberOfTimesEntered = authStrategy.authTypesFor(schema: mutationEvent.schema, operation: .create).count +// +// let expectCalllToApiMutateNTimesAndFail = expectation(description: "Call API.mutate \(expectedNumberOfTimesEntered) times and then fail") +// +// let response = HTTPURLResponse(url: URL(string: "http://localhost")!, +// statusCode: 401, +// httpVersion: nil, +// headerFields: nil)! +// let error = APIError.httpStatusError(401, response) +// +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: mutationEvent, +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: authStrategy, +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { result in +// if numberOfTimesEntered == expectedNumberOfTimesEntered { +// expectCalllToApiMutateNTimesAndFail.fulfill() +// +// } else { +// XCTFail("API.mutate was called incorrect amount of times, expected: \(expectedNumberOfTimesEntered), was : \(numberOfTimesEntered)") +// } +// } +// ) +// +// let responder = MutateRequestListenerResponder> { request, eventListener in +// let requestOptions = GraphQLOperationRequest>.Options(pluginOptions: nil) +// let request = GraphQLOperationRequest>(apiName: request.apiName, +// operationType: .mutation, +// document: request.document, +// variables: request.variables, +// responseType: request.responseType, +// options: requestOptions) +// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) +// +// numberOfTimesEntered += 1 +// +// DispatchQueue.global().sync { +// // Fail with 401 status code +// eventListener!(.failure(error)) +// } +// +// return operation +// } +// +// mockAPIPlugin.responders[.mutateRequestListener] = responder +// +// let queue = OperationQueue() +// queue.addOperation(operation) +// +// await fulfillment(of: [expectCalllToApiMutateNTimesAndFail], timeout: defaultAsyncWaitTimeout) +// } +// +// func testGetRetryAdvice_OperationErrorAuthErrorWithMultiAuth_RetryTrue() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: MockMultiAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// +// let authError = AuthError.notAuthorized("", "", nil) +// let error = APIError.operationError("", "", authError) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertTrue(advice.shouldRetry) +// } +// +// func testGetRetryAdvice_OperationErrorAuthErrorWithSingleAuth_RetryFalse() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// +// let authError = AuthError.notAuthorized("", "", nil) +// let error = APIError.operationError("", "", authError) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertFalse(advice.shouldRetry) +// } +// +// func testGetRetryAdvice_OperationErrorAuthErrorSessionExpired_RetryTrue() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// +// let authError = AuthError.sessionExpired("", "", nil) +// let error = APIError.operationError("", "", authError) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertTrue(advice.shouldRetry) +// } +// +// func testGetRetryAdvice_OperationErrorAuthErrorSignedOut_RetryTrue() async throws { +// let operation = await SyncMutationToCloudOperation( +// mutationEvent: try createMutationEvent(), +// getLatestSyncMetadata: { nil }, +// api: mockAPIPlugin, +// authModeStrategy: AWSDefaultAuthModeStrategy(), +// networkReachabilityPublisher: publisher, +// currentAttemptNumber: 1, +// completion: { _ in } +// ) +// +// let authError = AuthError.signedOut("", "", nil) +// let error = APIError.operationError("", "", authError) +// let advice = operation.getRetryAdviceIfRetryable(error: error) +// XCTAssertTrue(advice.shouldRetry) +// } +// +// private func createMutationEvent() throws -> MutationEvent { +// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) +// return try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) +// } +// +//} +// +//public class MockMultiAuthModeStrategy: AuthModeStrategy { +// public weak var authDelegate: AuthModeStrategyDelegate? +// required public init() {} +// +// public func authTypesFor(schema: ModelSchema, +// operation: ModelOperation) -> AWSAuthorizationTypeIterator { +// return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) +// } +// +// public func authTypesFor(schema: ModelSchema, +// operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { +// return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) +// } +//} +// +//extension SyncMutationToCloudOperationTests { +// private func setUpCore() async throws -> AmplifyConfiguration { +// await Amplify.reset() +// +// let dataStorePublisher = DataStorePublisher() +// let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), +// storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, +// dataStorePublisher: dataStorePublisher, +// validAPIPluginKey: "MockAPICategoryPlugin", +// validAuthPluginKey: "MockAuthCategoryPlugin") +// +// try Amplify.add(plugin: dataStorePlugin) +// let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ +// "awsDataStorePlugin": true +// ]) +// +// let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) +// +// return amplifyConfig +// } +// +// private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { +// mockAPIPlugin = MockAPICategoryPlugin() +// try Amplify.add(plugin: mockAPIPlugin) +// +// let apiConfig = APICategoryConfiguration(plugins: [ +// "MockAPICategoryPlugin": true +// ]) +// let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) +// return amplifyConfig +// } +// +// private func setUpWithAPI() async throws { +// let configWithoutAPI = try await setUpCore() +// let configWithAPI = try setUpAPICategory(config: configWithoutAPI) +// try Amplify.configure(configWithAPI) +// } +//} diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ModelReconciliationDeleteTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ModelReconciliationDeleteTests.swift index b1b3d978bd..d153f98310 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ModelReconciliationDeleteTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ModelReconciliationDeleteTests.swift @@ -40,14 +40,16 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { storageAdapter.save(localSyncMetadata) { _ in localMetadataSaved.fulfill() } await fulfillment(of: [localMetadataSaved], timeout: 1) - var valueListenerFromRequest: MutationSyncInProcessListener? + var asyncSequence: AmplifyAsyncThrowingSequence>>? let expectationListener = expectation(description: "listener") - let responder = SubscribeRequestListenerResponder> { request, valueListener, _ in + let responder = SubscribeRequestListenerResponder> { request in if request.document.contains("onUpdateMockSynced") { - valueListenerFromRequest = valueListener expectationListener.fulfill() } - return nil + + let sequence = AmplifyAsyncThrowingSequence>>() + asyncSequence = sequence + return sequence } apiPlugin.responders[.subscribeRequestListener] = responder @@ -59,7 +61,7 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { } await fulfillment(of: [expectationListener], timeout: 2) - guard let valueListener = valueListenerFromRequest else { + guard let asyncSequence else { XCTFail("Incoming responder didn't set up listener") return } @@ -71,7 +73,7 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { lastChangedAt: Date().unixSeconds, version: 1) let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - valueListener(.data(.success(remoteMutationSync))) + asyncSequence.send(.data(.success(remoteMutationSync))) // Because we expect this event to be dropped, there won't be a Hub notification or callback to listen to, so // we have to brute-force this wait @@ -106,9 +108,13 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { let onUpdateListener: MutationSyncInProcessListener = { _ in print("emptyListener") } - _ = self.apiPlugin.subscribe(request: request, - valueListener: onUpdateListener, - completionListener: nil) + + Task { + let sequence = self.apiPlugin.subscribe(request: request) + for try await event in sequence { + onUpdateListener(event) + } + } MockAWSIncomingEventReconciliationQueue.mockSend(event: .initialized) } default: @@ -131,15 +137,15 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { try setUpStorageAdapter() } - var valueListenerFromRequest: MutationSyncInProcessListener? + var asyncSequence: AmplifyAsyncThrowingSequence>>? - let responder = SubscribeRequestListenerResponder> {request, valueListener, _ in + let responder = SubscribeRequestListenerResponder> {request in if request.document.contains("onUpdateMockSynced") { - valueListenerFromRequest = valueListener expectationListener.fulfill() } - - return nil + let sequence = AmplifyAsyncThrowingSequence>>() + asyncSequence = sequence + return sequence } apiPlugin.responders[.subscribeRequestListener] = responder @@ -151,7 +157,7 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { } await fulfillment(of: [expectationListener], timeout: 1) - guard let valueListener = valueListenerFromRequest else { + guard let asyncSequence else { XCTFail("Incoming responder didn't set up listener") return } @@ -174,7 +180,7 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { lastChangedAt: Date().unixSeconds, version: 2) let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - valueListener(.data(.success(remoteMutationSync))) + asyncSequence.send(.data(.success(remoteMutationSync))) await fulfillment(of: [syncReceivedNotification], timeout: 1) let finalLocalMetadata = try storageAdapter.queryMutationSyncMetadata(for: model.id, @@ -225,9 +231,12 @@ class ModelReconciliationDeleteTests: SyncEngineTestBase { break } } - _ = self.apiPlugin.subscribe(request: request, - valueListener: onUpdateListener, - completionListener: nil) + Task { + let sequence = self.apiPlugin.subscribe(request: request) + for try await event in sequence { + onUpdateListener(event) + } + } MockAWSIncomingEventReconciliationQueue.mockSend(event: .initialized) } default: diff --git a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift index 31ac246db1..1a085b0ae3 100644 --- a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift @@ -51,102 +51,107 @@ class MockAPICategoryPlugin: MessageReporter, // MARK: - Request-based GraphQL methods - func mutate(request: GraphQLRequest, - listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { - // This is a really weighty notification message, but needed for tests to be able to assert that a particular - // model is being mutated - notify("mutate(request) document: \(request.document); variables: \(String(describing: request.variables))") - - if let responder = responders[.mutateRequestListener] as? MutateRequestListenerResponder { - if let operation = responder.callback((request, listener)) { - return operation - } - } - let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) - let request = GraphQLOperationRequest(apiName: request.apiName, - operationType: .mutation, - document: request.document, - variables: request.variables, - responseType: request.responseType, - options: requestOptions) - let operation = MockGraphQLOperation(request: request, responseType: request.responseType) - - return operation - } +// func mutate(request: GraphQLRequest, +// listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { +// // This is a really weighty notification message, but needed for tests to be able to assert that a particular +// // model is being mutated +// notify("mutate(request) document: \(request.document); variables: \(String(describing: request.variables))") +// +// if let responder = responders[.mutateRequestListener] as? MutateRequestListenerResponder { +// if let operation = responder.callback((request, listener)) { +// return operation +// } +// } +// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) +// let request = GraphQLOperationRequest(apiName: request.apiName, +// operationType: .mutation, +// document: request.document, +// variables: request.variables, +// responseType: request.responseType, +// options: requestOptions) +// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) +// +// return operation +// } func mutate(request: GraphQLRequest) async throws -> GraphQLTask.Success { // This is a really weighty notification message, but needed for tests to be able to assert that a particular // model is being mutated notify("mutate(request) document: \(request.document); variables: \(String(describing: request.variables))") - - return .failure(.unknown("", "'", nil)) - } - - func query(request: GraphQLRequest, - listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { - notify("query(request:listener:) request: \(request)") - - if let responder = responders[.queryRequestListener] as? QueryRequestListenerResponder { - if let operation = responder.callback((request, listener)) { - return operation - } + if let responder = responders[.mutateRequestResponse] as? MutateRequestResponder { + return await responder.callback(request) } - let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) - let request = GraphQLOperationRequest(apiName: request.apiName, - operationType: .query, - document: request.document, - variables: request.variables, - responseType: request.responseType, - options: requestOptions) - let operation = MockGraphQLOperation(request: request, responseType: request.responseType) - - return operation + return .failure(.unknown("No request responder configured", "", nil)) } +// func query(request: GraphQLRequest, +// listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { +// notify("query(request:listener:) request: \(request)") +// +// if let responder = responders[.queryRequestListener] as? QueryRequestListenerResponder { +// if let operation = responder.callback((request, listener)) { +// return operation +// } +// } +// +// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) +// let request = GraphQLOperationRequest(apiName: request.apiName, +// operationType: .query, +// document: request.document, +// variables: request.variables, +// responseType: request.responseType, +// options: requestOptions) +// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) +// +// return operation +// } + func query(request: GraphQLRequest) async throws -> GraphQLTask.Success { notify("query(request:) request: \(request)") if let responder = responders[.queryRequestResponse] as? QueryRequestResponder { - - let result = responder.callback(request) - switch result { - case .success(let response): - return response - case .failure(let error): - throw error - } + return try await responder.callback(request) } - return .failure(.unknown("", "", nil)) - } - - func subscribe(request: GraphQLRequest, - valueListener: GraphQLSubscriptionOperation.InProcessListener?, - completionListener: GraphQLSubscriptionOperation.ResultListener?) - -> GraphQLSubscriptionOperation { - notify( - """ - subscribe(request:listener:) document: \(request.document); \ - variables: \(String(describing: request.variables)) - """ - ) - - if let responder = responders[.subscribeRequestListener] as? SubscribeRequestListenerResponder { - if let operation = responder.callback((request, valueListener, completionListener)) { - return operation - } - } - let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) - let request = GraphQLOperationRequest(apiName: request.apiName, - operationType: .subscription, - document: request.document, - variables: request.variables, - responseType: request.responseType, - options: requestOptions) - let operation = MockSubscriptionGraphQLOperation(request: request, responseType: request.responseType) - return operation +// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) +// let request = GraphQLOperationRequest(apiName: request.apiName, +// operationType: .query, +// document: request.document, +// variables: request.variables, +// responseType: request.responseType, +// options: requestOptions) +// + return .failure(.unknown("", "", nil)) } + +// func subscribe(request: GraphQLRequest, +// valueListener: GraphQLSubscriptionOperation.InProcessListener?, +// completionListener: GraphQLSubscriptionOperation.ResultListener?) +// -> GraphQLSubscriptionOperation { +// notify( +// """ +// subscribe(request:listener:) document: \(request.document); \ +// variables: \(String(describing: request.variables)) +// """ +// ) +// +// if let responder = responders[.subscribeRequestListener] as? SubscribeRequestListenerResponder { +// if let operation = responder.callback((request, valueListener, completionListener)) { +// return operation +// } +// } +// +// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) +// let request = GraphQLOperationRequest(apiName: request.apiName, +// operationType: .subscription, +// document: request.document, +// variables: request.variables, +// responseType: request.responseType, +// options: requestOptions) +// let operation = MockSubscriptionGraphQLOperation(request: request, responseType: request.responseType) +// return operation +// } func subscribe(request: GraphQLRequest) -> AmplifyAsyncThrowingSequence> { notify( @@ -155,7 +160,10 @@ class MockAPICategoryPlugin: MessageReporter, variables: \(String(describing: request.variables)) """ ) - + if let responder = responders[.subscribeRequestListener] as? SubscribeRequestListenerResponder { + return responder.callback(request) + } + let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) let request = GraphQLOperationRequest(apiName: request.apiName, operationType: .subscription, diff --git a/AmplifyTestCommon/Mocks/MockAPIResponders.swift b/AmplifyTestCommon/Mocks/MockAPIResponders.swift index 6fc5d44f62..c76e8a7890 100644 --- a/AmplifyTestCommon/Mocks/MockAPIResponders.swift +++ b/AmplifyTestCommon/Mocks/MockAPIResponders.swift @@ -9,33 +9,35 @@ import Amplify extension MockAPICategoryPlugin { enum ResponderKeys { - case queryRequestListener +// case queryRequestListener case queryRequestResponse case subscribeRequestListener - case mutateRequestListener +// case mutateRequestListener + case mutateRequestResponse } } -typealias QueryRequestListenerResponder = MockResponder< - (GraphQLRequest, GraphQLOperation.ResultListener?), - GraphQLOperation? -> +//typealias QueryRequestListenerResponder = MockResponder< +// (GraphQLRequest, GraphQLOperation.ResultListener?), +// GraphQLOperation? +//> -typealias QueryRequestResponder = MockResponder< +typealias QueryRequestResponder = MockAsyncThrowingResponder< GraphQLRequest, - GraphQLOperation.OperationResult + GraphQLResponse > -typealias MutateRequestListenerResponder = MockResponder< - (GraphQLRequest, GraphQLOperation.ResultListener?), - GraphQLOperation? +//typealias MutateRequestListenerResponder = MockResponder< +// (GraphQLRequest, GraphQLOperation.ResultListener?), +// GraphQLOperation? +//> + +typealias MutateRequestResponder = MockAsyncResponder< + GraphQLRequest, + GraphQLResponse > typealias SubscribeRequestListenerResponder = MockResponder< - ( GraphQLRequest, - GraphQLSubscriptionOperation.InProcessListener?, - GraphQLSubscriptionOperation.ResultListener? - ), - GraphQLSubscriptionOperation? + AmplifyAsyncThrowingSequence> > diff --git a/AmplifyTestCommon/Mocks/MockDataStoreCategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockDataStoreCategoryPlugin.swift index 10c6fb4787..5ba46da457 100644 --- a/AmplifyTestCommon/Mocks/MockDataStoreCategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockDataStoreCategoryPlugin.swift @@ -26,13 +26,15 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func save(_ model: M, where condition: QueryPredicate? = nil, - completion: (DataStoreResult) -> Void) { + completion: @escaping (DataStoreResult) -> Void) { notify("save") if let responder = responders[.saveModelListener] as? SaveModelResponder { - if let callback = responder.callback((model: model, - where: condition)) { - completion(callback) + Task { + if let callback = await responder.callback((model: model, + where: condition)) { + completion(callback) + } } } } @@ -45,12 +47,14 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func query(_ modelType: M.Type, byId id: String, - completion: (DataStoreResult) -> Void) { + completion: @escaping (DataStoreResult) -> Void) { notify("queryById") if let responder = responders[.queryByIdListener] as? QueryByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id)) { + completion(callback) + } } } } @@ -63,13 +67,15 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func query(_ modelType: M.Type, byIdentifier id: String, - completion: (DataStoreResult) -> Void) where M: ModelIdentifiable, + completion: @escaping (DataStoreResult) -> Void) where M: ModelIdentifiable, M.IdentifierFormat == ModelIdentifierFormat.Default { notify("queryByIdentifier") if let responder = responders[.queryByIdListener] as? QueryByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id)) { + completion(callback) + } } } } @@ -85,15 +91,17 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { where predicate: QueryPredicate?, sort sortInput: QuerySortInput?, paginate paginationInput: QueryPaginationInput?, - completion: (DataStoreResult<[M]>) -> Void) { + completion: @escaping (DataStoreResult<[M]>) -> Void) { notify("queryByPredicate") if let responder = responders[.queryModelsListener] as? QueryModelsResponder { - if let result = responder.callback((modelType: modelType, - where: predicate, - sort: sortInput, - paginate: paginationInput)) { - completion(result) + Task { + if let result = await responder.callback((modelType: modelType, + where: predicate, + sort: sortInput, + paginate: paginationInput)) { + completion(result) + } } } } @@ -105,7 +113,7 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("queryByPredicate") if let responder = responders[.queryModelsListener] as? QueryModelsResponder { - if let result = responder.callback((modelType: modelType, + if let result = await responder.callback((modelType: modelType, where: predicate, sort: sortInput, paginate: paginationInput)) { @@ -123,12 +131,14 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func query(_ modelType: M.Type, byIdentifier id: ModelIdentifier, - completion: (DataStoreResult) -> Void) where M: Model, M: ModelIdentifiable { + completion: @escaping (DataStoreResult) -> Void) where M: Model, M: ModelIdentifiable { notify("queryWithIdentifier") if let responder = responders[.queryByIdListener] as? QueryByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id.stringValue)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id.stringValue)) { + completion(callback) + } } } } @@ -143,12 +153,14 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func delete(_ modelType: M.Type, withId id: String, where predicate: QueryPredicate? = nil, - completion: (DataStoreResult) -> Void) { + completion: @escaping (DataStoreResult) -> Void) { notify("deleteById") if let responder = responders[.deleteByIdListener] as? DeleteByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id)) { + completion(callback) + } } } } @@ -167,8 +179,10 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("deleteByIdentifier") if let responder = responders[.deleteByIdListener] as? DeleteByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id)) { + completion(callback) + } } } } @@ -187,8 +201,10 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("deleteByIdentifier") if let responder = responders[.deleteByIdListener] as? DeleteByIdResponder { - if let callback = responder.callback((modelType: modelType, id: id.stringValue)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, id: id.stringValue)) { + completion(callback) + } } } } @@ -201,12 +217,14 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { func delete(_ modelType: M.Type, where predicate: QueryPredicate, - completion: (DataStoreResult) -> Void) { + completion: @escaping (DataStoreResult) -> Void) { notify("deleteModelTypeByPredicate") if let responder = responders[.deleteModelTypeListener] as? DeleteModelTypeResponder { - if let callback = responder.callback((modelType: modelType, where: predicate)) { - completion(callback) + Task { + if let callback = await responder.callback((modelType: modelType, where: predicate)) { + completion(callback) + } } } } @@ -222,9 +240,11 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("deleteByPredicate") if let responder = responders[.deleteModelListener] as? DeleteModelResponder { - if let callback = responder.callback((model: model, - where: predicate)) { - completion(callback) + Task { + if let callback = await responder.callback((model: model, + where: predicate)) { + completion(callback) + } } } } @@ -238,8 +258,10 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("clear") if let responder = responders[.clearListener] as? ClearResponder { - if let callback = responder.callback(()) { - completion(callback) + Task { + if let callback = await responder.callback(()) { + completion(callback) + } } } } @@ -252,8 +274,10 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("start") if let responder = responders[.clearListener] as? ClearResponder { - if let callback = responder.callback(()) { - completion(callback) + Task { + if let callback = await responder.callback(()) { + completion(callback) + } } } } @@ -266,8 +290,10 @@ class MockDataStoreCategoryPlugin: MessageReporter, DataStoreCategoryPlugin { notify("stop") if let responder = responders[.stopListener] as? StopResponder { - if let callback = responder.callback(()) { - completion(callback) + Task { + if let callback = await responder.callback(()) { + completion(callback) + } } } } diff --git a/AmplifyTestCommon/Mocks/MockResponder.swift b/AmplifyTestCommon/Mocks/MockResponder.swift index 32fd09407f..c18a627ca2 100644 --- a/AmplifyTestCommon/Mocks/MockResponder.swift +++ b/AmplifyTestCommon/Mocks/MockResponder.swift @@ -42,6 +42,22 @@ public struct MockResponder { } } +public struct MockAsyncResponder { + public typealias Callback = (Parameters) async -> Result + public let callback: Callback + public init(callback: @escaping Callback) { + self.callback = callback + } +} + +public struct MockAsyncThrowingResponder { + public typealias Callback = (Parameters) async throws -> Result + public let callback: Callback + public init(callback: @escaping Callback) { + self.callback = callback + } +} + /// A MockResponder variant whose callback throws public struct ThrowingMockResponder { public typealias Callback = (Parameters) throws -> Result diff --git a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift index 897e7303c3..10a5ff3853 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -14,31 +14,31 @@ import Combine class RetryableGraphQLOperationTests: XCTestCase { let testApiName = "apiName" - /// Given: a RetryableGraphQLOperation with 2 operations - /// When: the first one fails with a .signedOut error, the next one succeed with response - /// Then: return the success response - func testShouldRetryOperationWithSignedOutAuthError() async throws { - let expectation1 = expectation(description: "Operation 1 throws signed out auth error") - let operation1: () async throws -> GraphQLResponse = { - expectation1.fulfill() - throw APIError.operationError("", "", AuthError.signedOut("", "")) - } - - let expectation2 = expectation(description: "Operation 2 successfully finished") - let operation2: () async throws -> GraphQLResponse = { - expectation2.fulfill() - return .success("operation 2") - } - - let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() - let result = await RetryableGraphQLOperation(requestStream: publisher).run() - if case .success(.success(let string)) = result { - XCTAssertEqual(string, "operation 2") - } else { - XCTFail("Wrong result") - } - await fulfillment(of: [expectation1, expectation2], timeout: 1) - } +// /// Given: a RetryableGraphQLOperation with 2 operations +// /// When: the first one fails with a .signedOut error, the next one succeed with response +// /// Then: return the success response +// func testShouldRetryOperationWithSignedOutAuthError() async throws { +// let expectation1 = expectation(description: "Operation 1 throws signed out auth error") +// let operation1: () async throws -> GraphQLResponse = { +// expectation1.fulfill() +// throw APIError.operationError("", "", AuthError.signedOut("", "")) +// } +// +// let expectation2 = expectation(description: "Operation 2 successfully finished") +// let operation2: () async throws -> GraphQLResponse = { +// expectation2.fulfill() +// return .success("operation 2") +// } +// +// let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() +// let result = await RetryableGraphQLOperation(requestStream: publisher).run() +// if case .success(.success(let string)) = result { +// XCTAssertEqual(string, "operation 2") +// } else { +// XCTFail("Wrong result") +// } +// await fulfillment(of: [expectation1, expectation2], timeout: 1) +// } /// Given: a RetryableGraphQLOperation with 2 operations /// When: the first one fails with a .notAuthorized error, the next one succeed with response From fa725667295ab47d803dc530d6b1510eddec52e5 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 24 Apr 2024 17:31:37 -0700 Subject: [PATCH 07/23] fix broken unit test cases of AWSAPIPlugin --- .../AppSyncListProviderPaginationTests.swift | 17 ++++------- .../Core/AppSyncListProviderTests.swift | 28 ++++++------------- .../Operation/AWSRESTOperationTests.swift | 5 ++-- 3 files changed, 17 insertions(+), 33 deletions(-) diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift index 47710842e3..41e0a2f854 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift @@ -52,8 +52,7 @@ extension AppSyncListProviderTests { let nextPage = List(elements: [Comment4(content: "content"), Comment4(content: "content"), Comment4(content: "content")]) - let event: GraphQLOperation>.OperationResult = .success(.success(nextPage)) - return event + return .success(nextPage) } let elements = [Comment4(content: "content")] let provider = AppSyncListProvider(elements: elements, nextToken: "nextToken") @@ -84,10 +83,8 @@ extension AppSyncListProviderTests { } func testLoadedStateGetNextPageFailure_APIError() async { - mockAPIPlugin.responders[.queryRequestResponse] = - QueryRequestResponder> { _ in - let event: GraphQLOperation>.OperationResult = .failure(APIError.unknown("", "", nil)) - return event + mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder> { _ in + throw APIError.unknown("", "", nil) } let elements = [Comment4(content: "content")] let provider = AppSyncListProvider(elements: elements, nextToken: "nextToken") @@ -108,12 +105,10 @@ extension AppSyncListProviderTests { } func testLoadedStateGetNextPageFailure_GraphQLErrorResponse() async { - mockAPIPlugin.responders[.queryRequestResponse] = - QueryRequestResponder> { _ in - let event: GraphQLOperation>.OperationResult = .success( - .failure(GraphQLResponseError.error([GraphQLError]()))) - return event + mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder> { _ in + return .failure(GraphQLResponseError.error([GraphQLError]())) } + let elements = [Comment4(content: "content")] let provider = AppSyncListProvider(elements: elements, nextToken: "nextToken") guard case .loaded = provider.loadedState else { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift index 0116c9c206..39f434aaf6 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift @@ -133,8 +133,7 @@ class AppSyncListProviderTests: XCTestCase { ], "nextToken": "nextToken" ] - let event: GraphQLOperation.OperationResult = .success(.success(json)) - return event + return .success(json) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], @@ -168,10 +167,8 @@ class AppSyncListProviderTests: XCTestCase { } func testNotLoadedStateSynchronousLoadFailure() async { - mockAPIPlugin.responders[.queryRequestResponse] = - QueryRequestResponder { _ in - let event: GraphQLOperation.OperationResult = .failure(APIError.unknown("", "", nil)) - return event + mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder { _ in + throw APIError.unknown("", "", nil) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], @@ -217,8 +214,7 @@ class AppSyncListProviderTests: XCTestCase { ], "nextToken": "nextToken" ] - let event: GraphQLOperation.OperationResult = .success(.success(json)) - return event + return .success(json) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], @@ -252,10 +248,8 @@ class AppSyncListProviderTests: XCTestCase { } func testNotLoadedStateLoadWithCompletionFailure_APIError() async { - mockAPIPlugin.responders[.queryRequestResponse] = - QueryRequestResponder { _ in - let event: GraphQLOperation.OperationResult = .failure(APIError.unknown("", "", nil)) - return event + mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder { _ in + throw APIError.unknown("", "", nil) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], @@ -287,11 +281,8 @@ class AppSyncListProviderTests: XCTestCase { } func testNotLoadedStateLoadWithCompletionFailure_GraphQLErrorResponse() async { - mockAPIPlugin.responders[.queryRequestResponse] = - QueryRequestResponder { _ in - let event: GraphQLOperation.OperationResult = .success( - .failure(GraphQLResponseError.error([GraphQLError]()))) - return event + mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder { _ in + return .failure(GraphQLResponseError.error([GraphQLError]())) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], @@ -339,8 +330,7 @@ class AppSyncListProviderTests: XCTestCase { ], "nextToken": "nextToken" ] - let event: GraphQLOperation.OperationResult = .success(.success(json)) - return event + return .success(json) } let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift index c65f1257ac..633a358d48 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift @@ -33,7 +33,7 @@ class AWSRESTOperationTests: OperationTestBase { } // TODO: Fix this test - func testGetReturnsOperation() throws { + func testGetReturnsOperation() async throws { try setUpPlugin(endpointType: .rest) // Use this as a semaphore to ensure the task is cleaned up before proceeding to the next test @@ -49,8 +49,7 @@ class AWSRESTOperationTests: OperationTestBase { } XCTAssertNotNil(operation.request) - - waitForExpectations(timeout: 1.00) + await fulfillment(of: [listenerWasInvoked], timeout: 1) } func testGetFailsWithBadAPIName() throws { From b44a9ceb71834f72feb2adceb581b83f3e37444d Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 25 Apr 2024 14:44:45 -0700 Subject: [PATCH 08/23] fix broken AWSDataStorePlugin unit test cases --- .../OutgoingMutationQueueTests.swift | 67 ++++++++++--------- .../API/RetryableGraphQLOperationTests.swift | 9 +-- 2 files changed, 39 insertions(+), 37 deletions(-) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift index 915c493072..dbaf391b64 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift @@ -140,21 +140,45 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { // pre-load the MutationEvent table with mutation data let mutationEventSaved = expectation(description: "Preloaded mutation event saved") mutationEventSaved.expectedFulfillmentCount = 2 - for id in 1 ... 2 { - let postId = "pendingPost-\(id)" - let pendingPost = Post(id: postId, - title: "pendingPost-\(id) title", - content: "pendingPost-\(id) content", - createdAt: .now()) - - let pendingPostJSON = try pendingPost.toJSON() - let event = MutationEvent(id: "mutation-\(id)", - modelId: "pendingPost-\(id)", + + let posts = (1...2).map { Post( + id: "pendingPost-\($0)", + title: "pendingPost-\($0) title", + content: "pendingPost-\($0) content", + createdAt: .now() + )} + + let postMutationEvents = try posts.map { + let pendingPostJSON = try $0.toJSON() + return MutationEvent( + id: "mutation-\($0.id)", + modelId: $0.id, modelName: Post.modelName, json: pendingPostJSON, mutationType: .create, - createdAt: .now()) + createdAt: .now() + ) + } + + apiPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + if let variables = request.variables?["input"] as? [String: Any], + let postId = variables["id"] as? String, + let post = posts.first(where: { $0.id == postId }) + { + let anyModel = try! post.eraseToAnyModel() + let remoteSyncMetadata = MutationSyncMetadata(modelId: post.id, + modelName: Post.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) + } + return .failure(.unknown("No matching post found", "", nil)) + } + + postMutationEvents.forEach { event in storageAdapter.save(event) { result in switch result { case .failure(let dataStoreError): @@ -163,7 +187,6 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { mutationEventSaved.fulfill() } } - } await fulfillment(of: [mutationEventSaved], timeout: 1.0) @@ -195,13 +218,6 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { XCTFail("Should not trigger outbox status event") } -// if outboxStatusReceivedCurrentCount == 1 { -// XCTAssertFalse(outboxStatusEvent.isEmpty) -// outboxStatusOnStart.fulfill() -// } else { -// XCTAssertFalse(outboxStatusEvent.isEmpty) -// outboxStatusOnMutationEnqueued.fulfill() -// } } guard try await HubListenerTestUtilities.waitForListener(with: hubListener, timeout: 5.0) else { @@ -211,7 +227,6 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { let mutation1Sent = expectation(description: "Create mutation 1 sent to API category") let mutation2Sent = expectation(description: "Create mutation 2 sent to API category") - mutation2Sent.isInverted = true apiPlugin.listeners.append { message in if message.contains("createPost") && message.contains("pendingPost-1") { mutation1Sent.fulfill() @@ -227,17 +242,9 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { try await startAmplify() } + await fulfillment(of: [outboxStatusOnStart, outboxStatusOnMutationEnqueued],timeout: 5.0) - - await fulfillment( - of: [ - outboxStatusOnStart, - outboxStatusOnMutationEnqueued, - mutation1Sent, - mutation2Sent - ], - timeout: 5.0 - ) + await fulfillment(of: [mutation1Sent, mutation2Sent], timeout: 5, enforceOrder: true) Amplify.Hub.removeListener(hubListener) } diff --git a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift index 10a5ff3853..9822c6b3d4 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -85,13 +85,8 @@ class RetryableGraphQLOperationTests: XCTestCase { let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() let result = await RetryableGraphQLOperation(requestStream: publisher).run() - if case .failure(.operationError(_, _, let error)) = result { - XCTAssert(error is APIError) - if case .unknown(let description, _, _) = error as! APIError { - XCTAssertEqual(description, "~Unknown~") - } else { - XCTFail("Wrong result") - } + if case .failure(.unknown(let description, _, _)) = result { + XCTAssertEqual(description, "~Unknown~") } else { XCTFail("Wrong result") } From becb5f5d60ac4977ae96ff21e1b9a85468a5b2c9 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 25 Apr 2024 17:20:35 -0700 Subject: [PATCH 09/23] fix OutgoingMutationQueue test case --- .../OutgoingMutationQueueTests.swift | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift index dbaf391b64..610a6b0ffa 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift @@ -129,10 +129,11 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { /// - Given: A sync-configured DataStore /// - When: /// - I start syncing with mutation events already in the database + /// - keep the mutaiton sync request in process /// - Then: /// - The mutation queue delivers the first previously loaded event func testMutationQueueLoadsPendingMutations() async throws { - + let timeout: TimeInterval = 5 await tryOrFail { try setUpStorageAdapter() } @@ -165,6 +166,7 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { let postId = variables["id"] as? String, let post = posts.first(where: { $0.id == postId }) { + try? await Task.sleep(seconds: timeout + 1) let anyModel = try! post.eraseToAnyModel() let remoteSyncMetadata = MutationSyncMetadata(modelId: post.id, modelName: Post.modelName, @@ -210,10 +212,6 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { case 2: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnMutationEnqueued.fulfill() - case 3, 4: - XCTAssertFalse(outboxStatusEvent.isEmpty) - case 5: - XCTAssertTrue(outboxStatusEvent.isEmpty) default: XCTFail("Should not trigger outbox status event") } @@ -227,6 +225,7 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { let mutation1Sent = expectation(description: "Create mutation 1 sent to API category") let mutation2Sent = expectation(description: "Create mutation 2 sent to API category") + mutation2Sent.isInverted = true apiPlugin.listeners.append { message in if message.contains("createPost") && message.contains("pendingPost-1") { mutation1Sent.fulfill() @@ -242,9 +241,12 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { try await startAmplify() } - await fulfillment(of: [outboxStatusOnStart, outboxStatusOnMutationEnqueued],timeout: 5.0) - - await fulfillment(of: [mutation1Sent, mutation2Sent], timeout: 5, enforceOrder: true) + await fulfillment(of: [ + outboxStatusOnStart, + outboxStatusOnMutationEnqueued, + mutation1Sent, + mutation2Sent + ], timeout: timeout) Amplify.Hub.removeListener(hubListener) } From 802d7068a56ac09f59eb9fafbaa19cc8496a208c Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 25 Apr 2024 17:21:30 -0700 Subject: [PATCH 10/23] remove unused methods --- .../Mocks/MockAPICategoryPlugin.swift | 80 ------------------- 1 file changed, 80 deletions(-) diff --git a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift index 1a085b0ae3..d1336f66a1 100644 --- a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift +++ b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift @@ -51,29 +51,6 @@ class MockAPICategoryPlugin: MessageReporter, // MARK: - Request-based GraphQL methods -// func mutate(request: GraphQLRequest, -// listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { -// // This is a really weighty notification message, but needed for tests to be able to assert that a particular -// // model is being mutated -// notify("mutate(request) document: \(request.document); variables: \(String(describing: request.variables))") -// -// if let responder = responders[.mutateRequestListener] as? MutateRequestListenerResponder { -// if let operation = responder.callback((request, listener)) { -// return operation -// } -// } -// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) -// let request = GraphQLOperationRequest(apiName: request.apiName, -// operationType: .mutation, -// document: request.document, -// variables: request.variables, -// responseType: request.responseType, -// options: requestOptions) -// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) -// -// return operation -// } - func mutate(request: GraphQLRequest) async throws -> GraphQLTask.Success { // This is a really weighty notification message, but needed for tests to be able to assert that a particular // model is being mutated @@ -85,27 +62,6 @@ class MockAPICategoryPlugin: MessageReporter, return .failure(.unknown("No request responder configured", "", nil)) } -// func query(request: GraphQLRequest, -// listener: GraphQLOperation.ResultListener?) -> GraphQLOperation { -// notify("query(request:listener:) request: \(request)") -// -// if let responder = responders[.queryRequestListener] as? QueryRequestListenerResponder { -// if let operation = responder.callback((request, listener)) { -// return operation -// } -// } -// -// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) -// let request = GraphQLOperationRequest(apiName: request.apiName, -// operationType: .query, -// document: request.document, -// variables: request.variables, -// responseType: request.responseType, -// options: requestOptions) -// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) -// -// return operation -// } func query(request: GraphQLRequest) async throws -> GraphQLTask.Success { notify("query(request:) request: \(request)") @@ -114,44 +70,8 @@ class MockAPICategoryPlugin: MessageReporter, return try await responder.callback(request) } -// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) -// let request = GraphQLOperationRequest(apiName: request.apiName, -// operationType: .query, -// document: request.document, -// variables: request.variables, -// responseType: request.responseType, -// options: requestOptions) -// return .failure(.unknown("", "", nil)) } - -// func subscribe(request: GraphQLRequest, -// valueListener: GraphQLSubscriptionOperation.InProcessListener?, -// completionListener: GraphQLSubscriptionOperation.ResultListener?) -// -> GraphQLSubscriptionOperation { -// notify( -// """ -// subscribe(request:listener:) document: \(request.document); \ -// variables: \(String(describing: request.variables)) -// """ -// ) -// -// if let responder = responders[.subscribeRequestListener] as? SubscribeRequestListenerResponder { -// if let operation = responder.callback((request, valueListener, completionListener)) { -// return operation -// } -// } -// -// let requestOptions = GraphQLOperationRequest.Options(pluginOptions: nil) -// let request = GraphQLOperationRequest(apiName: request.apiName, -// operationType: .subscription, -// document: request.document, -// variables: request.variables, -// responseType: request.responseType, -// options: requestOptions) -// let operation = MockSubscriptionGraphQLOperation(request: request, responseType: request.responseType) -// return operation -// } func subscribe(request: GraphQLRequest) -> AmplifyAsyncThrowingSequence> { notify( From 5ad91f709b4efdad27a25c2525f9a2d95a358f2e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 25 Apr 2024 19:05:30 -0700 Subject: [PATCH 11/23] fix broken test cases of SyncMutationToCloudOperationTests --- ...MutationErrorFromCloudOperationTests.swift | 2574 +++++++++-------- .../SyncMutationToCloudOperationTests.swift | 902 +++--- 2 files changed, 1714 insertions(+), 1762 deletions(-) diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift index 402341d803..fc2133fa69 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift @@ -1,1280 +1,1296 @@ -//// -//// Copyright Amazon.com Inc. or its affiliates. -//// All Rights Reserved. -//// -//// SPDX-License-Identifier: Apache-2.0 -//// // -//import Foundation -//import XCTest -//import Combine -// -//@testable import Amplify -//@testable import AmplifyTestCommon -//@testable import AWSPluginsCore -//@testable import AWSDataStorePlugin -// -//// swiftlint:disable type_body_length -//// swiftlint:disable type_name -//// swiftlint:disable file_length -//class ProcessMutationErrorFromCloudOperationTests: XCTestCase { -// // swiftlint:enable type_name -// let defaultAsyncWaitTimeout = 10.0 -// var mockAPIPlugin: MockAPICategoryPlugin! -// var storageAdapter: StorageEngineAdapter! -// var localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let queue = OperationQueue() -// let reconciliationQueue = MockReconciliationQueue() -// -// override func setUp() async throws { -// await tryOrFail { -// try await setUpWithAPI() -// } -// storageAdapter = MockSQLiteStorageEngineAdapter() -// -// ModelRegistry.register(modelType: Post.self) -// ModelRegistry.register(modelType: Comment.self) -// } -// -// /// - Given: APIError -// /// - When: -// /// - APIError contains AuthError indicating user is not authenticated -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForAuthError() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// let authError = AuthError.signedOut("User is not authenticated", "Authenticate user", nil) -// let apiError = APIError.operationError("not signed in", "Sign In User", authError) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let actualAPIError = amplifyError as? APIError, -// case let .operationError(_, _, underlyingError) = actualAPIError, -// let authError = underlyingError as? AuthError, -// case .signedOut = authError else { -// XCTFail("Should be `signedOut` error") -// return -// } -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// apiError: apiError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: APIError -// /// - When: -// /// - APIError unrelated to AuthError -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForAPIError() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// let apiError = APIError.operationError("Operation failed", "", nil) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let actualAPIError = amplifyError as? APIError, -// case .operationError = actualAPIError else { -// XCTFail("Missing APIError.operationError") -// return -// } -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// apiError: apiError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: GraphQLError with no errors -// /// - When: -// /// - GraphQLError with no errors -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrors() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.unknown("", "", nil) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .unknown = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: GraphQLError with no error -// /// - When: -// /// - GraphQLError with no error -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrorsArray() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.error([]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// XCTAssertEqual(errors.count, 0) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: GraphQLError more than one error to handle -// /// - When: -// /// - GraphQLError with multiple errors -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithMultipleErrors() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let error = GraphQLError(message: "error message") -// let graphQLResponseError = GraphQLResponseError>.error([error, error]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// XCTAssertEqual(errors.count, 2) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: GraphQLError ConditionalCheck -// /// - When: -// /// - GraphQLError with errors containing type ConditionalCheck -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testProcessMutationErrorFromCloudOperationSuccessForConditionalCheck() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.conditionalCheck)]) -// -// let expectHubEvent = expectation(description: "Hub is notified") -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in -// if payload.eventName == "DataStore.conditionalSaveFailed" { -// expectHubEvent.fulfill() -// } -// } -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// XCTAssertEqual(errors.count, 1) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectHubEvent, expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// Amplify.Hub.removeListener(hubListener) -// } -// -// func testProcessMutationErrorFromCloudOperationSuccessForUnauthorized() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unauthorized)]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// XCTAssertEqual(errors.count, 1) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// func testProcessMutationErrorFromCloudOperationSuccessForOperationDisabled() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.operationDisabled)]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError") -// return -// } -// XCTAssertEqual(errors.count, 1) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// func testProcessMutationErrorFromCloudOperationSuccessForUnknownError() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unknown("unknownErrorType"))]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let configuration = custom(errorHandler: { error in -// guard let dataStoreError = error as? DataStoreError, -// case let .api(amplifyError, mutationEventOptional) = dataStoreError else { -// XCTFail("Expected API error with mutationEvent") -// return -// } -// guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, -// case .error(let errors) = graphQLResponseError else { -// XCTFail("Missing GraphQLResponseError.unknown") -// return -// } -// XCTAssertEqual(errors.count, 1) -// guard let actualMutationEvent = mutationEventOptional else { -// XCTFail("Missing mutationEvent for api error") -// return -// } -// XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) -// expectErrorHandlerCalled.fulfill() -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - Error does not contain the remote model -// /// - Then: -// /// - Unexpected scenario, there should never be an conflict unhandled error without error.data -// func testConflictUnhandledReturnsErrorForMissingRemoteModel() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) -// let graphQLError = GraphQLError(message: "conflict unhandled", -// extensions: ["errorType": .string(AppSyncErrorType.conflictUnhandled.rawValue)]) -// let graphQLResponseError = GraphQLResponseError>.error([graphQLError]) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case let .failure(error) = result, -// let dataStoreError = error as? DataStoreError, -// case .unknown = dataStoreError else { -// XCTFail("Should have failed with DataStoreError.unknown") -// return -// } -// -// XCTAssertEqual(dataStoreError.errorDescription, "Missing remote model from the response from AppSync.") -// expectCompletion.fulfill() -// } -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `create` -// /// - Then: -// /// - Unexpected scenario, there should never get a conflict for create mutations -// func testConflictUnhandledReturnsErrorForCreateMutation() throws { -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) -// let remotePost = Post(title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 1) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case let .failure(error) = result, -// let dataStoreError = error as? DataStoreError, -// case .unknown = dataStoreError else { -// XCTFail("Should have failed with DataStoreError.unknown") -// return -// } -// -// XCTAssertEqual(dataStoreError.errorDescription, "Should never get conflict unhandled for create mutation") -// expectCompletion.fulfill() -// } -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `delete`, remote model is deleted. -// /// - Then: -// /// - No-op, operation finishes successfully -// func testConflictUnhandledForDeleteMutationAndDeletedRemoteModel() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: true, -// version: 1) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retryLocal` -// /// - Then: -// /// - API is called to delete with local model -// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryLocal() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// -// var eventListenerOptional: GraphQLOperation>.ResultListener? -// let apiMutateCalled = expectation(description: "API was called") -// mockAPIPlugin.responders[.mutateRequestListener] = -// MutateRequestListenerResponder> { request, eventListener in -// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { -// XCTFail("The document variables property doesn't contain a valid input") -// return nil -// } -// XCTAssert(input["id"] as? String == localPost.id) -// XCTAssert(request.document.contains("DeletePost")) -// eventListenerOptional = eventListener -// apiMutateCalled.fulfill() -// return nil -// } -// -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let configuration = custom(conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.retryLocal) -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// reconciliationQueue: reconciliationQueue, -// completion: completion) -// -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) -// guard let eventListener = eventListenerOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, -// modelName: remotePost.modelName, -// deleted: true, -// lastChangedAt: 0, -// version: 3) -// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) -// eventListener(.success(.success(mockResponse))) -// -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retry(model)` -// /// - Then: -// /// - API is called with the model from the conflict handler result -// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryModel() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// self.assertSuccessfulNil(result) -// expectCompletion.fulfill() -// } -// -// let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) -// var eventListenerOptional: GraphQLOperation>.ResultListener? -// let apiMutateCalled = expectation(description: "API was called") -// mockAPIPlugin.responders[.mutateRequestListener] = -// MutateRequestListenerResponder> { request, eventListener in -// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { -// XCTFail("The document variables property doesn't contain a valid input") -// return nil -// } -// XCTAssert(input["title"] as? String == retryModel.title) -// XCTAssertTrue(request.document.contains("UpdatePost")) -// eventListenerOptional = eventListener -// apiMutateCalled.fulfill() -// return nil -// } -// -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let configuration = custom(conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.retry(retryModel)) -// }) -// -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// reconciliationQueue: reconciliationQueue, -// completion: completion) -// -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) -// guard let eventListener = eventListenerOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, -// modelName: remotePost.modelName, -// deleted: false, -// lastChangedAt: 0, -// version: 3) -// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) -// eventListener(.success(.success(mockResponse))) -// -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `delete`, remote model is an update, conflict handler returns `.applyRemote` -// /// - Then: -// /// - Local Store is reconciled(recreated) to remote model, result mutationEvent is `update` -// func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsApplyRemote() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result, -// let mutationEvent = mutationEventOptional else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertEqual(mutationEvent.mutationType, "update") -// XCTAssertEqual(mutationEvent.modelId, remotePost.id) -// expectCompletion.fulfill() -// } -// -// let modelSavedEvent = expectation(description: "model saved event") -// modelSavedEvent.expectedFulfillmentCount = 2 -// let storageAdapter = MockSQLiteStorageEngineAdapter() -// storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in -// guard let savedPost = model as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// XCTAssertEqual(savedPost.title, remotePost.title) -// modelSavedEvent.fulfill() -// completion(.success(model)) -// } -// -// storageAdapter.responders[.saveModelCompletion] = -// SaveModelCompletionResponder { metadata, completion in -// XCTAssertEqual(metadata.deleted, false) -// XCTAssertEqual(metadata.version, 2) -// modelSavedEvent.fulfill() -// completion(.success(metadata)) -// } -// -// let expectHubEvent = expectation(description: "Hub is notified") -// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in -// if payload.eventName == "DataStore.syncReceived" { -// expectHubEvent.fulfill() -// } -// } -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// -// wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// Amplify.Hub.removeListener(hubListener) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `update`, remote model is deleted -// /// - Then: -// /// - Local model is deleted, result mutationEvent is `delete` -// func testConflictUnhandledForUpdateMutationAndDeletedRemoteModel() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: true, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result, -// let mutationEvent = mutationEventOptional else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertEqual(mutationEvent.mutationType, "delete") -// XCTAssertEqual(mutationEvent.modelId, localPost.id) -// expectCompletion.fulfill() -// } -// -// let modelDeletedEvent = expectation(description: "model deleted event") -// let metadataSavedEvent = expectation(description: "metadata saved event") -// let storageAdapter = MockSQLiteStorageEngineAdapter() -// storageAdapter.shouldReturnErrorOnDeleteMutation = false -// storageAdapter.responders[.deleteUntypedModel] = DeleteUntypedModelCompletionResponder { _ in -// modelDeletedEvent.fulfill() -// return .emptyResult -// } -// storageAdapter.responders[.saveModelCompletion] = -// SaveModelCompletionResponder { metadata, completion in -// XCTAssertEqual(metadata.deleted, true) -// XCTAssertEqual(metadata.version, 2) -// metadataSavedEvent.fulfill() -// completion(.success(metadata)) -// } -// -// let expectHubEvent = expectation(description: "Hub is notified") -// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in -// if payload.eventName == "DataStore.syncReceived" { -// expectHubEvent.fulfill() -// } -// } -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// -// queue.addOperation(operation) -// -// wait(for: [modelDeletedEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// Amplify.Hub.removeListener(hubListener) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `update`, remote model is an update, conflict handler returns `.applyRemote` -// /// - Then: -// /// - Local model is updated with remote model data, result mutationEvent is `update` -// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsApplyRemote() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result, -// let mutationEvent = mutationEventOptional else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertEqual(mutationEvent.mutationType, "update") -// XCTAssertEqual(mutationEvent.modelId, remotePost.id) -// expectCompletion.fulfill() -// } -// -// let storageAdapter = MockSQLiteStorageEngineAdapter() -// let modelSavedEvent = expectation(description: "model saved event") -// let metadataSavedEvent = expectation(description: "metadata saved event") -// storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in -// guard let savedPost = model as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// XCTAssertEqual(savedPost.title, remotePost.title) -// modelSavedEvent.fulfill() -// completion(.success(model)) -// } -// storageAdapter.responders[.saveModelCompletion] = -// SaveModelCompletionResponder { metadata, completion in -// XCTAssertEqual(metadata.deleted, false) -// XCTAssertEqual(metadata.version, 2) -// metadataSavedEvent.fulfill() -// completion(.success(metadata)) -// } -// -// let expectHubEvent = expectation(description: "Hub is notified") -// let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in -// if payload.eventName == "DataStore.syncReceived" { -// expectHubEvent.fulfill() -// } -// } -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let configuration = custom(conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.applyRemote) -// }) -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [metadataSavedEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// Amplify.Hub.removeListener(hubListener) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` -// /// - Then: -// /// - API is called to update with the local model -// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryLocal() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertNil(mutationEventOptional) -// expectCompletion.fulfill() -// } -// -// var eventListenerOptional: GraphQLOperation>.ResultListener? -// let apiMutateCalled = expectation(description: "API was called") -// mockAPIPlugin.responders[.mutateRequestListener] = -// MutateRequestListenerResponder> { request, eventListener in -// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { -// XCTFail("The document variables property doesn't contain a valid input") -// return nil -// } -// XCTAssert(input["title"] as? String == localPost.title) -// XCTAssertTrue(request.document.contains("UpdatePost")) -// eventListenerOptional = eventListener -// apiMutateCalled.fulfill() -// return nil -// } -// -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let configuration = custom(conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.retryLocal) -// }) -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// reconciliationQueue: reconciliationQueue, -// completion: completion) -// -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) -// guard let eventListener = eventListenerOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, -// modelName: remotePost.modelName, -// deleted: false, -// lastChangedAt: 0, -// version: 3) -// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) -// eventListener(.success(.success(mockResponse))) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retry(Model)` -// /// - Then: -// /// - API is called to update the model from the conflict handler result -// func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryModel() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertNil(mutationEventOptional) -// expectCompletion.fulfill() -// } -// -// let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) -// var eventListenerOptional: GraphQLOperation>.ResultListener? -// let apiMutateCalled = expectation(description: "API was called") -// mockAPIPlugin.responders[.mutateRequestListener] = -// MutateRequestListenerResponder> { request, eventListener in -// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { -// XCTFail("The document variables property doesn't contain a valid input") -// return nil -// } -// XCTAssert(input["title"] as? String == retryModel.title) -// XCTAssertTrue(request.document.contains("UpdatePost")) -// eventListenerOptional = eventListener -// apiMutateCalled.fulfill() -// return nil -// } -// -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let configuration = custom(conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.retry(retryModel)) -// }) -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// reconciliationQueue: reconciliationQueue, -// completion: completion) -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) -// guard let eventListener = eventListenerOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, -// modelName: remotePost.modelName, -// deleted: false, -// lastChangedAt: 0, -// version: 3) -// let mockResponse = MutationSync(model: try localPost.eraseToAnyModel(), syncMetadata: updatedMetadata) -// eventListener(.success(.success(mockResponse))) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// - Given: Conflict Unhandled error -// /// - When: -// /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` -// /// - API is called to update with local model and response contains error -// /// - Then: -// /// - `DataStoreErrorHandler` is called -// func testConflictUnhandledSyncToCloudReturnsError() throws { -// let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) -// guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, -// deleted: false, -// version: 2) else { -// XCTFail("Couldn't get GraphQL response with remote post") -// return -// } -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// guard case .success(let mutationEventOptional) = result else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertNil(mutationEventOptional) -// expectCompletion.fulfill() -// } -// -// var eventListenerOptional: GraphQLOperation>.ResultListener? -// let apiMutateCalled = expectation(description: "API was called") -// mockAPIPlugin.responders[.mutateRequestListener] = -// MutateRequestListenerResponder> { request, eventListener in -// guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { -// XCTFail("The document variables property doesn't contain a valid input") -// return nil -// } -// XCTAssert(input["title"] as? String == localPost.title) -// XCTAssertTrue(request.document.contains("UpdatePost")) -// eventListenerOptional = eventListener -// apiMutateCalled.fulfill() -// return nil -// } -// -// let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") -// let expectErrorHandlerCalled = expectation(description: "Expect error handler called") -// let configuration = custom(errorHandler: { _ in -// expectErrorHandlerCalled.fulfill() -// }, conflictHandler: { data, resolve in -// guard let localPost = data.local as? Post, -// let remotePost = data.remote as? Post else { -// XCTFail("Couldn't get Posts from local and remote data") -// return -// } -// -// XCTAssertEqual(localPost.title, "localTitle") -// XCTAssertEqual(remotePost.title, "remoteTitle") -// expectConflicthandlerCalled.fulfill() -// resolve(.retryLocal) -// }) -// let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLResponseError, -// completion: completion) -// queue.addOperation(operation) -// -// wait(for: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) -// guard let eventListener = eventListenerOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// let error = GraphQLError(message: "some other error") -// eventListener(.success(.failure(.error([error])))) -// -// wait(for: [expectErrorHandlerCalled], timeout: defaultAsyncWaitTimeout) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// /// Given: GraphQL "OperationDisabled" error -// /// - When: -// /// - API is called and response contains an "OperationDisabled" error -// /// - Then: -// /// - Completion handler is successfully called -// func testProcessOperationDisabledError() throws { -// let post = Post(title: "localTitle", content: "localContent", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: post, modelSchema: Post.schema, mutationType: .create) -// let expectCompletion = expectation(description: "Expect to complete error processing") -// let completion: (Result) -> Void = { result in -// if case .success(let mutationEventOptional) = result { -// XCTAssertNil(mutationEventOptional) -// expectCompletion.fulfill() -// return -// } -// XCTFail("Should have been successful") -// } -// -// let graphQLError = try getGraphQLResponseError(withRemote: post, -// deleted: false, -// version: 0, -// errorType: .operationDisabled) -// -// let operation = ProcessMutationErrorFromCloudOperation( -// dataStoreConfiguration: DataStoreConfiguration.testDefault(), -// mutationEvent: mutationEvent, -// api: mockAPIPlugin, -// storageAdapter: storageAdapter, -// graphQLResponseError: graphQLError, -// completion: completion) -// -// queue.addOperation(operation) -// wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) -// } -//} -// -//extension ProcessMutationErrorFromCloudOperationTests { -// private func setUpCore() async throws -> AmplifyConfiguration { -// await Amplify.reset() -// -// let dataStorePublisher = DataStorePublisher() -// let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), -// storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, -// dataStorePublisher: dataStorePublisher, -// validAPIPluginKey: "MockAPICategoryPlugin", -// validAuthPluginKey: "MockAuthCategoryPlugin") -// try Amplify.add(plugin: dataStorePlugin) -// let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ -// "awsDataStorePlugin": true -// ]) -// -// let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) -// -// return amplifyConfig -// } -// -// private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { -// mockAPIPlugin = MockAPICategoryPlugin() -// try Amplify.add(plugin: mockAPIPlugin) -// -// let apiConfig = APICategoryConfiguration(plugins: [ -// "MockAPICategoryPlugin": true -// ]) -// let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) -// return amplifyConfig -// } -// -// private func setUpWithAPI() async throws { -// let configWithoutAPI = try await setUpCore() -// let configWithAPI = try setUpAPICategory(config: configWithoutAPI) -// try Amplify.configure(configWithAPI) -// } -// -// private func assertSuccessfulNil(_ result: Result) { -// guard case .success(let mutationEventOptional) = result else { -// XCTFail("Should have been successful") -// return -// } -// XCTAssertNil(mutationEventOptional) -// } -// -// private func getGraphQLResponseError(withRemote post: Post = Post(title: "remoteTitle", -// content: "remoteContent", -// createdAt: .now()), -// deleted: Bool = false, -// version: Int = 1, -// errorType: AppSyncErrorType? = .conflictUnhandled) -// throws -> GraphQLResponseError>? { -// let data = Data(try post.toJSON().utf8) -// let decoder = JSONDecoder() -// decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy -// let remoteData = try decoder.decode(JSONValue.self, from: data) -// guard case var .object(remoteDataObject) = remoteData else { -// return nil -// } -// remoteDataObject["_deleted"] = .boolean(deleted) -// remoteDataObject["_lastChangedAt"] = .number(123) -// remoteDataObject["_version"] = .number(Double(version)) -// remoteDataObject["__typename"] = .string(post.modelName) -// if let errorType = errorType { -// let graphQLError = GraphQLError(message: "error message", -// extensions: ["errorType": .string(errorType.rawValue), -// "data": .object(remoteDataObject)]) -// return GraphQLResponseError>.error([graphQLError]) -// } else { -// let graphQLError = GraphQLError(message: "error message") -// return GraphQLResponseError>.error([graphQLError]) -// } -// } -// -// private func graphQLError(_ errorType: AppSyncErrorType) -> GraphQLError { -// GraphQLError(message: "message", -// locations: nil, -// path: nil, -// extensions: ["errorType": .string(errorType.rawValue)]) -// } -// -// private func custom(errorHandler: DataStoreErrorHandler? = nil, -// conflictHandler: (DataStoreConflictHandler)? = nil) -> DataStoreConfiguration { -// if let conflictHandler = conflictHandler, let errorHandler = errorHandler { -// #if os(watchOS) -// return .custom(errorHandler: errorHandler, -// conflictHandler: conflictHandler, -// disableSubscriptions: { false }) -// #else -// return .custom(errorHandler: errorHandler, -// conflictHandler: conflictHandler) -// #endif -// } else if let errorHandler = errorHandler { -// #if os(watchOS) -// return .custom(errorHandler: errorHandler, -// disableSubscriptions: { false }) -// #else -// return .custom(errorHandler: errorHandler) -// #endif -// } else if let conflictHandler = conflictHandler { -// #if os(watchOS) -// return .custom(conflictHandler: conflictHandler, -// disableSubscriptions: { false }) -// #else -// return .custom(conflictHandler: conflictHandler) -// #endif -// } -// return .testDefault() -// } -//} +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import XCTest +import Combine + +@testable import Amplify +@testable import AmplifyTestCommon +@testable import AWSPluginsCore +@testable import AWSDataStorePlugin + +// swiftlint:disable type_body_length +// swiftlint:disable type_name +// swiftlint:disable file_length +class ProcessMutationErrorFromCloudOperationTests: XCTestCase { + // swiftlint:enable type_name + let defaultAsyncWaitTimeout = 10.0 + var mockAPIPlugin: MockAPICategoryPlugin! + var storageAdapter: StorageEngineAdapter! + var localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let queue = OperationQueue() + let reconciliationQueue = MockReconciliationQueue() + + override func setUp() async throws { + await tryOrFail { + try await setUpWithAPI() + } + storageAdapter = MockSQLiteStorageEngineAdapter() + + ModelRegistry.register(modelType: Post.self) + ModelRegistry.register(modelType: Comment.self) + } + + /// - Given: APIError + /// - When: + /// - APIError contains AuthError indicating user is not authenticated + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForAuthError() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + let authError = AuthError.signedOut("User is not authenticated", "Authenticate user", nil) + let apiError = APIError.operationError("not signed in", "Sign In User", authError) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let actualAPIError = amplifyError as? APIError, + case let .operationError(_, _, underlyingError) = actualAPIError, + let authError = underlyingError as? AuthError, + case .signedOut = authError else { + XCTFail("Should be `signedOut` error") + return + } + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + apiError: apiError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: APIError + /// - When: + /// - APIError unrelated to AuthError + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForAPIError() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + let apiError = APIError.operationError("Operation failed", "", nil) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let actualAPIError = amplifyError as? APIError, + case .operationError = actualAPIError else { + XCTFail("Missing APIError.operationError") + return + } + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + apiError: apiError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: GraphQLError with no errors + /// - When: + /// - GraphQLError with no errors + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrors() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.unknown("", "", nil) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .unknown = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: GraphQLError with no error + /// - When: + /// - GraphQLError with no error + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithNoErrorsArray() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.error([]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + XCTAssertEqual(errors.count, 0) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: GraphQLError more than one error to handle + /// - When: + /// - GraphQLError with multiple errors + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForGraphQLResponseWithMultipleErrors() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let error = GraphQLError(message: "error message") + let graphQLResponseError = GraphQLResponseError>.error([error, error]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + XCTAssertEqual(errors.count, 2) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: GraphQLError ConditionalCheck + /// - When: + /// - GraphQLError with errors containing type ConditionalCheck + /// - Then: + /// - `DataStoreErrorHandler` is called + func testProcessMutationErrorFromCloudOperationSuccessForConditionalCheck() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.conditionalCheck)]) + + let expectHubEvent = expectation(description: "Hub is notified") + let expectCompletion = expectation(description: "Expect to complete error processing") + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in + if payload.eventName == "DataStore.conditionalSaveFailed" { + expectHubEvent.fulfill() + } + } + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + XCTAssertEqual(errors.count, 1) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectHubEvent, expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + Amplify.Hub.removeListener(hubListener) + } + + func testProcessMutationErrorFromCloudOperationSuccessForUnauthorized() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unauthorized)]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + XCTAssertEqual(errors.count, 1) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + func testProcessMutationErrorFromCloudOperationSuccessForOperationDisabled() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.operationDisabled)]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError") + return + } + XCTAssertEqual(errors.count, 1) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + func testProcessMutationErrorFromCloudOperationSuccessForUnknownError() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + let graphQLResponseError = GraphQLResponseError>.error([graphQLError(.unknown("unknownErrorType"))]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let configuration = custom(errorHandler: { error in + guard let dataStoreError = error as? DataStoreError, + case let .api(amplifyError, mutationEventOptional) = dataStoreError else { + XCTFail("Expected API error with mutationEvent") + return + } + guard let graphQLResponseError = amplifyError as? GraphQLResponseError>, + case .error(let errors) = graphQLResponseError else { + XCTFail("Missing GraphQLResponseError.unknown") + return + } + XCTAssertEqual(errors.count, 1) + guard let actualMutationEvent = mutationEventOptional else { + XCTFail("Missing mutationEvent for api error") + return + } + XCTAssertEqual(actualMutationEvent.id, mutationEvent.id) + expectErrorHandlerCalled.fulfill() + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectErrorHandlerCalled, expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - Error does not contain the remote model + /// - Then: + /// - Unexpected scenario, there should never be an conflict unhandled error without error.data + func testConflictUnhandledReturnsErrorForMissingRemoteModel() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) + let graphQLError = GraphQLError(message: "conflict unhandled", + extensions: ["errorType": .string(AppSyncErrorType.conflictUnhandled.rawValue)]) + let graphQLResponseError = GraphQLResponseError>.error([graphQLError]) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case let .failure(error) = result, + let dataStoreError = error as? DataStoreError, + case .unknown = dataStoreError else { + XCTFail("Should have failed with DataStoreError.unknown") + return + } + + XCTAssertEqual(dataStoreError.errorDescription, "Missing remote model from the response from AppSync.") + expectCompletion.fulfill() + } + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `create` + /// - Then: + /// - Unexpected scenario, there should never get a conflict for create mutations + func testConflictUnhandledReturnsErrorForCreateMutation() throws { + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .create) + let remotePost = Post(title: "remoteTitle", content: "remoteContent", createdAt: .now()) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 1) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case let .failure(error) = result, + let dataStoreError = error as? DataStoreError, + case .unknown = dataStoreError else { + XCTFail("Should have failed with DataStoreError.unknown") + return + } + + XCTAssertEqual(dataStoreError.errorDescription, "Should never get conflict unhandled for create mutation") + expectCompletion.fulfill() + } + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `delete`, remote model is deleted. + /// - Then: + /// - No-op, operation finishes successfully + func testConflictUnhandledForDeleteMutationAndDeletedRemoteModel() throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: true, + version: 1) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retryLocal` + /// - Then: + /// - API is called to delete with local model + func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryLocal() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 2) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + + let apiMutateCalled = expectation(description: "API was called") + mockAPIPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, + modelName: remotePost.modelName, + deleted: true, + lastChangedAt: 0, + version: 3) + + guard let variables = request.variables, + let input = variables["input"] as? [String: Any] + else { + XCTFail("The document variables property doesn't contain a valid input") + return .failure(.unknown("", "", nil)) + } + XCTAssert(input["id"] as? String == localPost.id) + XCTAssert(request.document.contains("DeletePost")) + apiMutateCalled.fulfill() + + guard let mockResponse = ( + try? localPost.eraseToAnyModel() + ).map({ MutationSync(model:$0 , syncMetadata: updatedMetadata) }) + else { + XCTFail("Failed to wrap to AnyModel") + return .failure(.unknown("", "", nil)) + } + return .success(mockResponse) + } + + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let configuration = custom(conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.retryLocal) + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + reconciliationQueue: reconciliationQueue, + completion: completion) + + queue.addOperation(operation) + + await fulfillment(of: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retry(model)` + /// - Then: + /// - API is called with the model from the conflict handler result + func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryModel() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 2) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + self.assertSuccessfulNil(result) + expectCompletion.fulfill() + } + + let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) + + let apiMutateCalled = expectation(description: "API was called") + mockAPIPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { + XCTFail("The document variables property doesn't contain a valid input") + return .failure(.unknown("The document variables property doesn't contain a valid input", "", nil)) + } + XCTAssert(input["title"] as? String == retryModel.title) + XCTAssertTrue(request.document.contains("UpdatePost")) + apiMutateCalled.fulfill() + + let updatedMetadata = MutationSyncMetadata(modelId: remotePost.id, + modelName: remotePost.modelName, + deleted: false, + lastChangedAt: 0, + version: 3) + guard let mockResponse = (try? localPost.eraseToAnyModel()).map({ MutationSync(model: $0, syncMetadata: updatedMetadata) }) else { + XCTFail("Failed to wrap to AnyModel") + return .failure(.unknown("Failed to wrap to AnyModel", "", nil)) + } + + return .success(mockResponse) + } + + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let configuration = custom(conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.retry(retryModel)) + }) + + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + reconciliationQueue: reconciliationQueue, + completion: completion) + + queue.addOperation(operation) + + await fulfillment(of: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `delete`, remote model is an update, conflict handler returns `.applyRemote` + /// - Then: + /// - Local Store is reconciled(recreated) to remote model, result mutationEvent is `update` + func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsApplyRemote() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .delete) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 2) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result, + let mutationEvent = mutationEventOptional else { + XCTFail("Should have been successful") + return + } + XCTAssertEqual(mutationEvent.mutationType, "update") + XCTAssertEqual(mutationEvent.modelId, remotePost.id) + expectCompletion.fulfill() + } + + let modelSavedEvent = expectation(description: "model saved event") + modelSavedEvent.expectedFulfillmentCount = 2 + let storageAdapter = MockSQLiteStorageEngineAdapter() + storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in + guard let savedPost = model as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + XCTAssertEqual(savedPost.title, remotePost.title) + modelSavedEvent.fulfill() + completion(.success(model)) + } + + storageAdapter.responders[.saveModelCompletion] = + SaveModelCompletionResponder { metadata, completion in + XCTAssertEqual(metadata.deleted, false) + XCTAssertEqual(metadata.version, 2) + modelSavedEvent.fulfill() + completion(.success(metadata)) + } + + let expectHubEvent = expectation(description: "Hub is notified") + let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in + if payload.eventName == "DataStore.syncReceived" { + expectHubEvent.fulfill() + } + } + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + + await fulfillment(of: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectHubEvent], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) + Amplify.Hub.removeListener(hubListener) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `update`, remote model is deleted + /// - Then: + /// - Local model is deleted, result mutationEvent is `delete` + func testConflictUnhandledForUpdateMutationAndDeletedRemoteModel() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: true, + version: 2 + ) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result, + let mutationEvent = mutationEventOptional else { + XCTFail("Should have been successful") + return + } + XCTAssertEqual(mutationEvent.mutationType, "delete") + XCTAssertEqual(mutationEvent.modelId, localPost.id) + expectCompletion.fulfill() + } + + let modelDeletedEvent = expectation(description: "model deleted event") + let metadataSavedEvent = expectation(description: "metadata saved event") + let storageAdapter = MockSQLiteStorageEngineAdapter() + storageAdapter.shouldReturnErrorOnDeleteMutation = false + storageAdapter.responders[.deleteUntypedModel] = DeleteUntypedModelCompletionResponder { _ in + modelDeletedEvent.fulfill() + return .emptyResult + } + storageAdapter.responders[.saveModelCompletion] = + SaveModelCompletionResponder { metadata, completion in + XCTAssertEqual(metadata.deleted, true) + XCTAssertEqual(metadata.version, 2) + metadataSavedEvent.fulfill() + completion(.success(metadata)) + } + + let expectHubEvent = expectation(description: "Hub is notified") + let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in + if payload.eventName == "DataStore.syncReceived" { + expectHubEvent.fulfill() + } + } + + let operation = ProcessMutationErrorFromCloudOperation( + dataStoreConfiguration: .testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion + ) + queue.addOperation(operation) + + await fulfillment(of: [ + modelDeletedEvent, + metadataSavedEvent, + expectHubEvent, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) + Amplify.Hub.removeListener(hubListener) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `update`, remote model is an update, conflict handler returns `.applyRemote` + /// - Then: + /// - Local model is updated with remote model data, result mutationEvent is `update` + func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsApplyRemote() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: false, + version: 2 + ) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result, + let mutationEvent = mutationEventOptional else { + XCTFail("Should have been successful") + return + } + XCTAssertEqual(mutationEvent.mutationType, "update") + XCTAssertEqual(mutationEvent.modelId, remotePost.id) + expectCompletion.fulfill() + } + + let storageAdapter = MockSQLiteStorageEngineAdapter() + let modelSavedEvent = expectation(description: "model saved event") + let metadataSavedEvent = expectation(description: "metadata saved event") + storageAdapter.responders[.saveUntypedModel] = SaveUntypedModelResponder { model, completion in + guard let savedPost = model as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + XCTAssertEqual(savedPost.title, remotePost.title) + modelSavedEvent.fulfill() + completion(.success(model)) + } + storageAdapter.responders[.saveModelCompletion] = + SaveModelCompletionResponder { metadata, completion in + XCTAssertEqual(metadata.deleted, false) + XCTAssertEqual(metadata.version, 2) + metadataSavedEvent.fulfill() + completion(.success(metadata)) + } + + let expectHubEvent = expectation(description: "Hub is notified") + let hubListener = Amplify.Hub.listen(to: .dataStore) { payload in + if payload.eventName == "DataStore.syncReceived" { + expectHubEvent.fulfill() + } + } + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let configuration = custom(conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.applyRemote) + }) + + let operation = ProcessMutationErrorFromCloudOperation( + dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion + ) + queue.addOperation(operation) + + await fulfillment(of: [ + expectConflicthandlerCalled, + modelSavedEvent, + metadataSavedEvent, + expectHubEvent, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) + Amplify.Hub.removeListener(hubListener) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` + /// - Then: + /// - API is called to update with the local model + func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryLocal() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: false, + version: 2 + ) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result else { + XCTFail("Should have been successful") + return + } + XCTAssertNil(mutationEventOptional) + expectCompletion.fulfill() + } + + let apiMutateCalled = expectation(description: "API was called") + mockAPIPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { + XCTFail("The document variables property doesn't contain a valid input") + return .failure(.unknown("The document variables property doesn't contain a valid input", "", nil)) + } + XCTAssert(input["title"] as? String == localPost.title) + XCTAssertTrue(request.document.contains("UpdatePost")) + apiMutateCalled.fulfill() + + let updatedMetadata = MutationSyncMetadata( + modelId: remotePost.id, + modelName: remotePost.modelName, + deleted: false, + lastChangedAt: 0, + version: 3 + ) + + guard let mockResponse = (try? localPost.eraseToAnyModel()) + .map({ MutationSync(model: $0, syncMetadata: updatedMetadata) }) + else { + XCTFail("Failed to wrap to AnyModel") + return .failure(.unknown("Failed to wrap AnyModel", "", nil)) + } + + return .success(mockResponse) + } + + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let configuration = custom(conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.retryLocal) + }) + + let operation = ProcessMutationErrorFromCloudOperation( + dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + reconciliationQueue: reconciliationQueue, + completion: completion + ) + queue.addOperation(operation) + + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `update`, remote model is an update, conflict handler returns `.retry(Model)` + /// - Then: + /// - API is called to update the model from the conflict handler result + func testConflictUnhandledUpdateMutationAndUpdatedRemoteReturnsRetryModel() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 2) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result else { + XCTFail("Should have been successful") + return + } + XCTAssertNil(mutationEventOptional) + expectCompletion.fulfill() + } + + let retryModel = Post(title: "retryModel", content: "retryContent", createdAt: .now()) + + let apiMutateCalled = expectation(description: "API was called") + mockAPIPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { + XCTFail("The document variables property doesn't contain a valid input") + return .failure(.unknown("The document variables property doesn't contain a valid input", "", nil)) + } + XCTAssert(input["title"] as? String == retryModel.title) + XCTAssertTrue(request.document.contains("UpdatePost")) + apiMutateCalled.fulfill() + let updatedMetadata = MutationSyncMetadata( + modelId: remotePost.id, + modelName: remotePost.modelName, + deleted: false, + lastChangedAt: 0, + version: 3 + ) + guard let mockResponse = (try? localPost.eraseToAnyModel()) + .map({ MutationSync(model: $0, syncMetadata: updatedMetadata) }) + else { + XCTFail("Failed to wrap to AnyModel") + return .failure(.unknown("Failed to wrap to AnyModel", "", nil)) + } + return .success(mockResponse) + } + + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let configuration = custom(conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.retry(retryModel)) + }) + let operation = ProcessMutationErrorFromCloudOperation( + dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + reconciliationQueue: reconciliationQueue, + completion: completion + ) + queue.addOperation(operation) + + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) + } + + /// - Given: Conflict Unhandled error + /// - When: + /// - MutationType is `update`, remote model is an update, conflict handler returns `.retryLocal` + /// - API is called to update with local model and response contains error + /// - Then: + /// - `DataStoreErrorHandler` is called + func testConflictUnhandledSyncToCloudReturnsError() async throws { + let localPost = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let remotePost = Post(id: localPost.id, title: "remoteTitle", content: "remoteContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: localPost, modelSchema: localPost.schema, mutationType: .update) + guard let graphQLResponseError = try getGraphQLResponseError(withRemote: remotePost, + deleted: false, + version: 2) else { + XCTFail("Couldn't get GraphQL response with remote post") + return + } + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + guard case .success(let mutationEventOptional) = result else { + XCTFail("Should have been successful") + return + } + XCTAssertNil(mutationEventOptional) + expectCompletion.fulfill() + } + + + let apiMutateCalled = expectation(description: "API was called") + mockAPIPlugin.responders[.mutateRequestResponse] = MutateRequestResponder> { request in + guard let variables = request.variables, let input = variables["input"] as? [String: Any] else { + XCTFail("The document variables property doesn't contain a valid input") + return .failure(.unknown("The document variables property doesn't contain a valid input", "", nil)) + } + + XCTAssert(input["title"] as? String == localPost.title) + XCTAssertTrue(request.document.contains("UpdatePost")) + apiMutateCalled.fulfill() + return .failure(.error([GraphQLError(message: "some other error")])) + } + + let expectConflicthandlerCalled = expectation(description: "Expect conflict handler called") + let expectErrorHandlerCalled = expectation(description: "Expect error handler called") + let configuration = custom(errorHandler: { _ in + expectErrorHandlerCalled.fulfill() + }, conflictHandler: { data, resolve in + guard let localPost = data.local as? Post, + let remotePost = data.remote as? Post else { + XCTFail("Couldn't get Posts from local and remote data") + return + } + + XCTAssertEqual(localPost.title, "localTitle") + XCTAssertEqual(remotePost.title, "remoteTitle") + expectConflicthandlerCalled.fulfill() + resolve(.retryLocal) + }) + let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLResponseError, + completion: completion) + queue.addOperation(operation) + + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectErrorHandlerCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) + } + + /// Given: GraphQL "OperationDisabled" error + /// - When: + /// - API is called and response contains an "OperationDisabled" error + /// - Then: + /// - Completion handler is successfully called + func testProcessOperationDisabledError() async throws { + let post = Post(title: "localTitle", content: "localContent", createdAt: .now()) + let mutationEvent = try MutationEvent(model: post, modelSchema: Post.schema, mutationType: .create) + let expectCompletion = expectation(description: "Expect to complete error processing") + let completion: (Result) -> Void = { result in + if case .success(let mutationEventOptional) = result { + XCTAssertNil(mutationEventOptional) + expectCompletion.fulfill() + return + } + XCTFail("Should have been successful") + } + + let graphQLError = try getGraphQLResponseError(withRemote: post, + deleted: false, + version: 0, + errorType: .operationDisabled) + + let operation = ProcessMutationErrorFromCloudOperation( + dataStoreConfiguration: DataStoreConfiguration.testDefault(), + mutationEvent: mutationEvent, + api: mockAPIPlugin, + storageAdapter: storageAdapter, + graphQLResponseError: graphQLError, + completion: completion) + + queue.addOperation(operation) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) + } +} + +extension ProcessMutationErrorFromCloudOperationTests { + private func setUpCore() async throws -> AmplifyConfiguration { + await Amplify.reset() + + let dataStorePublisher = DataStorePublisher() + let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), + storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, + dataStorePublisher: dataStorePublisher, + validAPIPluginKey: "MockAPICategoryPlugin", + validAuthPluginKey: "MockAuthCategoryPlugin") + try Amplify.add(plugin: dataStorePlugin) + let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ + "awsDataStorePlugin": true + ]) + + let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) + + return amplifyConfig + } + + private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { + mockAPIPlugin = MockAPICategoryPlugin() + try Amplify.add(plugin: mockAPIPlugin) + + let apiConfig = APICategoryConfiguration(plugins: [ + "MockAPICategoryPlugin": true + ]) + let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) + return amplifyConfig + } + + private func setUpWithAPI() async throws { + let configWithoutAPI = try await setUpCore() + let configWithAPI = try setUpAPICategory(config: configWithoutAPI) + try Amplify.configure(configWithAPI) + } + + private func assertSuccessfulNil(_ result: Result) { + guard case .success(let mutationEventOptional) = result else { + XCTFail("Should have been successful") + return + } + XCTAssertNil(mutationEventOptional) + } + + private func getGraphQLResponseError(withRemote post: Post = Post(title: "remoteTitle", + content: "remoteContent", + createdAt: .now()), + deleted: Bool = false, + version: Int = 1, + errorType: AppSyncErrorType? = .conflictUnhandled) + throws -> GraphQLResponseError>? { + let data = Data(try post.toJSON().utf8) + let decoder = JSONDecoder() + decoder.dateDecodingStrategy = ModelDateFormatting.decodingStrategy + let remoteData = try decoder.decode(JSONValue.self, from: data) + guard case var .object(remoteDataObject) = remoteData else { + return nil + } + remoteDataObject["_deleted"] = .boolean(deleted) + remoteDataObject["_lastChangedAt"] = .number(123) + remoteDataObject["_version"] = .number(Double(version)) + remoteDataObject["__typename"] = .string(post.modelName) + if let errorType = errorType { + let graphQLError = GraphQLError(message: "error message", + extensions: ["errorType": .string(errorType.rawValue), + "data": .object(remoteDataObject)]) + return GraphQLResponseError>.error([graphQLError]) + } else { + let graphQLError = GraphQLError(message: "error message") + return GraphQLResponseError>.error([graphQLError]) + } + } + + private func graphQLError(_ errorType: AppSyncErrorType) -> GraphQLError { + GraphQLError(message: "message", + locations: nil, + path: nil, + extensions: ["errorType": .string(errorType.rawValue)]) + } + + private func custom(errorHandler: DataStoreErrorHandler? = nil, + conflictHandler: (DataStoreConflictHandler)? = nil) -> DataStoreConfiguration { + if let conflictHandler = conflictHandler, let errorHandler = errorHandler { + #if os(watchOS) + return .custom(errorHandler: errorHandler, + conflictHandler: conflictHandler, + disableSubscriptions: { false }) + #else + return .custom(errorHandler: errorHandler, + conflictHandler: conflictHandler) + #endif + } else if let errorHandler = errorHandler { + #if os(watchOS) + return .custom(errorHandler: errorHandler, + disableSubscriptions: { false }) + #else + return .custom(errorHandler: errorHandler) + #endif + } else if let conflictHandler = conflictHandler { + #if os(watchOS) + return .custom(conflictHandler: conflictHandler, + disableSubscriptions: { false }) + #else + return .custom(conflictHandler: conflictHandler) + #endif + } + return .testDefault() + } +} diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift index 256295250e..d594fe65e6 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift @@ -1,484 +1,420 @@ -//// -//// Copyright Amazon.com Inc. or its affiliates. -//// All Rights Reserved. -//// -//// SPDX-License-Identifier: Apache-2.0 -//// // -//import Foundation -//import XCTest -//import Combine -// -//@testable import Amplify -//@testable import AmplifyTestCommon -//@testable import AWSPluginsCore -//@testable import AWSDataStorePlugin -// -//class SyncMutationToCloudOperationTests: XCTestCase { -// let defaultAsyncWaitTimeout = 2.0 -// let secondsInADay = 60 * 60 * 24 -// var mockAPIPlugin: MockAPICategoryPlugin! -// -// var reachabilityPublisher: CurrentValueSubject! -// var publisher: AnyPublisher { -// return reachabilityPublisher.eraseToAnyPublisher() -// } -// -// override func setUp() async throws { -// reachabilityPublisher = CurrentValueSubject(ReachabilityUpdate(isOnline: false)) -// await tryOrFail { -// try await setUpWithAPI() -// } -// ModelRegistry.register(modelType: Post.self) -// ModelRegistry.register(modelType: Comment.self) -// } -// -// func testRetryOnTimeoutOfWaiting() async throws { -// let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") -// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") -// let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") -// -// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) -// -// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? -// var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? -// -// var numberOfTimesEntered = 0 -// let responder = MutateRequestListenerResponder> { request, eventListener in -// if numberOfTimesEntered == 0 { -// let requestInputVersion = request.variables.flatMap { $0["input"] as? [String: Any] }.flatMap { $0["_version"] as? Int } -// XCTAssertEqual(requestInputVersion, 10) -// listenerFromFirstRequestOptional = eventListener -// expectFirstCallToAPIMutate.fulfill() -// } else if numberOfTimesEntered == 1 { -// listenerFromSecondRequestOptional = eventListener -// expectSecondCallToAPIMutate.fulfill() -// } else { -// XCTFail("This should not be called more than once") -// } -// numberOfTimesEntered += 1 -// // We could return an operation here, but we don't need to. -// // The main reason for having this responder is to get the eventListener. -// // the eventListener block will execute the the call to validateResponseFromCloud -// return nil -// } -// mockAPIPlugin.responders[.mutateRequestListener] = responder -// -// let completion: GraphQLOperation>.ResultListener = { _ in -// expectMutationRequestCompletion.fulfill() -// } -// -// let model = MockSynced(id: "id-1") -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: mutationEvent, -// getLatestSyncMetadata: { -// MutationSyncMetadata( -// modelId: model.id, -// modelName: model.modelName, -// deleted: false, -// lastChangedAt: Date().unixSeconds, -// version: 10 -// ) -// }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: completion -// ) -// let queue = OperationQueue() -// queue.addOperation(operation) -// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) -// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// let urlError = URLError(URLError.notConnectedToInternet) -// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) -// await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) -// -// guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// -// let anyModel = try model.eraseToAnyModel() -// let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, -// modelName: model.modelName, -// deleted: false, -// lastChangedAt: Date().unixSeconds, -// version: 2) -// let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) -// listenerFromSecondRequest(.success(.success(remoteMutationSync))) -// // waitForExpectations(timeout: 1) -// await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// func testRetryOnChangeReachability() async throws { -// let mockRequestRetryPolicy = MockRequestRetryablePolicy() -// let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) -// mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) -// -// let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") -// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") -// let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") -// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) -// -// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? -// var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? -// -// var numberOfTimesEntered = 0 -// let responder = MutateRequestListenerResponder> { _, eventListener in -// if numberOfTimesEntered == 0 { -// listenerFromFirstRequestOptional = eventListener -// expectFirstCallToAPIMutate.fulfill() -// } else if numberOfTimesEntered == 1 { -// listenerFromSecondRequestOptional = eventListener -// expectSecondCallToAPIMutate.fulfill() -// } else { -// XCTFail("This should not be called more than once") -// } -// numberOfTimesEntered += 1 -// // We could return an operation here, but we don't need to. -// // The main reason for having this responder is to get the eventListener. -// // the eventListener block will execute the the call to validateResponseFromCloud -// return nil -// } -// mockAPIPlugin.responders[.mutateRequestListener] = responder -// -// let completion: GraphQLOperation>.ResultListener = { _ in -// expectMutationRequestCompletion.fulfill() -// } -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: mutationEvent, -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// requestRetryablePolicy: mockRequestRetryPolicy, -// completion: completion -// ) -// let queue = OperationQueue() -// queue.addOperation(operation) -// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) -// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// let urlError = URLError(URLError.notConnectedToInternet) -// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) -// reachabilityPublisher.send(ReachabilityUpdate(isOnline: true)) -// -// await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) -// guard let listenerFromSecondRequest = listenerFromSecondRequestOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// let model = MockSynced(id: "id-1") -// let anyModel = try model.eraseToAnyModel() -// let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, -// modelName: model.modelName, -// deleted: false, -// lastChangedAt: Date().unixSeconds, -// version: 2) -// let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) -// listenerFromSecondRequest(.success(.success(remoteMutationSync))) -// await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) -// } -// -// func testAbilityToCancel() async throws { -// let mockRequestRetryPolicy = MockRequestRetryablePolicy() -// let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) -// mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) -// -// let expectMutationRequestFailed = expectation(description: "Expect to fail mutation request") -// let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") -// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) -// let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) -// -// var listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? -// -// var numberOfTimesEntered = 0 -// let responder = MutateRequestListenerResponder> { _, eventListener in -// if numberOfTimesEntered == 0 { -// listenerFromFirstRequestOptional = eventListener -// expectFirstCallToAPIMutate.fulfill() -// } else { -// XCTFail("This should not be called more than once") -// } -// numberOfTimesEntered += 1 -// // We could return an operation here, but we don't need to. -// // The main reason for having this responder is to get the eventListener. -// // the eventListener block will execute the the call to validateResponseFromCloud -// return nil -// } -// mockAPIPlugin.responders[.mutateRequestListener] = responder -// -// let completion: GraphQLOperation>.ResultListener = { asyncEvent in -// switch asyncEvent { -// case .failure: -// expectMutationRequestFailed.fulfill() -// default: -// break -// } -// } -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: mutationEvent, -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// requestRetryablePolicy: mockRequestRetryPolicy, -// completion: completion -// ) -// let queue = OperationQueue() -// queue.addOperation(operation) -// await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) -// guard let listenerFromFirstRequest = listenerFromFirstRequestOptional else { -// XCTFail("Listener was not called through MockAPICategoryPlugin") -// return -// } -// -// let urlError = URLError(URLError.notConnectedToInternet) -// listenerFromFirstRequest(.failure(APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) -// -// // At this point, we will be "waiting forever" to retry our request or until the operation is canceled -// operation.cancel() -// await fulfillment(of: [expectMutationRequestFailed], timeout: defaultAsyncWaitTimeout) -// } -// -// // MARK: - GetRetryAdviceIfRetryableTests -// -// func testGetRetryAdvice_NetworkError_RetryTrue() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// -// let error = APIError.networkError("", nil, URLError(.userAuthenticationRequired)) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertTrue(advice.shouldRetry) -// } -// -// func testGetRetryAdvice_HTTPStatusError401WithMultiAuth_RetryTrue() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: MockMultiAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// let response = HTTPURLResponse(url: URL(string: "http://localhost")!, -// statusCode: 401, -// httpVersion: nil, -// headerFields: nil)! -// let error = APIError.httpStatusError(401, response) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertTrue(advice.shouldRetry) -// } -// -// /// Given: Model with multiple auth types. Mutation requests always fail with 401 error code -// /// When: Mutating model fails with 401 -// /// Then: DataStore will try again with each auth type and eventually fails -// func testGetRetryAdviceForEachModelAuthTypeThenFail_HTTPStatusError401() async throws { -// var numberOfTimesEntered = 0 -// let mutationEvent = try createMutationEvent() -// let authStrategy = MockMultiAuthModeStrategy() -// let expectedNumberOfTimesEntered = authStrategy.authTypesFor(schema: mutationEvent.schema, operation: .create).count -// -// let expectCalllToApiMutateNTimesAndFail = expectation(description: "Call API.mutate \(expectedNumberOfTimesEntered) times and then fail") -// -// let response = HTTPURLResponse(url: URL(string: "http://localhost")!, -// statusCode: 401, -// httpVersion: nil, -// headerFields: nil)! -// let error = APIError.httpStatusError(401, response) -// -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: mutationEvent, -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: authStrategy, -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { result in -// if numberOfTimesEntered == expectedNumberOfTimesEntered { -// expectCalllToApiMutateNTimesAndFail.fulfill() -// -// } else { -// XCTFail("API.mutate was called incorrect amount of times, expected: \(expectedNumberOfTimesEntered), was : \(numberOfTimesEntered)") -// } -// } -// ) -// -// let responder = MutateRequestListenerResponder> { request, eventListener in -// let requestOptions = GraphQLOperationRequest>.Options(pluginOptions: nil) -// let request = GraphQLOperationRequest>(apiName: request.apiName, -// operationType: .mutation, -// document: request.document, -// variables: request.variables, -// responseType: request.responseType, -// options: requestOptions) -// let operation = MockGraphQLOperation(request: request, responseType: request.responseType) -// -// numberOfTimesEntered += 1 -// -// DispatchQueue.global().sync { -// // Fail with 401 status code -// eventListener!(.failure(error)) -// } -// -// return operation -// } -// -// mockAPIPlugin.responders[.mutateRequestListener] = responder -// -// let queue = OperationQueue() -// queue.addOperation(operation) -// -// await fulfillment(of: [expectCalllToApiMutateNTimesAndFail], timeout: defaultAsyncWaitTimeout) -// } -// -// func testGetRetryAdvice_OperationErrorAuthErrorWithMultiAuth_RetryTrue() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: MockMultiAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// -// let authError = AuthError.notAuthorized("", "", nil) -// let error = APIError.operationError("", "", authError) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertTrue(advice.shouldRetry) -// } -// -// func testGetRetryAdvice_OperationErrorAuthErrorWithSingleAuth_RetryFalse() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// -// let authError = AuthError.notAuthorized("", "", nil) -// let error = APIError.operationError("", "", authError) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertFalse(advice.shouldRetry) -// } -// -// func testGetRetryAdvice_OperationErrorAuthErrorSessionExpired_RetryTrue() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// -// let authError = AuthError.sessionExpired("", "", nil) -// let error = APIError.operationError("", "", authError) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertTrue(advice.shouldRetry) -// } -// -// func testGetRetryAdvice_OperationErrorAuthErrorSignedOut_RetryTrue() async throws { -// let operation = await SyncMutationToCloudOperation( -// mutationEvent: try createMutationEvent(), -// getLatestSyncMetadata: { nil }, -// api: mockAPIPlugin, -// authModeStrategy: AWSDefaultAuthModeStrategy(), -// networkReachabilityPublisher: publisher, -// currentAttemptNumber: 1, -// completion: { _ in } -// ) -// -// let authError = AuthError.signedOut("", "", nil) -// let error = APIError.operationError("", "", authError) -// let advice = operation.getRetryAdviceIfRetryable(error: error) -// XCTAssertTrue(advice.shouldRetry) -// } -// -// private func createMutationEvent() throws -> MutationEvent { -// let post1 = Post(title: "post1", content: "content1", createdAt: .now()) -// return try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) -// } -// -//} -// -//public class MockMultiAuthModeStrategy: AuthModeStrategy { -// public weak var authDelegate: AuthModeStrategyDelegate? -// required public init() {} -// -// public func authTypesFor(schema: ModelSchema, -// operation: ModelOperation) -> AWSAuthorizationTypeIterator { -// return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) -// } -// -// public func authTypesFor(schema: ModelSchema, -// operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { -// return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) -// } -//} -// -//extension SyncMutationToCloudOperationTests { -// private func setUpCore() async throws -> AmplifyConfiguration { -// await Amplify.reset() -// -// let dataStorePublisher = DataStorePublisher() -// let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), -// storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, -// dataStorePublisher: dataStorePublisher, -// validAPIPluginKey: "MockAPICategoryPlugin", -// validAuthPluginKey: "MockAuthCategoryPlugin") -// -// try Amplify.add(plugin: dataStorePlugin) -// let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ -// "awsDataStorePlugin": true -// ]) -// -// let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) -// -// return amplifyConfig -// } -// -// private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { -// mockAPIPlugin = MockAPICategoryPlugin() -// try Amplify.add(plugin: mockAPIPlugin) -// -// let apiConfig = APICategoryConfiguration(plugins: [ -// "MockAPICategoryPlugin": true -// ]) -// let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) -// return amplifyConfig -// } -// -// private func setUpWithAPI() async throws { -// let configWithoutAPI = try await setUpCore() -// let configWithAPI = try setUpAPICategory(config: configWithoutAPI) -// try Amplify.configure(configWithAPI) -// } -//} +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import XCTest +import Combine + +@testable import Amplify +@testable import AmplifyTestCommon +@testable import AWSPluginsCore +@testable import AWSDataStorePlugin + +class SyncMutationToCloudOperationTests: XCTestCase { + let defaultAsyncWaitTimeout = 2.0 + let secondsInADay = 60 * 60 * 24 + var mockAPIPlugin: MockAPICategoryPlugin! + + var reachabilityPublisher: CurrentValueSubject! + var publisher: AnyPublisher { + return reachabilityPublisher.eraseToAnyPublisher() + } + + override func setUp() async throws { + reachabilityPublisher = CurrentValueSubject(ReachabilityUpdate(isOnline: false)) + await tryOrFail { + try await setUpWithAPI() + } + ModelRegistry.register(modelType: Post.self) + ModelRegistry.register(modelType: Comment.self) + } + + func testRetryOnTimeoutOfWaiting() async throws { + let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") + let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") + let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") + + let model = MockSynced(id: "id-1") + let post1 = Post(title: "post1", content: "content1", createdAt: .now()) + let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) + + var numberOfTimesEntered = 0 + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } + if numberOfTimesEntered == 0 { + let requestInputVersion = request.variables.flatMap { $0["input"] as? [String: Any] }.flatMap { $0["_version"] as? Int } + XCTAssertEqual(requestInputVersion, 10) + expectFirstCallToAPIMutate.fulfill() + let urlError = URLError(URLError.notConnectedToInternet) + return .failure(.unknown("", "", APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) + } else if numberOfTimesEntered == 1, let anyModel = try? model.eraseToAnyModel() { + expectSecondCallToAPIMutate.fulfill() + let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, + modelName: model.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + return .success(MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata)) + } else { + XCTFail("This should not be called more than once") + return .failure(.unknown("Unexpected operation", "", nil)) + } + } + mockAPIPlugin.responders[.mutateRequestResponse] = responder + + let completion: GraphQLOperation>.ResultListener = { _ in + expectMutationRequestCompletion.fulfill() + } + + let operation = await SyncMutationToCloudOperation( + mutationEvent: mutationEvent, + getLatestSyncMetadata: { + MutationSyncMetadata( + modelId: model.id, + modelName: model.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 10 + ) + }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: completion + ) + let queue = OperationQueue() + queue.addOperation(operation) + await fulfillment(of: [ + expectFirstCallToAPIMutate, + expectSecondCallToAPIMutate, + expectMutationRequestCompletion + ], timeout: defaultAsyncWaitTimeout) + } + + func testRetryOnChangeReachability() async throws { + let mockRequestRetryPolicy = MockRequestRetryablePolicy() + let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) + mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) + + let expectMutationRequestCompletion = expectation(description: "Expect to complete mutation request") + let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") + let expectSecondCallToAPIMutate = expectation(description: "Second call to API.mutate") + let post1 = Post(title: "post1", content: "content1", createdAt: .now()) + let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) + let model = MockSynced(id: "id-1") + + var numberOfTimesEntered = 0 + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } + if numberOfTimesEntered == 0 { + expectFirstCallToAPIMutate.fulfill() + let urlError = URLError(URLError.notConnectedToInternet) + return .failure(.unknown("", "", APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) + } else if numberOfTimesEntered == 1, let anyModel = try? model.eraseToAnyModel() { + expectSecondCallToAPIMutate.fulfill() + let remoteSyncMetadata = MutationSyncMetadata(modelId: model.id, + modelName: model.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) + } else { + XCTFail("This should not be called more than once") + return .failure(.unknown("This should not be called more than once", "", nil)) + } + } + mockAPIPlugin.responders[.mutateRequestResponse] = responder + + let completion: GraphQLOperation>.ResultListener = { _ in + expectMutationRequestCompletion.fulfill() + } + let operation = await SyncMutationToCloudOperation( + mutationEvent: mutationEvent, + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + requestRetryablePolicy: mockRequestRetryPolicy, + completion: completion + ) + let queue = OperationQueue() + queue.addOperation(operation) + await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) + + reachabilityPublisher.send(ReachabilityUpdate(isOnline: true)) + + await fulfillment(of: [expectSecondCallToAPIMutate], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectMutationRequestCompletion], timeout: defaultAsyncWaitTimeout) + } + + func testAbilityToCancel() async throws { + let mockRequestRetryPolicy = MockRequestRetryablePolicy() + let waitForeverToRetry = RequestRetryAdvice(shouldRetry: true, retryInterval: .seconds(secondsInADay)) + mockRequestRetryPolicy.pushOnRetryRequestAdvice(response: waitForeverToRetry) + + let expectMutationRequestFailed = expectation(description: "Expect to fail mutation request") + let expectFirstCallToAPIMutate = expectation(description: "First call to API.mutate") + let post1 = Post(title: "post1", content: "content1", createdAt: .now()) + let mutationEvent = try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) + + var numberOfTimesEntered = 0 + let responder = MutateRequestResponder> { _ in + defer { numberOfTimesEntered += 1 } + if numberOfTimesEntered == 0 { + expectFirstCallToAPIMutate.fulfill() + let urlError = URLError(URLError.notConnectedToInternet) + return .failure(.unknown("", "", APIError.networkError("mock NotConnectedToInternetError", nil, urlError))) + } else { + XCTFail("This should not be called more than once") + return .failure(.unknown("This should not be called more than once", "", nil)) + } + + } + mockAPIPlugin.responders[.mutateRequestResponse] = responder + + let completion: GraphQLOperation>.ResultListener = { asyncEvent in + switch asyncEvent { + case .failure: + expectMutationRequestFailed.fulfill() + default: + break + } + } + let operation = await SyncMutationToCloudOperation( + mutationEvent: mutationEvent, + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + requestRetryablePolicy: mockRequestRetryPolicy, + completion: completion + ) + let queue = OperationQueue() + queue.addOperation(operation) + await fulfillment(of: [expectFirstCallToAPIMutate], timeout: defaultAsyncWaitTimeout) + + // At this point, we will be "waiting forever" to retry our request or until the operation is canceled + operation.cancel() + await fulfillment(of: [expectMutationRequestFailed], timeout: defaultAsyncWaitTimeout) + } + + // MARK: - GetRetryAdviceIfRetryableTests + + func testGetRetryAdvice_NetworkError_RetryTrue() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + + let error = APIError.networkError("", nil, URLError(.userAuthenticationRequired)) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertTrue(advice.shouldRetry) + } + + func testGetRetryAdvice_HTTPStatusError401WithMultiAuth_RetryTrue() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: MockMultiAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + let response = HTTPURLResponse(url: URL(string: "http://localhost")!, + statusCode: 401, + httpVersion: nil, + headerFields: nil)! + let error = APIError.httpStatusError(401, response) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertTrue(advice.shouldRetry) + } + + /// Given: Model with multiple auth types. Mutation requests always fail with 401 error code + /// When: Mutating model fails with 401 + /// Then: DataStore will try again with each auth type and eventually fails + func testGetRetryAdviceForEachModelAuthTypeThenFail_HTTPStatusError401() async throws { + var numberOfTimesEntered = 0 + let mutationEvent = try createMutationEvent() + let authStrategy = MockMultiAuthModeStrategy() + let expectedNumberOfTimesEntered = authStrategy.authTypesFor(schema: mutationEvent.schema, operation: .create).count + + let expectCalllToApiMutateNTimesAndFail = expectation(description: "Call API.mutate \(expectedNumberOfTimesEntered) times and then fail") + + let response = HTTPURLResponse(url: URL(string: "http://localhost")!, + statusCode: 401, + httpVersion: nil, + headerFields: nil)! + let error = APIError.httpStatusError(401, response) + + let operation = await SyncMutationToCloudOperation( + mutationEvent: mutationEvent, + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: authStrategy, + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { result in + if numberOfTimesEntered == expectedNumberOfTimesEntered { + expectCalllToApiMutateNTimesAndFail.fulfill() + + } else { + XCTFail("API.mutate was called incorrect amount of times, expected: \(expectedNumberOfTimesEntered), was : \(numberOfTimesEntered)") + } + } + ) + + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } + return .failure(.unknown("", "", error)) + } + + mockAPIPlugin.responders[.mutateRequestResponse] = responder + + let queue = OperationQueue() + queue.addOperation(operation) + + await fulfillment(of: [expectCalllToApiMutateNTimesAndFail], timeout: defaultAsyncWaitTimeout) + } + + func testGetRetryAdvice_OperationErrorAuthErrorWithMultiAuth_RetryTrue() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: MockMultiAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + + let authError = AuthError.notAuthorized("", "", nil) + let error = APIError.operationError("", "", authError) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertTrue(advice.shouldRetry) + } + + func testGetRetryAdvice_OperationErrorAuthErrorWithSingleAuth_RetryFalse() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + + let authError = AuthError.notAuthorized("", "", nil) + let error = APIError.operationError("", "", authError) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertFalse(advice.shouldRetry) + } + + func testGetRetryAdvice_OperationErrorAuthErrorSessionExpired_RetryTrue() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + + let authError = AuthError.sessionExpired("", "", nil) + let error = APIError.operationError("", "", authError) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertTrue(advice.shouldRetry) + } + + func testGetRetryAdvice_OperationErrorAuthErrorSignedOut_RetryTrue() async throws { + let operation = await SyncMutationToCloudOperation( + mutationEvent: try createMutationEvent(), + getLatestSyncMetadata: { nil }, + api: mockAPIPlugin, + authModeStrategy: AWSDefaultAuthModeStrategy(), + networkReachabilityPublisher: publisher, + currentAttemptNumber: 1, + completion: { _ in } + ) + + let authError = AuthError.signedOut("", "", nil) + let error = APIError.operationError("", "", authError) + let advice = operation.getRetryAdviceIfRetryable(error: error) + XCTAssertTrue(advice.shouldRetry) + } + + private func createMutationEvent() throws -> MutationEvent { + let post1 = Post(title: "post1", content: "content1", createdAt: .now()) + return try MutationEvent(model: post1, modelSchema: post1.schema, mutationType: .create) + } + +} + +public class MockMultiAuthModeStrategy: AuthModeStrategy { + public weak var authDelegate: AuthModeStrategyDelegate? + required public init() {} + + public func authTypesFor(schema: ModelSchema, + operation: ModelOperation) -> AWSAuthorizationTypeIterator { + return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) + } + + public func authTypesFor(schema: ModelSchema, + operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { + return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) + } +} + +extension SyncMutationToCloudOperationTests { + private func setUpCore() async throws -> AmplifyConfiguration { + await Amplify.reset() + + let dataStorePublisher = DataStorePublisher() + let dataStorePlugin = AWSDataStorePlugin(modelRegistration: TestModelRegistration(), + storageEngineBehaviorFactory: MockStorageEngineBehavior.mockStorageEngineBehaviorFactory, + dataStorePublisher: dataStorePublisher, + validAPIPluginKey: "MockAPICategoryPlugin", + validAuthPluginKey: "MockAuthCategoryPlugin") + + try Amplify.add(plugin: dataStorePlugin) + let dataStoreConfig = DataStoreCategoryConfiguration(plugins: [ + "awsDataStorePlugin": true + ]) + + let amplifyConfig = AmplifyConfiguration(dataStore: dataStoreConfig) + + return amplifyConfig + } + + private func setUpAPICategory(config: AmplifyConfiguration) throws -> AmplifyConfiguration { + mockAPIPlugin = MockAPICategoryPlugin() + try Amplify.add(plugin: mockAPIPlugin) + + let apiConfig = APICategoryConfiguration(plugins: [ + "MockAPICategoryPlugin": true + ]) + let amplifyConfig = AmplifyConfiguration(api: apiConfig, dataStore: config.dataStore) + return amplifyConfig + } + + private func setUpWithAPI() async throws { + let configWithoutAPI = try await setUpCore() + let configWithAPI = try setUpAPICategory(config: configWithoutAPI) + try Amplify.configure(configWithAPI) + } +} From acbb4e8f209e1eb7f5c231f66c0d2c2ec56daadb Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 26 Apr 2024 12:20:01 -0700 Subject: [PATCH 12/23] fix broken unit test cases of API and DataStore --- ...SAPICategoryPlugin+ReachabilityTests.swift | 4 +- .../Operation/AWSGraphQLOperationTests.swift | 4 +- .../Operation/AWSRESTOperationTests.swift | 13 +-- .../NetworkReachabilityNotifierTests.swift | 6 +- .../Core/AWSDataStorePluginTests.swift | 94 +++++++++------- .../SQLiteStorageEngineAdapterJsonTests.swift | 16 +-- .../OutgoingMutationQueueTests.swift | 104 +++++++++--------- .../MutationEventExtensionsTests.swift | 30 ++--- .../TestSupport/SyncEngineTestBase.swift | 1 - ...aStoreFlutterConsecutiveUpdatesTests.swift | 8 +- ...SyncEngineFlutterIntegrationTestBase.swift | 2 +- 11 files changed, 147 insertions(+), 135 deletions(-) diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift index 6123db2a2e..2d005cbdf1 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+ReachabilityTests.swift @@ -82,7 +82,7 @@ class AWSAPICategoryPluginReachabilityTests: XCTestCase { XCTAssertEqual(reachability.key, graphQLAPI) } - func testReachabilityConcurrentPerform() throws { + func testReachabilityConcurrentPerform() async throws { let graphQLAPI = "graphQLAPI" let restAPI = "restAPI" do { @@ -114,7 +114,7 @@ class AWSAPICategoryPluginReachabilityTests: XCTestCase { concurrentPerformCompleted.fulfill() } - wait(for: [concurrentPerformCompleted], timeout: 1) + await fulfillment(of: [concurrentPerformCompleted], timeout: 1) XCTAssertEqual(apiPlugin.reachabilityMap.count, 2) } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift index 717a87f5ab..9400006ebd 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLOperationTests.swift @@ -15,7 +15,7 @@ import AWSPluginsCore class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase { /// Tests that upon completion, the operation is removed from the task mapper. - func testOperationCleanup() { + func testOperationCleanup() async { let request = GraphQLRequest(apiName: apiName, document: testDocument, variables: nil, @@ -34,7 +34,7 @@ class AWSGraphQLOperationTests: AWSAPICategoryPluginTestBase { } receiveValue: { _ in } defer { sink.cancel() } - wait(for: [receivedCompletion], timeout: 1) + await fulfillment(of: [receivedCompletion], timeout: 1) let task = operation.mapper.task(for: operation) XCTAssertNil(task) } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift index 771e7e60f4..e53fb5f38a 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift @@ -52,7 +52,7 @@ class AWSRESTOperationTests: OperationTestBase { await fulfillment(of: [listenerWasInvoked], timeout: 3) } - func testGetFailsWithBadAPIName() throws { + func testGetFailsWithBadAPIName() async throws { let sentData = Data([0x00, 0x01, 0x02, 0x03]) try setUpPluginForSingleResponse(sending: sentData, for: .rest) @@ -69,14 +69,13 @@ class AWSRESTOperationTests: OperationTestBase { receivedFailure.fulfill() } } - - waitForExpectations(timeout: 1.00) + await fulfillment(of: [receivedSuccess, receivedFailure], timeout: 1) } /// - Given: A configured plugin /// - When: I invoke `APICategory.get(apiName:path:listener:)` /// - Then: The listener is invoked with the successful value - func testGetReturnsValue() throws { + func testGetReturnsValue() async throws { let sentData = Data([0x00, 0x01, 0x02, 0x03]) try setUpPluginForSingleResponse(sending: sentData, for: .rest) @@ -92,10 +91,10 @@ class AWSRESTOperationTests: OperationTestBase { callbackInvoked.fulfill() } - wait(for: [callbackInvoked], timeout: 1.0) + await fulfillment(of: [callbackInvoked], timeout: 1.0) } - func testRESTOperation_withCustomHeader_shouldOverrideDefaultAmplifyHeaders() throws { + func testRESTOperation_withCustomHeader_shouldOverrideDefaultAmplifyHeaders() async throws { let expectedHeaderValue = "text/plain" let sentData = Data([0x00, 0x01, 0x02, 0x03]) try setUpPluginForSingleResponse(sending: sentData, for: .rest) @@ -117,7 +116,7 @@ class AWSRESTOperationTests: OperationTestBase { } callbackInvoked.fulfill() } - wait(for: [callbackInvoked, validated], timeout: 1.0) + await fulfillment(of: [callbackInvoked, validated], timeout: 1.0) } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Reachability/NetworkReachabilityNotifierTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Reachability/NetworkReachabilityNotifierTests.swift index 48f6886616..9b64dc0305 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Reachability/NetworkReachabilityNotifierTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Reachability/NetworkReachabilityNotifierTests.swift @@ -93,7 +93,7 @@ class NetworkReachabilityNotifierTests: XCTestCase { cancellable.cancel() } - func testWifiConnectivity_publisherGoesOutOfScope() { + func testWifiConnectivity_publisherGoesOutOfScope() async { MockReachability.iConnection = .wifi let defaultValueExpect = expectation(description: ".sink receives default value") let completeExpect = expectation(description: ".sink receives completion") @@ -104,12 +104,12 @@ class NetworkReachabilityNotifierTests: XCTestCase { defaultValueExpect.fulfill() }) - wait(for: [defaultValueExpect], timeout: 1.0) + await fulfillment(of: [defaultValueExpect], timeout: 1.0) notifier = nil notification = Notification.init(name: .reachabilityChanged) NotificationCenter.default.post(notification) - wait(for: [completeExpect], timeout: 1.0) + await fulfillment(of: [completeExpect], timeout: 1.0) cancellable.cancel() } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift index de910896fe..24150d51ac 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift @@ -32,7 +32,7 @@ class AWSDataStorePluginTests: XCTestCase { } } - func testStorageEngineDoesNotStartsOnConfigure() throws { + func testStorageEngineDoesNotStartsOnConfigure() async throws { let startExpectation = expectation(description: "Start Sync should not be called") startExpectation.isInverted = true let storageEngine = MockStorageEngineBehavior() @@ -53,10 +53,10 @@ class AWSDataStorePluginTests: XCTestCase { } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } - waitForExpectations(timeout: 1.0) + await fulfillment(of: [startExpectation], timeout: 1) } - func testStorageEngineStartsOnPluginStart() throws { + func testStorageEngineStartsOnPluginStart() async throws { let startExpectation = expectation(description: "Start Sync should be called") let storageEngine = MockStorageEngineBehavior() storageEngine.responders[.startSync] = StartSyncResponder { _ in @@ -77,7 +77,7 @@ class AWSDataStorePluginTests: XCTestCase { } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } - waitForExpectations(timeout: 1.0) + await fulfillment(of: [startExpectation], timeout: 1) } func testStorageEngineStartsOnQuery() async throws { @@ -113,7 +113,7 @@ class AWSDataStorePluginTests: XCTestCase { ) } - func testStorageEngineStartsOnPluginStopStart() throws { + func testStorageEngineStartsOnPluginStopStart() async throws { let stopExpectation = expectation(description: "Stop plugin should be called") stopExpectation.isInverted = true let startExpectation = expectation(description: "Start Sync should be called") @@ -148,10 +148,10 @@ class AWSDataStorePluginTests: XCTestCase { } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } - waitForExpectations(timeout: 1.0) + await fulfillment(of: [startExpectation, stopExpectation], timeout: 1) } - func testStorageEngineStartsOnPluginClearStart() throws { + func testStorageEngineStartsOnPluginClearStart() async throws { let clearExpectation = expectation(description: "Clear should be called") let startExpectation = expectation(description: "Start Sync should be called") var currCount = 0 @@ -185,10 +185,10 @@ class AWSDataStorePluginTests: XCTestCase { } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } - waitForExpectations(timeout: 1.0) + await fulfillment(of: [startExpectation, clearExpectation], timeout: 1) } - func testStorageEngineStartStopStart() throws { + func testStorageEngineStartStopStart() async throws { let startExpectation = expectation(description: "Start Sync should be called with start") let stopExpectation = expectation(description: "stop should be called") let startExpectationOnSecondStart = expectation(description: "Start Sync should be called again") @@ -234,7 +234,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) let stopCompleted = expectation(description: "stop completed") plugin.stop(completion: { _ in @@ -242,7 +242,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) stopCompleted.fulfill() }) - wait(for: [stopCompleted], timeout: 1.0) + await fulfillment(of: [stopCompleted], timeout: 1.0) storageEngine.responders[.startSync] = StartSyncResponder { _ in startExpectationOnSecondStart.fulfill() @@ -252,19 +252,19 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.storageEngine) XCTAssertNotNil(plugin.dataStorePublisher) }) - wait( - for: [startExpectation, stopExpectation, startExpectationOnSecondStart], + await fulfillment( + of: [startExpectation, stopExpectation, startExpectationOnSecondStart], timeout: 1, enforceOrder: true ) - wait(for: [finishNotReceived], timeout: 1) + await fulfillment(of: [finishNotReceived], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } } - func testStorageEngineStartClearStart() throws { + func testStorageEngineStartClearStart() async throws { let startExpectation = expectation(description: "Start Sync should be called with start") let clearExpectation = expectation(description: "Clear should be called") let startExpectationOnSecondStart = expectation(description: "Start Sync should be called again") @@ -309,7 +309,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) let clearCompleted = expectation(description: "clear completed") plugin.clear(completion: { _ in @@ -317,7 +317,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) clearCompleted.fulfill() }) - wait(for: [clearCompleted], timeout: 1.0) + await fulfillment(of: [clearCompleted], timeout: 1.0) storageEngine.responders[.startSync] = StartSyncResponder {_ in startExpectationOnSecondStart.fulfill() @@ -328,19 +328,19 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) }) - wait( - for: [startExpectation, clearExpectation, startExpectationOnSecondStart], + await fulfillment( + of: [startExpectation, clearExpectation, startExpectationOnSecondStart], timeout: 1, enforceOrder: true ) - wait(for: [finishNotReceived], timeout: 1) + await fulfillment(of: [finishNotReceived], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } } - func testStorageEngineQueryClearQuery() throws { + func testStorageEngineQueryClearQuery() async throws { let startExpectation = expectation(description: "Start Sync should be called with Query") let clearExpectation = expectation(description: "Clear should be called") let startExpectationOnQuery = expectation(description: "Start Sync should be called again with Query") @@ -387,7 +387,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) queryCompleted.fulfill() }) - wait(for: [queryCompleted], timeout: 1.0) + await fulfillment(of: [queryCompleted], timeout: 1.0) let clearCompleted = expectation(description: "clear completed") plugin.clear(completion: { _ in @@ -395,7 +395,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) clearCompleted.fulfill() }) - wait(for: [clearCompleted], timeout: 1.0) + await fulfillment(of: [clearCompleted], timeout: 1.0) storageEngine.responders[.query] = QueryResponder {_ in count = self.expect(startExpectationOnQuery, count, 3) return .success([]) @@ -405,7 +405,12 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.storageEngine) XCTAssertNotNil(plugin.dataStorePublisher) }) - waitForExpectations(timeout: 1.0) + await fulfillment(of: [ + startExpectation, + clearExpectation, + startExpectationOnQuery, + finishNotReceived + ], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") @@ -427,7 +432,7 @@ class AWSDataStorePluginTests: XCTestCase { /// - a mutation event is sent /// - Then: The subscriber to plugin's publisher should receive the mutation - func testStorageEngineStartClearSend() { + func testStorageEngineStartClearSend() async { let startExpectation = expectation(description: "Start Sync should be called with start") let clearExpectation = expectation(description: "Clear should be called") @@ -479,7 +484,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) let clearCompleted = expectation(description: "clear completed") plugin.clear(completion: { _ in @@ -487,14 +492,19 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) clearCompleted.fulfill() }) - wait(for: [clearCompleted], timeout: 1.0) + await fulfillment(of: [clearCompleted], timeout: 1.0) let mockModel = MockSynced(id: "12345") try plugin.dataStorePublisher?.send(input: MutationEvent(model: mockModel, modelSchema: mockModel.schema, mutationType: .create)) - waitForExpectations(timeout: 1.0) + await fulfillment(of: [ + startExpectation, + clearExpectation, + finishNotReceived, + publisherReceivedValue + ], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") @@ -507,7 +517,7 @@ class AWSDataStorePluginTests: XCTestCase { /// - plugin.stop() is called /// - a mutation event is sent /// - Then: The subscriber to plugin's publisher should receive the mutation - func testStorageEngineStartStopSend() { + func testStorageEngineStartStopSend() async { let startExpectation = expectation(description: "Start Sync should be called with start") let stopExpectation = expectation(description: "Stop should be called") @@ -558,7 +568,7 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) let stopCompleted = expectation(description: "stop completed") plugin.stop(completion: { _ in @@ -566,10 +576,10 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) stopCompleted.fulfill() }) - wait(for: [stopCompleted], timeout: 1.0) + await fulfillment(of: [stopCompleted], timeout: 1.0) - wait( - for: [startExpectation, stopExpectation], + await fulfillment( + of: [startExpectation, stopExpectation], timeout: 1, enforceOrder: true ) @@ -578,7 +588,7 @@ class AWSDataStorePluginTests: XCTestCase { modelSchema: mockModel.schema, mutationType: .create)) - wait(for: [publisherReceivedValue, finishNotReceived], timeout: 1) + await fulfillment(of: [publisherReceivedValue, finishNotReceived], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") @@ -589,7 +599,7 @@ class AWSDataStorePluginTests: XCTestCase { /// - When: /// - plugin.clear() is called /// - Then: StorageEngine.clear is called - func testClearStorageWhenEngineIsNotStarted() { + func testClearStorageWhenEngineIsNotStarted() async { let storageEngine = MockStorageEngineBehavior() let pluginClearExpectation = expectation(description: "DataStore plugin .clear should called") let storageClearExpectation = expectation(description: "StorageEngine .clear should be called") @@ -611,10 +621,10 @@ class AWSDataStorePluginTests: XCTestCase { pluginClearExpectation.fulfill() } } - waitForExpectations(timeout: 1.0) + await fulfillment(of: [pluginClearExpectation, storageClearExpectation], timeout: 1) } - func testStopStorageEngineOnTerminalEvent() { + func testStopStorageEngineOnTerminalEvent() async { let storageEngine = MockStorageEngineBehavior() let stopExpectation = expectation(description: "stop should be called") @@ -637,14 +647,14 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) storageEngine.mockSyncEnginePublisher.send(completion: .finished) - wait(for: [stopExpectation], timeout: 1) + await fulfillment(of: [stopExpectation], timeout: 1) } - func testStopStorageEngineOnTerminalFailureEvent() { + func testStopStorageEngineOnTerminalFailureEvent() async { let storageEngine = MockStorageEngineBehavior() let stopExpectation = expectation(description: "stop should be called") var count = 0 @@ -667,10 +677,10 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) startCompleted.fulfill() }) - wait(for: [startCompleted], timeout: 1.0) + await fulfillment(of: [startCompleted], timeout: 1.0) storageEngine.mockSyncEnginePublisher.send(completion: .failure(.internalOperation("", "", nil))) - waitForExpectations(timeout: 1.0) + await fulfillment(of: [stopExpectation], timeout: 1) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/SQLiteStorageEngineAdapterJsonTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/SQLiteStorageEngineAdapterJsonTests.swift index d71082ff45..7db19c714d 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/SQLiteStorageEngineAdapterJsonTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/SQLiteStorageEngineAdapterJsonTests.swift @@ -76,7 +76,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { /// - the `save(post)` is called /// - Then: /// - call `query(Post)` to check if the model was correctly inserted - func testInsertPost() { + func testInsertPost() async { let expectation = self.expectation( description: "it should save and select a Post from the database") @@ -115,7 +115,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { expectation.fulfill() } } - wait(for: [expectation], timeout: 5) + await fulfillment(of: [expectation], timeout: 5) } /// - Given: a list a `Post` instance @@ -124,7 +124,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { /// - Then: /// - call `query(Post, where: title == post.title)` to check /// if the model was correctly inserted using a predicate - func testInsertPostAndSelectByTitle() { + func testInsertPostAndSelectByTitle() async { let expectation = self.expectation( description: "it should save and select a Post from the database") @@ -163,7 +163,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { } } - wait(for: [expectation], timeout: 5) + await fulfillment(of: [expectation], timeout: 5) } /// - Given: a list a `Post` instance @@ -173,7 +173,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { /// - call `save(post)` again with an updated title /// - check if the `query(Post)` returns only 1 post /// - the post has the updated title - func testInsertPostAndThenUpdateIt() { + func testInsertPostAndThenUpdateIt() async { let expectation = self.expectation( description: "it should insert and update a Post") @@ -224,7 +224,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { } } - wait(for: [expectation], timeout: 5) + await fulfillment(of: [expectation], timeout: 5) } /// - Given: a list a `Post` instance @@ -233,7 +233,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { /// - Then: /// - call `delete(Post, id)` and check if `query(Post)` is empty /// - check if `storageAdapter.exists(Post, id)` returns `false` - func testInsertPostAndThenDeleteIt() { + func testInsertPostAndThenDeleteIt() async { let saveExpectation = expectation(description: "Saved") let deleteExpectation = expectation(description: "Deleted") let queryExpectation = expectation(description: "Queried") @@ -270,7 +270,7 @@ class SQLiteStorageEngineAdapterJsonTests: XCTestCase { } } - wait(for: [saveExpectation, deleteExpectation, queryExpectation], timeout: 2) + await fulfillment(of: [saveExpectation, deleteExpectation, queryExpectation], timeout: 2) } /// - Given: A Post instance diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift index 610a6b0ffa..8802e832ff 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift @@ -24,67 +24,57 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { await tryOrFail { try setUpStorageAdapter() - try setUpDataStore(mutationQueue: OutgoingMutationQueue(storageAdapter: storageAdapter, - dataStoreConfiguration: .testDefault(), - authModeStrategy: AWSDefaultAuthModeStrategy())) - } - - let post = Post(title: "Post title", - content: "Post content", - createdAt: .now()) - - apiPlugin.responders[.mutateRequestResponse] = MutateRequestResponder { request in - let anyModel = try! post.eraseToAnyModel() - let remoteSyncMetadata = MutationSyncMetadata(modelId: post.id, - modelName: Post.modelName, - deleted: false, - lastChangedAt: Date().unixSeconds, - version: 2) - let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) - return .success(remoteMutationSync) + try setUpDataStore( + mutationQueue: OutgoingMutationQueue( + storageAdapter: storageAdapter, + dataStoreConfiguration: .testDefault(), + authModeStrategy: AWSDefaultAuthModeStrategy() + ) + ) } + let post = Post(title: "Post title", content: "Post content", createdAt: .now()) let outboxStatusReceivedCurrentCount = AtomicValue(initialValue: 0) let outboxStatusOnStart = expectation(description: "On DataStore start, outboxStatus received") let outboxStatusOnMutationEnqueued = expectation(description: "Mutation enqueued, outboxStatus received") let outboxMutationEnqueued = expectation(description: "Mutation enqueued, outboxMutationEnqueued received") - let outboxStatusFilter = HubFilters.forEventName(HubPayload.EventName.DataStore.outboxStatus) - let outboxMutationEnqueuedFilter = HubFilters.forEventName(HubPayload.EventName.DataStore.outboxMutationEnqueued) - let filters = HubFilters.any(filters: outboxStatusFilter, outboxMutationEnqueuedFilter) - let hubListener = Amplify.Hub.listen(to: .dataStore, isIncluded: filters) { payload in - if payload.eventName == HubPayload.EventName.DataStore.outboxStatus { - _ = outboxStatusReceivedCurrentCount.increment(by: 1) - guard let outboxStatusEvent = payload.data as? OutboxStatusEvent else { - XCTFail("Failed to cast payload data as OutboxStatusEvent") - return - } + let hubListener0 = Amplify.Hub.listen(to: .dataStore, eventName: HubPayload.EventName.DataStore.outboxStatus) { payload in + defer { _ = outboxStatusReceivedCurrentCount.increment(by: 1) } + guard let outboxStatusEvent = payload.data as? OutboxStatusEvent else { + XCTFail("Failed to cast payload data as OutboxStatusEvent") + return + } - switch outboxStatusReceivedCurrentCount.get() { - case 1: - XCTAssertTrue(outboxStatusEvent.isEmpty) - outboxStatusOnStart.fulfill() - case 2: - XCTAssertFalse(outboxStatusEvent.isEmpty) - outboxStatusOnMutationEnqueued.fulfill() - case 3: - XCTAssertTrue(outboxStatusEvent.isEmpty) - default: - XCTFail("Should not trigger outbox status event") - } + switch outboxStatusReceivedCurrentCount.get() { + case 0: + XCTAssertTrue(outboxStatusEvent.isEmpty) + outboxStatusOnStart.fulfill() + case 1: + XCTAssertFalse(outboxStatusEvent.isEmpty) + outboxStatusOnMutationEnqueued.fulfill() + case 2: + XCTAssertTrue(outboxStatusEvent.isEmpty) + default: + XCTFail("Should not trigger outbox status event") } + } - if payload.eventName == HubPayload.EventName.DataStore.outboxMutationEnqueued { - guard let outboxStatusEvent = payload.data as? OutboxMutationEvent else { - XCTFail("Failed to cast payload data as OutboxMutationEvent") - return - } - XCTAssertEqual(outboxStatusEvent.modelName, "Post") - outboxMutationEnqueued.fulfill() + let hubListener1 = Amplify.Hub.listen(to: .dataStore, eventName: HubPayload.EventName.DataStore.outboxMutationEnqueued) { payload in + guard let outboxStatusEvent = payload.data as? OutboxMutationEvent else { + XCTFail("Failed to cast payload data as OutboxMutationEvent") + return } + XCTAssertEqual(outboxStatusEvent.modelName, "Post") + outboxMutationEnqueued.fulfill() } - guard try await HubListenerTestUtilities.waitForListener(with: hubListener, timeout: 5.0) else { + guard try await HubListenerTestUtilities.waitForListener(with: hubListener0, timeout: 5.0) else { + XCTFail("Listener not registered for hub") + return + } + + guard try await HubListenerTestUtilities.waitForListener(with: hubListener1, timeout: 5.0) else { XCTFail("Listener not registered for hub") return } @@ -96,6 +86,19 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { } } + apiPlugin.responders[.mutateRequestResponse] = MutateRequestResponder { request in + let anyModel = try! post.eraseToAnyModel() + let remoteSyncMetadata = MutationSyncMetadata( + modelId: post.id, + modelName: Post.modelName, + deleted: false, + lastChangedAt: Date().unixSeconds, + version: 2 + ) + let remoteMutationSync = MutationSync(model: anyModel, syncMetadata: remoteSyncMetadata) + return .success(remoteMutationSync) + } + try await startAmplifyAndWaitForSync() let saveSuccess = expectation(description: "save success") @@ -103,10 +106,10 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { _ = try await Amplify.DataStore.save(post) saveSuccess.fulfill() } - await fulfillment(of: [saveSuccess], timeout: 1.0) await fulfillment( of: [ + saveSuccess, outboxStatusOnStart, outboxStatusOnMutationEnqueued, outboxMutationEnqueued, @@ -114,7 +117,8 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { ], timeout: 5.0 ) - Amplify.Hub.removeListener(hubListener) + Amplify.Hub.removeListener(hubListener0) + Amplify.Hub.removeListener(hubListener1) } /// - Given: A sync-configured DataStore diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/MutationEventExtensionsTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/MutationEventExtensionsTests.swift index ab8f7640c1..0add6aebd1 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/MutationEventExtensionsTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/MutationEventExtensionsTests.swift @@ -21,7 +21,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { /// event model that matches the received mutation sync model. The received mutation sync has version 1. /// - When: The sent model matches the received model and the first pending mutation event version is `nil`. /// - Then: The pending mutation event version should be updated to the received model version of 1. - func testSentModelWithNilVersion_Reconciled() throws { + func testSentModelWithNilVersion_Reconciled() async throws { let modelId = UUID().uuidString let post = Post(id: modelId, title: "title", content: "content", createdAt: .now()) let requestMutationEvent = try createMutationEvent(model: post, @@ -57,7 +57,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { updatingVersionExpectation.fulfill() } } - wait(for: [updatingVersionExpectation], timeout: 1) + await fulfillment(of: [updatingVersionExpectation], timeout: 1) // query for head of mutation event table for given model id and check if it has the updated version MutationEvent.pendingMutationEvents(forModel: post, @@ -75,7 +75,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { queryAfterUpdatingVersionExpectation.fulfill() } } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) + await fulfillment(of: [queryAfterUpdatingVersionExpectation], timeout: 1) } /// - Given: A pending mutation events queue with two events(update and delete) containing `nil` version, @@ -85,7 +85,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { /// the second pending mutation event(delete) version is `nil`. /// - Then: The first pending mutation event(update) version should be updated to the received model version of 1 /// and the second pending mutation event version(delete) should not be updated. - func testSentModelWithNilVersion_SecondPendingEventNotReconciled() throws { + func testSentModelWithNilVersion_SecondPendingEventNotReconciled() async throws { let modelId = UUID().uuidString let post = Post(id: modelId, title: "title", content: "content", createdAt: .now()) let requestMutationEvent = try createMutationEvent(model: post, @@ -127,7 +127,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { updatingVersionExpectation.fulfill() } } - wait(for: [updatingVersionExpectation], timeout: 1) + await fulfillment(of: [updatingVersionExpectation], timeout: 1) // query for head of mutation event table for given model id and check if it has the updated version MutationEvent.pendingMutationEvents(forModel: post, @@ -146,7 +146,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { queryAfterUpdatingVersionExpectation.fulfill() } } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) + await fulfillment(of: [queryAfterUpdatingVersionExpectation], timeout: 1) } /// - Given: A pending mutation events queue with event containing version 2, a sent mutation event model @@ -154,7 +154,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { /// version 1. /// - When: The sent model matches the received model and the first pending mutation event version is 2. /// - Then: The first pending mutation event version should NOT be updated. - func testSentModelVersionNewerThanResponseVersion_PendingEventNotReconciled() throws { + func testSentModelVersionNewerThanResponseVersion_PendingEventNotReconciled() async throws { let modelId = UUID().uuidString let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) @@ -190,7 +190,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { updatingVersionExpectation.fulfill() } } - wait(for: [updatingVersionExpectation], timeout: 1) + await fulfillment(of: [updatingVersionExpectation], timeout: 1) // query for head of mutation event table for given model id and check if it has the correct version MutationEvent.pendingMutationEvents(forModel: post1, @@ -208,7 +208,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { queryAfterUpdatingVersionExpectation.fulfill() } } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) + await fulfillment(of: [queryAfterUpdatingVersionExpectation], timeout: 1) } /// - Given: A pending mutation events queue with event containing version 1, a sent mutation event model @@ -216,7 +216,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { /// sync has version 2. /// - When: The sent model doesn't match the received model and the first pending mutation event version is 1. /// - Then: The first pending mutation event version should NOT be updated. - func testSentModelNotEqualToResponseModel_PendingEventNotReconciled() throws { + func testSentModelNotEqualToResponseModel_PendingEventNotReconciled() async throws { let modelId = UUID().uuidString let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) @@ -253,7 +253,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { updatingVersionExpectation.fulfill() } } - wait(for: [updatingVersionExpectation], timeout: 1) + await fulfillment(of: [updatingVersionExpectation], timeout: 1) // query for head of mutation event table for given model id and check if it has the correct version MutationEvent.pendingMutationEvents(forModel: post1, @@ -271,7 +271,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { queryAfterUpdatingVersionExpectation.fulfill() } } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) + await fulfillment(of: [queryAfterUpdatingVersionExpectation], timeout: 1) } /// - Given: A pending mutation events queue with event containing version 1, a sent mutation event model @@ -279,7 +279,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { /// has version 2. /// - When: The sent model matches the received model and the first pending mutation event version is 1. /// - Then: The first pending mutation event version should be updated to received mutation sync version i.e. 2. - func testPendingVersionReconciledSuccess() throws { + func testPendingVersionReconciledSuccess() async throws { let modelId = UUID().uuidString let post1 = Post(id: modelId, title: "title1", content: "content1", createdAt: .now()) let post2 = Post(id: modelId, title: "title2", content: "content2", createdAt: .now()) @@ -315,7 +315,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { updatingVersionExpectation.fulfill() } } - wait(for: [updatingVersionExpectation], timeout: 1) + await fulfillment(of: [updatingVersionExpectation], timeout: 1) // query for head of mutation event table for given model id and check if it has the correct version MutationEvent.pendingMutationEvents(forModel: post1, @@ -333,7 +333,7 @@ class MutationEventExtensionsTest: BaseDataStoreTests { queryAfterUpdatingVersionExpectation.fulfill() } } - wait(for: [queryAfterUpdatingVersionExpectation], timeout: 1) + await fulfillment(of: [queryAfterUpdatingVersionExpectation], timeout: 1) } private func createMutationEvent(model: Model, diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/SyncEngineTestBase.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/SyncEngineTestBase.swift index 5d69207902..d86b8ba8a4 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/SyncEngineTestBase.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/TestSupport/SyncEngineTestBase.swift @@ -75,7 +75,6 @@ class SyncEngineTestBase: XCTestCase { authPlugin = MockAuthCategoryPlugin() try Amplify.add(plugin: apiPlugin) try Amplify.add(plugin: authPlugin) - Amplify.Logging.logLevel = .verbose } override func tearDown() async throws { diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/DataStoreFlutterConsecutiveUpdatesTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/DataStoreFlutterConsecutiveUpdatesTests.swift index c0172df6ff..1aac1aa85e 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/DataStoreFlutterConsecutiveUpdatesTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/DataStoreFlutterConsecutiveUpdatesTests.swift @@ -17,7 +17,7 @@ class DataStoreFlutterConsecutiveUpdatesTests: SyncEngineFlutterIntegrationTestB /// - Given: API has been setup with `Post` model registered /// - When: A Post is saved and then immediately updated /// - Then: The post should be updated with new fields immediately and in the eventual consistent state - func testSaveAndImmediatelyUpdate() throws { + func testSaveAndImmediatelyUpdate() async throws { try startAmplifyAndWaitForSync() let plugin: AWSDataStorePlugin = try Amplify.DataStore.getPlugin(for: "awsDataStorePlugin") as! AWSDataStorePlugin let newPost = try PostWrapper(title: "MyPost", @@ -132,7 +132,7 @@ class DataStoreFlutterConsecutiveUpdatesTests: SyncEngineFlutterIntegrationTestB /// - Given: API has been setup with `Post` model registered /// - When: A Post is saved and deleted immediately /// - Then: The Post should not be returned when queried for immediately and in the eventual consistent state - func testSaveAndImmediatelyDelete() throws { + func testSaveAndImmediatelyDelete() async throws { try startAmplifyAndWaitForSync() let plugin: AWSDataStorePlugin = try Amplify.DataStore.getPlugin(for: "awsDataStorePlugin") as! AWSDataStorePlugin let newPost = try PostWrapper(title: "MyPost", @@ -237,7 +237,7 @@ class DataStoreFlutterConsecutiveUpdatesTests: SyncEngineFlutterIntegrationTestB /// - Given: API has been setup with `Post` model registered /// - When: A Post is saved with sync complete, updated and deleted immediately /// - Then: The Post should not be returned when queried for - func testSaveThenUpdateAndImmediatelyDelete() throws { + func testSaveThenUpdateAndImmediatelyDelete() async throws { try startAmplifyAndWaitForSync() let plugin: AWSDataStorePlugin = try Amplify.DataStore.getPlugin(for: "awsDataStorePlugin") as! AWSDataStorePlugin @@ -367,7 +367,7 @@ class DataStoreFlutterConsecutiveUpdatesTests: SyncEngineFlutterIntegrationTestB await fulfillment(of: [apiQuerySuccess], timeout: networkTimeout) } - private func queryPost(id: String, plugin: AWSDataStorePlugin) -> PostWrapper? { + private func queryPost(id: String, plugin: AWSDataStorePlugin) -> async PostWrapper? { let queryExpectation = expectation(description: "Query is successful") var queryResult: PostWrapper? plugin.query(FlutterSerializedModel.self, modelSchema: Post.schema, where: Post.keys.id.eq(id)) { result in diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/TestSupport/SyncEngineFlutterIntegrationTestBase.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/TestSupport/SyncEngineFlutterIntegrationTestBase.swift index 5505303ae7..b60c5bdef7 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/TestSupport/SyncEngineFlutterIntegrationTestBase.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginFlutterTests/TestSupport/SyncEngineFlutterIntegrationTestBase.swift @@ -62,7 +62,7 @@ class SyncEngineFlutterIntegrationTestBase: XCTestCase { } } - func startAmplifyAndWaitForSync() throws { + func startAmplifyAndWaitForSync() async throws { let syncStarted = expectation(description: "Sync started") let plugin: AWSDataStorePlugin = try Amplify.DataStore.getPlugin(for: "awsDataStorePlugin") as! AWSDataStorePlugin From 31106571d1b539cd0ae05fe04df0a7ac3e3994c4 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 30 Apr 2024 20:49:25 +0000 Subject: [PATCH 13/23] resolve plugins build issues (#3654) --- .../xcschemes/AWSPluginsSDKCore.xcscheme | 9 +++++-- .../xcschemes/Amplify-Package.xcscheme | 19 ++++++++------- .../AWSAPIPlugin/AWSAPIPlugin+Configure.swift | 9 +++---- .../Sources/AWSAPIPlugin/AWSAPIPlugin.swift | 3 ++- .../AWSAPICategoryPluginConfiguration.swift | 11 +++++---- .../AWSAPIEndpointInterceptors.swift | 5 ++-- .../APIKeyURLRequestInterceptor.swift | 1 + .../AuthTokenProviderWrapper.swift | 1 + .../AuthTokenURLRequestInterceptor.swift | 1 + .../IAMURLRequestInterceptor.swift | 1 + .../IAMAuthInterceptor.swift | 1 + .../AWSGraphQLSubscriptionTaskRunner.swift | 9 +++---- .../AppSyncRealTimeClientFactory.swift | 7 +++--- .../AppSyncRealTimeClientTests.swift | 1 + ...egoryPlugin+InterceptorBehaviorTests.swift | 1 + ...SAPICategoryPluginConfigurationTests.swift | 1 + .../AWSAPIEndpointInterceptorsTests.swift | 3 ++- .../AuthTokenURLRequestInterceptorTests.swift | 1 + .../Mocks/MockSubscription.swift | 6 ++--- .../AWSCognitoAuthPlugin+Configure.swift | 2 +- ...AWSCognitoAuthPlugin+PluginExtension.swift | 2 +- .../Support/Utils/HttpClientEngineProxy.swift | 2 +- .../AWSAuthCredentialsProviderBehavior.swift | 5 +++- ... AWSAuthService+CredentialsProvider.swift} | 5 ++-- .../AmplifyAWSCredentialsProvider.swift | 3 ++- .../AWSPluginsSDKCore/AuthTokenProvider.swift | 1 + .../IAMCredentialProvider.swift | 5 ++-- .../AWSPluginsSDKCore.xctestplan | 24 +++++++++++++++++++ .../Auth/AWSAuthServiceTests.swift | 1 + .../UserAgentSettingClientEngineTests.swift | 2 +- .../Utils/UserAgentSuffixAppenderTests.swift | 2 +- .../MockAWSAuthService.swift | 4 ++-- .../MockAWSSignatureV4Signer.swift | 1 + .../OutgoingMutationQueueNetworkTests.swift | 2 +- .../AWSLocationGeoPlugin+Configure.swift | 2 +- .../PinpointEvent+PinpointClientTypes.swift | 1 + .../PinpointClient+CredentialsProvider.swift | 2 +- .../Utils/PinpointRequestsRegistry.swift | 3 +-- ...WSCloudWatchLoggingSessionController.swift | 2 +- ...aultRemoteLoggingConstraintsProvider.swift | 1 + .../Predictions/AWSPredictionsService.swift | 2 +- .../Service/Storage/AWSS3StorageService.swift | 8 +++---- .../DefaultStorageTransferDatabaseTests.swift | 8 +++---- .../Mocks/MockCredentialsProvider.swift | 5 ++-- Package.swift | 21 ++++++++++------ 45 files changed, 135 insertions(+), 71 deletions(-) rename AmplifyPlugins/Core/AWSPluginsSDKCore/{AWSAuthCredentialsProvider.swift => AWSAuthService+CredentialsProvider.swift} (58%) create mode 100644 AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan rename AmplifyPlugins/Core/{AWSPluginsCoreTests => AWSPluginsSDKCoreTests}/Auth/AWSAuthServiceTests.swift (99%) rename AmplifyPlugins/Core/{AWSPluginsCoreTests => AWSPluginsSDKCoreTests}/Utils/UserAgentSettingClientEngineTests.swift (99%) rename AmplifyPlugins/Core/{AWSPluginsCoreTests => AWSPluginsSDKCoreTests}/Utils/UserAgentSuffixAppenderTests.swift (99%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme index 595e4a2023..f18064ad33 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme @@ -26,8 +26,13 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "YES" - shouldAutocreateTestPlan = "YES"> + shouldUseLaunchSchemeArgsEnv = "YES"> + + + + + + + + - - - - diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift index ec27a34b41..9591b353eb 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift @@ -7,6 +7,7 @@ @_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import AwsCommonRuntimeKit public extension AWSAPIPlugin { @@ -55,7 +56,7 @@ extension AWSAPIPlugin { /// A holder for AWSAPIPlugin dependencies that provides sane defaults for /// production struct ConfigurationDependencies { - let authService: AWSAuthServiceBehavior + let authService: AWSAuthCredentialsProviderBehavior let pluginConfig: AWSAPICategoryPluginConfiguration let appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol let logLevel: Amplify.LogLevel @@ -63,7 +64,7 @@ extension AWSAPIPlugin { init( configurationValues: JSONValue, apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior? = nil, + authService: AWSAuthCredentialsProviderBehavior? = nil, appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol? = nil, logLevel: Amplify.LogLevel? = nil ) throws { @@ -90,7 +91,7 @@ extension AWSAPIPlugin { init( configuration: AmplifyOutputsData, apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior = AWSAuthService(), + authService: AWSAuthCredentialsProviderBehavior = AWSAuthService(), appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol? = nil, logLevel: Amplify.LogLevel = Amplify.Logging.logLevel ) throws { @@ -111,7 +112,7 @@ extension AWSAPIPlugin { init( pluginConfig: AWSAPICategoryPluginConfiguration, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol, logLevel: Amplify.LogLevel ) { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift index ef6bc3c86d..acd8d40513 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift @@ -7,6 +7,7 @@ import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import Foundation final public class AWSAPIPlugin: NSObject, APICategoryPlugin, AWSAPIAuthInformation { @@ -25,7 +26,7 @@ final public class AWSAPIPlugin: NSObject, APICategoryPlugin, AWSAPIAuthInformat /// The provider for Auth services required to access protected APIs. This will be /// populated during the configuration phase, and is clearable by `reset()`. - var authService: AWSAuthServiceBehavior! + var authService: AWSAuthCredentialsProviderBehavior! /// The provider for network connections and operations. This will be populated /// during initialization, and is clearable by `reset()`. diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift index 1ea015dd7f..f52480a517 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift @@ -8,6 +8,7 @@ @_spi(InternalAmplifyConfiguration) import Amplify import Foundation import AWSPluginsCore +import AWSPluginsSDKCore // Convenience typealias typealias APIEndpointName = String @@ -17,11 +18,11 @@ public struct AWSAPICategoryPluginConfiguration { private var interceptors: [APIEndpointName: AWSAPIEndpointInterceptors] private var apiAuthProviderFactory: APIAuthProviderFactory? - private var authService: AWSAuthServiceBehavior? + private var authService: AWSAuthCredentialsProviderBehavior? init(jsonValue: JSONValue, apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior) throws { + authService: AWSAuthCredentialsProviderBehavior) throws { guard case .object(let config) = jsonValue else { throw PluginError.pluginConfigurationError( "Could not cast incoming configuration to a JSONValue `.object`", @@ -50,7 +51,7 @@ public struct AWSAPICategoryPluginConfiguration { init(configuration: AmplifyOutputsData, apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior) throws { + authService: AWSAuthCredentialsProviderBehavior) throws { guard let data = configuration.data else { throw PluginError.pluginConfigurationError( @@ -95,7 +96,7 @@ public struct AWSAPICategoryPluginConfiguration { internal init(endpoints: [APIEndpointName: EndpointConfig], interceptors: [APIEndpointName: AWSAPIEndpointInterceptors] = [:], apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior) { + authService: AWSAuthCredentialsProviderBehavior) { self.endpoints = endpoints self.interceptors = interceptors self.apiAuthProviderFactory = apiAuthProviderFactory @@ -215,7 +216,7 @@ public struct AWSAPICategoryPluginConfiguration { /// - Returns: dictionary of AWSAPIEndpointInterceptors indexed by API endpoint name private static func makeInterceptors(forEndpoints endpoints: [APIEndpointName: EndpointConfig], apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior) throws -> [APIEndpointName: AWSAPIEndpointInterceptors] { + authService: AWSAuthCredentialsProviderBehavior) throws -> [APIEndpointName: AWSAPIEndpointInterceptors] { var interceptors: [APIEndpointName: AWSAPIEndpointInterceptors] = [:] for (name, config) in endpoints { var interceptorsConfig = AWSAPIEndpointInterceptors(endpointName: name, diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift index 5d37dc6c6b..b58119a2b5 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift @@ -8,6 +8,7 @@ import Amplify import Foundation import AWSPluginsCore +import AWSPluginsSDKCore /// The order of interceptor decoration is as follows: /// 1. **prelude interceptors** @@ -22,7 +23,7 @@ struct AWSAPIEndpointInterceptors { let apiEndpointName: APIEndpointName let apiAuthProviderFactory: APIAuthProviderFactory - let authService: AWSAuthServiceBehavior? + let authService: AWSAuthCredentialsProviderBehavior? var preludeInterceptors: [URLRequestInterceptor] = [] @@ -46,7 +47,7 @@ struct AWSAPIEndpointInterceptors { init(endpointName: APIEndpointName, apiAuthProviderFactory: APIAuthProviderFactory, - authService: AWSAuthServiceBehavior? = nil) { + authService: AWSAuthCredentialsProviderBehavior? = nil) { self.apiEndpointName = endpointName self.apiAuthProviderFactory = apiAuthProviderFactory self.authService = authService diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/APIKeyURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/APIKeyURLRequestInterceptor.swift index e737479d9c..cc08e3fdc2 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/APIKeyURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/APIKeyURLRequestInterceptor.swift @@ -7,6 +7,7 @@ import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import Foundation struct APIKeyURLRequestInterceptor: URLRequestInterceptor { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenProviderWrapper.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenProviderWrapper.swift index bc5be0d627..99e9186ea1 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenProviderWrapper.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenProviderWrapper.swift @@ -7,6 +7,7 @@ import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import Foundation class AuthTokenProviderWrapper: AuthTokenProvider { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift index 80ba255a4f..108b31fce9 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift @@ -7,6 +7,7 @@ import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import Foundation struct AuthTokenURLRequestInterceptor: URLRequestInterceptor { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift index fa90ff71f4..0767ff6397 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift @@ -7,6 +7,7 @@ import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import Foundation import ClientRuntime diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift index c3d33320c2..71302c2e8b 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift @@ -7,6 +7,7 @@ import Foundation @_spi(WebSocket) import AWSPluginsCore +import AWSPluginsSDKCore import Amplify import AWSClientRuntime import ClientRuntime diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index dbc93508cc..9595c52bf0 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -8,6 +8,7 @@ import Amplify import Foundation import AWSPluginsCore +import AWSPluginsSDKCore import Combine public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, InternalTaskAsyncThrowingSequence, InternalTaskThrowingChannel { @@ -25,7 +26,7 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, } let appSyncClientFactory: AppSyncRealTimeClientFactoryProtocol let pluginConfig: AWSAPICategoryPluginConfiguration - let authService: AWSAuthServiceBehavior + let authService: AWSAuthCredentialsProviderBehavior var apiAuthProviderFactory: APIAuthProviderFactory private let userAgent = AmplifyAWSServiceConfiguration.userAgentLib private let subscriptionId = UUID().uuidString @@ -35,7 +36,7 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, init(request: Request, pluginConfig: AWSAPICategoryPluginConfiguration, appSyncClientFactory: AppSyncRealTimeClientFactoryProtocol, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, apiAuthProviderFactory: APIAuthProviderFactory) { self.request = request self.pluginConfig = pluginConfig @@ -185,7 +186,7 @@ final public class AWSGraphQLSubscriptionOperation: GraphQLSubscri let pluginConfig: AWSAPICategoryPluginConfiguration let appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol - let authService: AWSAuthServiceBehavior + let authService: AWSAuthCredentialsProviderBehavior private let userAgent = AmplifyAWSServiceConfiguration.userAgentLib var appSyncRealTimeClient: AppSyncRealTimeClientProtocol? @@ -201,7 +202,7 @@ final public class AWSGraphQLSubscriptionOperation: GraphQLSubscri init(request: GraphQLOperationRequest, pluginConfig: AWSAPICategoryPluginConfiguration, appSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, apiAuthProviderFactory: APIAuthProviderFactory, inProcessListener: AWSGraphQLSubscriptionOperation.InProcessListener?, resultListener: AWSGraphQLSubscriptionOperation.ResultListener?) { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift index 1666312feb..b9b8d0b396 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift @@ -9,13 +9,14 @@ import Foundation import Amplify import Combine +import AWSPluginsSDKCore @_spi(WebSocket) import AWSPluginsCore protocol AppSyncRealTimeClientFactoryProtocol { func getAppSyncRealTimeClient( for endpointConfig: AWSAPICategoryPluginConfiguration.EndpointConfig, endpoint: URL, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, authType: AWSAuthorizationType?, apiAuthProviderFactory: APIAuthProviderFactory ) async throws -> AppSyncRealTimeClientProtocol @@ -40,7 +41,7 @@ actor AppSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol { public func getAppSyncRealTimeClient( for endpointConfig: AWSAPICategoryPluginConfiguration.EndpointConfig, endpoint: URL, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, authType: AWSAuthorizationType? = nil, apiAuthProviderFactory: APIAuthProviderFactory ) throws -> AppSyncRealTimeClientProtocol { @@ -90,7 +91,7 @@ actor AppSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol { private func getInterceptor( for authorizationConfiguration: AWSAuthorizationConfiguration, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, apiAuthProviderFactory: APIAuthProviderFactory ) throws -> AppSyncRequestInterceptor & WebSocketInterceptor { switch authorizationConfiguration { diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift index 5e1ed6b31c..d212211abf 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift @@ -11,6 +11,7 @@ import Combine @testable import Amplify @testable import AWSAPIPlugin @testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsSDKCore class AppSyncRealTimeClientTests: XCTestCase { let subscriptionRequest = """ diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift index 2974af9f4d..2dc9f136ba 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift @@ -8,6 +8,7 @@ import XCTest @testable import AWSAPIPlugin import AWSPluginsCore +import AWSPluginsSDKCore // swiftlint:disable:next type_name class AWSAPICategoryPluginInterceptorBehaviorTests: AWSAPICategoryPluginTestBase { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift index a2f7503f01..a89d89b8c5 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift @@ -12,6 +12,7 @@ import Foundation @testable import AWSAPIPlugin @testable import AWSPluginsTestCommon import AWSPluginsCore +import AWSPluginsSDKCore class AWSAPICategoryPluginConfigurationTests: XCTestCase { let graphQLAPI = "graphQLAPI" diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift index a48d5fa96e..d20d4120c4 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift @@ -8,6 +8,7 @@ import XCTest import Amplify import AWSPluginsCore +import AWSPluginsSDKCore @testable import AmplifyTestCommon @testable import AWSAPIPlugin @testable import AWSPluginsTestCommon @@ -102,7 +103,7 @@ class AWSAPIEndpointInterceptorsTests: XCTestCase { // MARK: - Test Helpers - func createAPIInterceptorConfig(authService: AWSAuthServiceBehavior = MockAWSAuthService()) -> AWSAPIEndpointInterceptors { + func createAPIInterceptorConfig(authService: AWSAuthCredentialsProviderBehavior = MockAWSAuthService()) -> AWSAPIEndpointInterceptors { return AWSAPIEndpointInterceptors( endpointName: endpointName, apiAuthProviderFactory: APIAuthProviderFactory(), diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift index 5f25a0dc9a..149fc1923b 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift @@ -7,6 +7,7 @@ import XCTest import AWSPluginsCore +import AWSPluginsSDKCore @testable import Amplify @testable import AmplifyTestCommon @testable import AWSAPIPlugin diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift index 2ba9f97779..7cea9cf879 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift @@ -11,14 +11,14 @@ import Amplify import Combine @testable import AWSAPIPlugin @_spi(WebSocket) import AWSPluginsCore +import AWSPluginsSDKCore struct MockSubscriptionConnectionFactory: AppSyncRealTimeClientFactoryProtocol { - typealias OnGetOrCreateConnection = ( AWSAPICategoryPluginConfiguration.EndpointConfig, URL, - AWSAuthServiceBehavior, + AWSAuthCredentialsProviderBehavior, AWSAuthorizationType?, APIAuthProviderFactory ) async throws -> AppSyncRealTimeClientProtocol @@ -32,7 +32,7 @@ struct MockSubscriptionConnectionFactory: AppSyncRealTimeClientFactoryProtocol { func getAppSyncRealTimeClient( for endpointConfig: AWSAPICategoryPluginConfiguration.EndpointConfig, endpoint: URL, - authService: AWSAuthServiceBehavior, + authService: AWSAuthCredentialsProviderBehavior, authType: AWSAuthorizationType?, apiAuthProviderFactory: APIAuthProviderFactory ) async throws -> AppSyncRealTimeClientProtocol { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index 4581f1d799..d71cc35532 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -13,7 +13,7 @@ import AWSCognitoIdentityProvider import AWSPluginsCore import ClientRuntime import AWSClientRuntime -@_spi(PluginHTTPClientEngine) import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore @_spi(InternalHttpEngineProxy) import AWSPluginsCore extension AWSCognitoAuthPlugin { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift index 81c0d9a77e..3e0b9e8278 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalAmplifyPluginExtension) import AWSPluginsCore +@_spi(InternalAmplifyPluginExtension) import AWSPluginsSDKCore import Foundation import ClientRuntime diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift index 2a8864e5a1..258e42fe33 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalHttpEngineProxy) @_spi(InternalAmplifyPluginExtension) import AWSPluginsCore +@_spi(InternalHttpEngineProxy) @_spi(InternalAmplifyPluginExtension) import AWSPluginsSDKCore import ClientRuntime import Foundation diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift index 80f1b3c4ee..6d6cfe9277 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift @@ -8,7 +8,10 @@ import Foundation import Amplify import AWSClientRuntime +import AWSPluginsCore -public protocol AWSAuthCredentialsProviderBehavior { +public protocol AWSAuthCredentialsProviderBehavior: AWSAuthServiceBehavior { func getCredentialsProvider() -> CredentialsProviding } + + diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService+CredentialsProvider.swift similarity index 58% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService+CredentialsProvider.swift index 8c651aefb7..ec8babe3d5 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProvider.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService+CredentialsProvider.swift @@ -8,9 +8,10 @@ import Foundation import Amplify import AWSClientRuntime +import AWSPluginsCore -public class AWSAuthCredentialsProvider: AWSAuthCredentialsProviderBehavior { - public func getCredentialsProvider() -> CredentialsProviding { +extension AWSAuthService: AWSAuthCredentialsProviderBehavior { + public func getCredentialsProvider() -> AWSClientRuntime.CredentialsProviding { return AmplifyAWSCredentialsProvider() } } diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift index 1959aa58b5..63d1197a6d 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift @@ -8,6 +8,7 @@ import Amplify import AWSClientRuntime import AwsCommonRuntimeKit +import AWSPluginsCore import Foundation public class AmplifyAWSCredentialsProvider: AWSClientRuntime.CredentialsProviding { @@ -24,7 +25,7 @@ public class AmplifyAWSCredentialsProvider: AWSClientRuntime.CredentialsProvidin } } -extension AWSCredentials { +extension AWSPluginsCore.AWSCredentials { func toAWSSDKCredentials() -> AWSClientRuntime.AWSCredentials { if let tempCredentials = self as? AWSTemporaryCredentials { diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift index 4c79d1d77b..39f1bd43f2 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift @@ -7,6 +7,7 @@ import Foundation import Amplify +import AWSPluginsCore public protocol AuthTokenProvider { func getUserPoolAccessToken() async throws -> String diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift b/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift index 3ceee7167e..1265a9130f 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift @@ -8,15 +8,16 @@ import Foundation import Amplify import AWSClientRuntime +import AWSPluginsCore public protocol IAMCredentialsProvider { func getCredentialsProvider() -> CredentialsProviding } public struct BasicIAMCredentialsProvider: IAMCredentialsProvider { - let authService: AWSAuthServiceBehavior + let authService: AWSAuthCredentialsProviderBehavior - public init(authService: AWSAuthServiceBehavior) { + public init(authService: AWSAuthCredentialsProviderBehavior) { self.authService = authService } diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan new file mode 100644 index 0000000000..ca8fdab70a --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "9D7C41C1-F847-4136-AB74-D1E17831BCDD", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "AWSPluginsSDKCoreTests", + "name" : "AWSPluginsSDKCoreTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift index 1dc1c19582..152a1683d9 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import Amplify @testable import AWSPluginsCore +@testable import AWSPluginsSDKCore import AWSClientRuntime class AWSAuthServiceTests: XCTestCase { diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift index f395f6ef18..b141438eb7 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift @@ -8,7 +8,7 @@ @_spi(InternalAmplifyPluginExtension) @_spi(PluginHTTPClientEngine) @_spi(InternalHttpEngineProxy) -import AWSPluginsCore +import AWSPluginsSDKCore import ClientRuntime import XCTest diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSuffixAppenderTests.swift b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSuffixAppenderTests.swift rename to AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift index 3b6b167a92..5a03770137 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSuffixAppenderTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalAmplifyPluginExtension) @_spi(InternalHttpEngineProxy) import AWSPluginsCore +@_spi(InternalAmplifyPluginExtension) @_spi(InternalHttpEngineProxy) import AWSPluginsSDKCore import ClientRuntime import XCTest diff --git a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift index d0d87929a9..9e6a38e77e 100644 --- a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift +++ b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift @@ -8,9 +8,9 @@ import ClientRuntime import AWSClientRuntime import Amplify -import AWSPluginsCore +import AWSPluginsSDKCore -public class MockAWSAuthService: AWSAuthServiceBehavior { +public class MockAWSAuthService: AWSAuthCredentialsProviderBehavior { var interactions: [String] = [] var getIdentityIdError: AuthError? diff --git a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift index 090010b65a..85c3178277 100644 --- a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift +++ b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift @@ -8,6 +8,7 @@ import AWSPluginsCore import ClientRuntime import AWSClientRuntime +import AWSPluginsSDKCore import Foundation class MockAWSSignatureV4Signer: AWSSignatureV4Signer { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift index f5ccca5161..63f1acd748 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueNetworkTests.swift @@ -284,7 +284,7 @@ class OutgoingMutationQueueNetworkTests: SyncEngineTestBase { listenerDelay: TimeInterval ) -> MutateRequestResponder> { MutateRequestResponder> { _ in - try! await Task.sleep(seconds: listenerDelay) + try? await Task.sleep(seconds: listenerDelay) return .failure(.unknown("", "", self.connectionError)) } } diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index 7a59c3c809..acc5d5c3c6 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift @@ -8,7 +8,7 @@ import Foundation @_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore import AWSLocation import AWSClientRuntime diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift index b198b5e45d..ba9193d111 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift @@ -7,6 +7,7 @@ import AWSPinpoint import AWSPluginsCore +import AWSPluginsSDKCore import Foundation extension PinpointEvent { diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift index e93baf21c2..cc5d7c9b22 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift @@ -8,7 +8,7 @@ import AWSClientRuntime import AWSPluginsCore import AWSPinpoint -@_spi(PluginHTTPClientEngine) import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore extension PinpointClient { convenience init(region: String, credentialsProvider: CredentialsProviding) throws { diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift index fd10c9a9fa..a6de992534 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift @@ -8,8 +8,7 @@ import Foundation import AWSPinpoint import ClientRuntime -@_spi(PluginHTTPClientEngine) -import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore @globalActor actor PinpointRequestsRegistry { static let shared = PinpointRequestsRegistry() diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift index 76b32228ce..a23cc1ffa3 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift @@ -6,7 +6,7 @@ // import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore import Amplify import Combine import Foundation diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift index 90bebdf058..bbfce30e3a 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift @@ -8,6 +8,7 @@ import Foundation import Amplify import AWSPluginsCore +import AWSPluginsSDKCore import AWSClientRuntime import ClientRuntime diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift index b90c7e0a03..d336d39552 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift @@ -12,7 +12,7 @@ import AWSTextract import AWSComprehend import AWSPolly import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsCore +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore import Foundation import ClientRuntime import AWSClientRuntime diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index 14dd8aca32..722c5cc066 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -9,14 +9,14 @@ import Foundation import AWSS3 import Amplify import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsCore import ClientRuntime +@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore /// - Tag: AWSS3StorageService class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { // resettable values - private var authService: AWSAuthServiceBehavior? + private var authService: AWSAuthCredentialsProviderBehavior? var logger: Logger! var preSignedURLBuilder: AWSS3PreSignedURLBuilderBehavior! var awsS3: AWSS3Behavior! @@ -48,7 +48,7 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { storageConfiguration.sessionIdentifier } - convenience init(authService: AWSAuthServiceBehavior, + convenience init(authService: AWSAuthCredentialsProviderBehavior, region: String, bucket: String, httpClientEngineProxy: HttpClientEngineProxy? = nil, @@ -107,7 +107,7 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { bucket: bucket) } - init(authService: AWSAuthServiceBehavior, + init(authService: AWSAuthCredentialsProviderBehavior, storageConfiguration: StorageConfiguration = .default, storageTransferDatabase: StorageTransferDatabase = .default, fileSystem: FileSystem = .default, diff --git a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift index 127c151229..cb33e77a87 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift @@ -204,7 +204,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { /// Given: A DefaultStorageTransferDatabase /// When: recover is invoked with a StorageURLSession that returns a session /// Then: A .success is returned - func testLoadPersistableTasks() { + func testLoadPersistableTasks() async { let urlSession = MockStorageURLSession( sessionTasks: [ session @@ -217,18 +217,18 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { } expectation.fulfill() } - waitForExpectations(timeout: 1) + await fulfillment(of: [expectation], timeout: 1) } /// Given: A DefaultStorageTransferDatabase /// When: prepareForBackground is invoked /// Then: A callback is invoked - func testPrepareForBackground() { + func testPrepareForBackground() async { let expectation = self.expectation(description: "Prepare for Background") database.prepareForBackground() { expectation.fulfill() } - waitForExpectations(timeout: 1) + await fulfillment(of: [expectation], timeout: 1) } /// Given: The StorageTransferDatabase Type diff --git a/AmplifyTestCommon/Mocks/MockCredentialsProvider.swift b/AmplifyTestCommon/Mocks/MockCredentialsProvider.swift index 90a1984d6b..5b4f232074 100644 --- a/AmplifyTestCommon/Mocks/MockCredentialsProvider.swift +++ b/AmplifyTestCommon/Mocks/MockCredentialsProvider.swift @@ -5,11 +5,12 @@ // SPDX-License-Identifier: Apache-2.0 // +import AWSPluginsCore import AWSClientRuntime import Foundation -class MockCredentialsProvider: CredentialsProviding { - func getCredentials() async throws -> AWSCredentials { +class MockCredentialsProvider: AWSClientRuntime.CredentialsProviding { + func getCredentials() async throws -> AWSClientRuntime.AWSCredentials { return AWSCredentials( accessKey: "accessKey", secret: "secret", diff --git a/Package.swift b/Package.swift index ef55356bda..b1da193f85 100644 --- a/Package.swift +++ b/Package.swift @@ -44,12 +44,10 @@ let amplifyTargets: [Target] = [ name: "AWSPluginsSDKCore", dependencies: [ "Amplify", + "AWSPluginsCore", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], path: "AmplifyPlugins/Core/AWSPluginsSDKCore", - exclude: [ - "Info.plist" - ], resources: [ .copy("Resources/PrivacyInfo.xcprivacy") ] @@ -59,7 +57,7 @@ let amplifyTargets: [Target] = [ dependencies: [ "Amplify", "CwlPreconditionTesting", - "AWSPluginsCore" + "AWSPluginsSDKCore" ], path: "AmplifyTestCommon", exclude: [ @@ -102,6 +100,7 @@ let amplifyTargets: [Target] = [ dependencies: [ "Amplify", "AWSPluginsCore", + "AWSPluginsSDKCore", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], path: "AmplifyPlugins/Core/AWSPluginsTestCommon", @@ -113,13 +112,21 @@ let amplifyTargets: [Target] = [ name: "AWSPluginsCoreTests", dependencies: [ "AWSPluginsCore", - "AmplifyTestCommon", - .product(name: "AWSClientRuntime", package: "aws-sdk-swift") + "AmplifyTestCommon" ], path: "AmplifyPlugins/Core/AWSPluginsCoreTests", exclude: [ "Info.plist" ] + ), + .testTarget( + name: "AWSPluginsSDKCoreTests", + dependencies: [ + "AWSPluginsSDKCore", + "AmplifyTestCommon", + .product(name: "AWSClientRuntime", package: "aws-sdk-swift") + ], + path: "AmplifyPlugins/Core/AWSPluginsSDKCoreTests" ) ] @@ -128,7 +135,7 @@ let apiTargets: [Target] = [ name: "AWSAPIPlugin", dependencies: [ .target(name: "Amplify"), - .target(name: "AWSPluginsCore") + .target(name: "AWSPluginsSDKCore") ], path: "AmplifyPlugins/API/Sources/AWSAPIPlugin", exclude: [ From 678941ce5ac45f684c62e78e05934f4ad21c8303 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 1 May 2024 11:36:58 -0700 Subject: [PATCH 14/23] remove lock from SyncMutationToCloudOperation --- .../SyncMutationToCloudOperation.swift | 26 +++---------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift index db45220167..4321411644 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift @@ -23,8 +23,6 @@ class SyncMutationToCloudOperation: AsynchronousOperation { private let completion: GraphQLOperation>.ResultListener private let requestRetryablePolicy: RequestRetryablePolicy -// private let lock: NSRecursiveLock - private var networkReachabilityPublisher: AnyPublisher? private var mutationOperation: Task? private var mutationRetryNotifier: MutationRetryNotifier? @@ -46,7 +44,6 @@ class SyncMutationToCloudOperation: AsynchronousOperation { self.completion = completion self.currentAttemptNumber = currentAttemptNumber self.requestRetryablePolicy = requestRetryablePolicy ?? RequestRetryablePolicy() -// self.lock = NSRecursiveLock() if let modelSchema = ModelRegistry.modelSchema(from: mutationEvent.modelName), let mutationType = GraphQLMutationType(rawValue: mutationEvent.mutationType) { @@ -66,17 +63,14 @@ class SyncMutationToCloudOperation: AsynchronousOperation { override func cancel() { log.verbose(#function) -// lock.execute { mutationOperation?.cancel() mutationRetryNotifier?.cancel() mutationRetryNotifier = nil -// } let apiError = APIError(error: OperationCancelledError()) finish(result: .failure(apiError)) } - /// Does not require a locking context. Member access is read-only. private func sendMutationToCloud(withAuthType authType: AWSAuthorizationType? = nil) { guard !isCancelled else { return @@ -218,22 +212,18 @@ class SyncMutationToCloudOperation: AsynchronousOperation { result = .failure(.unknown("Failed to send sync mutation request", "", error)) } -// self?.lock.execute { [weak self] in self?.respond( toCloudResult: result, withAPIRequest: apiRequest ) -// } } } - /// Initiates a locking context private func respond( toCloudResult result: GraphQLResponse>, withAPIRequest apiRequest: GraphQLRequest> ) { -// lock.execute { guard !self.isCancelled else { Amplify.log.debug("SyncMutationToCloudOperation cancelled, aborting") return @@ -241,10 +231,8 @@ class SyncMutationToCloudOperation: AsynchronousOperation { log.verbose("GraphQL mutation operation received result: \(result)") validate(cloudResult: result, request: apiRequest) -// } } - /// - Warning: Must be invoked from a locking context private func validate( cloudResult: GraphQLResponse>, request: GraphQLRequest> @@ -275,7 +263,6 @@ class SyncMutationToCloudOperation: AsynchronousOperation { finish(result: .success(cloudResult)) } - /// - Warning: Must be invoked from a locking context private func resolveReachabilityPublisher(request: GraphQLRequest>) { if networkReachabilityPublisher == nil { if let reachability = api as? APICategoryReachabilityBehavior { @@ -292,7 +279,6 @@ class SyncMutationToCloudOperation: AsynchronousOperation { } } - /// - Warning: Must be invoked from a locking context func getRetryAdviceIfRetryable(error: APIError) -> RequestRetryAdvice { var advice = RequestRetryAdvice(shouldRetry: false, retryInterval: DispatchTimeInterval.never) @@ -334,13 +320,11 @@ class SyncMutationToCloudOperation: AsynchronousOperation { return advice } - /// - Warning: Must be invoked from a locking context private func shouldRetryWithDifferentAuthType() -> RequestRetryAdvice { let shouldRetry = authTypesIterator?.hasNext == true return RequestRetryAdvice(shouldRetry: shouldRetry, retryInterval: .milliseconds(0)) } - /// - Warning: Must be invoked from a locking context private func scheduleRetry(advice: RequestRetryAdvice, withAuthType authType: AWSAuthorizationType? = nil) { log.verbose("\(#function) scheduling retry for mutation \(advice)") @@ -353,23 +337,19 @@ class SyncMutationToCloudOperation: AsynchronousOperation { currentAttemptNumber += 1 } - /// Initiates a locking context + private func respondToMutationNotifierTriggered(withAuthType authType: AWSAuthorizationType?) { log.verbose("\(#function) mutationRetryNotifier triggered") -// lock.execute { - sendMutationToCloud(withAuthType: authType) - mutationRetryNotifier = nil -// } + sendMutationToCloud(withAuthType: authType) + mutationRetryNotifier = nil } /// Cleans up operation resources, finalizes AsynchronousOperation states, and invokes `completion` with `result` /// - Parameter result: The MutationSyncCloudResult to pass to `completion` private func finish(result: MutationSyncCloudResult) { log.verbose(#function) -// lock.execute { mutationOperation?.cancel() mutationOperation = nil -// } DispatchQueue.global().async { self.completion(result) From 0868a9fc2bc3b1dc2c50848065b5875b0833dfcf Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 1 May 2024 11:55:48 -0700 Subject: [PATCH 15/23] remove test case of retryable for signOut error --- .../API/RetryableGraphQLOperationTests.swift | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift index 9822c6b3d4..0f810d9838 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -14,32 +14,6 @@ import Combine class RetryableGraphQLOperationTests: XCTestCase { let testApiName = "apiName" -// /// Given: a RetryableGraphQLOperation with 2 operations -// /// When: the first one fails with a .signedOut error, the next one succeed with response -// /// Then: return the success response -// func testShouldRetryOperationWithSignedOutAuthError() async throws { -// let expectation1 = expectation(description: "Operation 1 throws signed out auth error") -// let operation1: () async throws -> GraphQLResponse = { -// expectation1.fulfill() -// throw APIError.operationError("", "", AuthError.signedOut("", "")) -// } -// -// let expectation2 = expectation(description: "Operation 2 successfully finished") -// let operation2: () async throws -> GraphQLResponse = { -// expectation2.fulfill() -// return .success("operation 2") -// } -// -// let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() -// let result = await RetryableGraphQLOperation(requestStream: publisher).run() -// if case .success(.success(let string)) = result { -// XCTAssertEqual(string, "operation 2") -// } else { -// XCTFail("Wrong result") -// } -// await fulfillment(of: [expectation1, expectation2], timeout: 1) -// } - /// Given: a RetryableGraphQLOperation with 2 operations /// When: the first one fails with a .notAuthorized error, the next one succeed with response /// Then: return the success response From 9df152407fa1e0410266a33c2475b145a522cc47 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 13 May 2024 16:05:47 -0700 Subject: [PATCH 16/23] resolve comments --- Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift | 1 - .../Operation/AWSGraphQLSubscriptionTaskRunner.swift | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift index 6a4841f13b..a1a450fd28 100644 --- a/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift +++ b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift @@ -6,7 +6,6 @@ // import Foundation -import Combine public typealias WeakAmplifyAsyncThrowingSequenceRef = WeakRef> diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 9595c52bf0..b75a1e0a94 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -114,8 +114,8 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, self.subscription = try await appSyncClient?.subscribe( id: subscriptionId, query: encodeRequest(query: request.document, variables: request.variables) - ).sink(receiveValue: { event in - self.onAsyncSubscriptionEvent(event: event) + ).sink(receiveValue: { [weak self] event in + self?.onAsyncSubscriptionEvent(event: event) }) } catch { let error = APIError.operationError("Unable to get connection for api \(endpointConfig.name)", "", error) From 7415441708c5ec7f6c4c222064d59b89266a58db Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 16 May 2024 17:30:58 +0000 Subject: [PATCH 17/23] fix(datastore): propagate remote mutationEvents to Hub for sync received (#3697) --- .../ReconcileAndLocalSaveOperation.swift | 79 ++++++++++++------- .../ReconcileAndLocalSaveOperationTests.swift | 64 +++++++++------ 2 files changed, 91 insertions(+), 52 deletions(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift index 474f76666e..7ba4449c50 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift @@ -337,7 +337,7 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } enum ApplyRemoteModelResult { - case applied(RemoteModel) + case applied(RemoteModel, AppliedModel) case dropped } @@ -363,7 +363,7 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { promise(.failure(dataStoreError)) } case .success: - promise(.success(.applied(remoteModel))) + promise(.success(.applied(remoteModel, remoteModel))) } } } @@ -387,14 +387,13 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { let anyModel: AnyModel do { anyModel = try savedModel.eraseToAnyModel() + let appliedModel = MutationSync(model: anyModel, syncMetadata: remoteModel.syncMetadata) + promise(.success(.applied(remoteModel, appliedModel))) } catch { let dataStoreError = DataStoreError(error: error) self.notifyDropped(error: dataStoreError) promise(.failure(dataStoreError)) - return } - let inProcessModel = MutationSync(model: anyModel, syncMetadata: remoteModel.syncMetadata) - promise(.success(.applied(inProcessModel))) } } } @@ -417,21 +416,15 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { result: ApplyRemoteModelResult, mutationType: MutationEvent.MutationType ) -> AnyPublisher { - if case let .applied(inProcessModel) = result { - return self.saveMetadata(storageAdapter: storageAdapter, remoteModel: inProcessModel, mutationType: mutationType) - .handleEvents( receiveOutput: { syncMetadata in - let appliedModel = MutationSync(model: inProcessModel.model, syncMetadata: syncMetadata) - self.notify(savedModel: appliedModel, mutationType: mutationType) - }, receiveCompletion: { completion in - if case .failure(let error) = completion { - self.notifyDropped(error: error) - } - }) - .map { _ in () } + switch result { + case .applied(let remoteModel, let appliedModel): + return self.saveMetadata(storageAdapter: storageAdapter, remoteModel: remoteModel, mutationType: mutationType) + .map { MutationSync(model: appliedModel.model, syncMetadata: $0) } + .map { [weak self] in self?.notify(appliedModel: $0, mutationType: mutationType) } .eraseToAnyPublisher() - + case .dropped: + return Just(()).setFailureType(to: DataStoreError.self).eraseToAnyPublisher() } - return Just(()).setFailureType(to: DataStoreError.self).eraseToAnyPublisher() } private func saveMetadata( @@ -440,9 +433,17 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { mutationType: MutationEvent.MutationType ) -> Future { Future { promise in - storageAdapter.save(remoteModel.syncMetadata, - condition: nil, - eagerLoad: self.isEagerLoad) { result in + storageAdapter.save( + remoteModel.syncMetadata, + condition: nil, + eagerLoad: self.isEagerLoad + ) { result in + switch result { + case .failure(let error): + self.notifyDropped(error: error) + case .success: + self.notifyHub(remoteModel: remoteModel, mutationType: mutationType) + } promise(result) } } @@ -454,28 +455,46 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { } } - private func notify(savedModel: AppliedModel, - mutationType: MutationEvent.MutationType) { - let version = savedModel.syncMetadata.version + /// Inform the mutationEvents subscribers about the updated model, + /// which incorporates lazy loading information if applicable. + private func notify(appliedModel: AppliedModel, mutationType: MutationEvent.MutationType) { + guard let json = try? appliedModel.model.instance.toJSON() else { + log.error("Could not notify mutation event") + return + } + + let modelIdentifier = appliedModel.model.instance.identifier(schema: modelSchema).stringValue + let mutationEvent = MutationEvent(modelId: modelIdentifier, + modelName: modelSchema.name, + json: json, + mutationType: mutationType, + version: appliedModel.syncMetadata.version) + mutationEventPublisher.send(.mutationEvent(mutationEvent)) + } + /// Inform the remote mutationEvents to Hub event subscribers, + /// which only contains information received from AppSync server. + private func notifyHub( + remoteModel: RemoteModel, + mutationType: MutationEvent.MutationType + ) { // TODO: Dispatch/notify error if we can't erase to any model? Would imply an error in JSON decoding, // which shouldn't be possible this late in the process. Possibly notify global conflict/error handler? - guard let json = try? savedModel.model.instance.toJSON() else { - log.error("Could not notify mutation event") + guard let json = try? remoteModel.model.instance.toJSON() else { + log.error("Could not notify Hub mutation event") return } - let modelIdentifier = savedModel.model.instance.identifier(schema: modelSchema).stringValue + + let modelIdentifier = remoteModel.model.instance.identifier(schema: modelSchema).stringValue let mutationEvent = MutationEvent(modelId: modelIdentifier, modelName: modelSchema.name, json: json, mutationType: mutationType, - version: version) + version: remoteModel.syncMetadata.version) let payload = HubPayload(eventName: HubPayload.EventName.DataStore.syncReceived, data: mutationEvent) Amplify.Hub.dispatch(to: .dataStore, payload: payload) - - mutationEventPublisher.send(.mutationEvent(mutationEvent)) } private func notifyFinished() { diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift index 610c58ba31..961adb38d7 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift @@ -705,16 +705,16 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { waitForExpectations(timeout: 1) } - func testApplyRemoteModels_deleteDisposition() { + func testApplyRemoteModels_deleteDisposition() async { let expect = expectation(description: "operation should send value and complete successfully") expect.expectedFulfillmentCount = 2 - let stoargeExpect = expectation(description: "storage delete should be called") + let storageExpect = expectation(description: "storage delete should be called") let storageMetadataExpect = expectation(description: "storage save metadata should be called") let notifyExpect = expectation(description: "mutation event should be emitted") let hubExpect = expectation(description: "Hub is notified") let deleteResponder = DeleteUntypedModelCompletionResponder { _, id in XCTAssertEqual(id, self.anyPostMutationSync.model.id) - stoargeExpect.fulfill() + storageExpect.fulfill() return .emptyResult } storageAdapter.responders[.deleteUntypedModel] = deleteResponder @@ -758,24 +758,33 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in expect.fulfill() }).store(in: &cancellables) - waitForExpectations(timeout: 1) - + + await fulfillment(of: [ + expect, + storageExpect, + storageMetadataExpect, + notifyExpect, + hubExpect + ], timeout: 1) } - func testApplyRemoteModels_multipleDispositions() { - let dispositions: [RemoteSyncReconciler.Disposition] = [.create(anyPostMutationSync), - .create(anyPostMutationSync), - .update(anyPostMutationSync), - .update(anyPostMutationSync), - .delete(anyPostMutationSync), - .delete(anyPostMutationSync), - .create(anyPostMutationSync), - .update(anyPostMutationSync), - .delete(anyPostMutationSync)] + func testApplyRemoteModels_multipleDispositions() async { + let dispositions: [RemoteSyncReconciler.Disposition] = [ + .create(anyPostMutationSync), + .create(anyPostMutationSync), + .update(anyPostMutationSync), + .update(anyPostMutationSync), + .delete(anyPostMutationSync), + .delete(anyPostMutationSync), + .create(anyPostMutationSync), + .update(anyPostMutationSync), + .delete(anyPostMutationSync) + ] + let expect = expectation(description: "should complete successfully") expect.expectedFulfillmentCount = 2 - let stoargeExpect = expectation(description: "storage save/delete should be called") - stoargeExpect.expectedFulfillmentCount = dispositions.count + let storageExpect = expectation(description: "storage save/delete should be called") + storageExpect.expectedFulfillmentCount = dispositions.count let storageMetadataExpect = expectation(description: "storage save metadata should be called") storageMetadataExpect.expectedFulfillmentCount = dispositions.count let notifyExpect = expectation(description: "mutation event should be emitted") @@ -784,14 +793,14 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { hubExpect.expectedFulfillmentCount = dispositions.count let saveResponder = SaveUntypedModelResponder { _, completion in - stoargeExpect.fulfill() + storageExpect.fulfill() completion(.success(self.anyPostMutationSync.model)) } storageAdapter.responders[.saveUntypedModel] = saveResponder let deleteResponder = DeleteUntypedModelCompletionResponder { _, id in XCTAssertEqual(id, self.anyPostMutationSync.model.id) - stoargeExpect.fulfill() + storageExpect.fulfill() return .emptyResult } storageAdapter.responders[.deleteUntypedModel] = deleteResponder @@ -835,10 +844,16 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in expect.fulfill() }).store(in: &cancellables) - waitForExpectations(timeout: 1) + await fulfillment(of: [ + expect, + storageExpect, + storageMetadataExpect, + notifyExpect, + hubExpect + ], timeout: 1) } - func testApplyRemoteModels_skipFailedOperations() throws { + func testApplyRemoteModels_skipFailedOperations() async throws { let dispositions: [RemoteSyncReconciler.Disposition] = [.create(anyPostMutationSync), .create(anyPostMutationSync), .update(anyPostMutationSync), @@ -890,7 +905,12 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in }).store(in: &cancellables) - waitForExpectations(timeout: 1) + + await fulfillment(of: [ + expect, + expectedDropped, + expectedDeleteSuccess + ], timeout: 1) } func testApplyRemoteModels_failWithConstraintViolationShouldBeSuccessful() { From 75b9a455d5c0c2c27f567b5ae6e0977f769bedeb Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 17 May 2024 11:45:38 -0700 Subject: [PATCH 18/23] rename the package to InternalAmplifyCredentials --- .../xcschemes/Amplify-Package.xcscheme | 68 +++++++++++++++++++ .../AWSAPIPlugin/AWSAPIPlugin+Configure.swift | 2 +- .../Sources/AWSAPIPlugin/AWSAPIPlugin.swift | 2 +- .../AWSAPICategoryPluginConfiguration.swift | 2 +- .../AWSAPIEndpointInterceptors.swift | 2 +- .../APIKeyURLRequestInterceptor.swift | 2 +- .../AuthTokenProviderWrapper.swift | 2 +- .../AuthTokenURLRequestInterceptor.swift | 2 +- .../IAMURLRequestInterceptor.swift | 2 +- .../IAMAuthInterceptor.swift | 2 +- .../AWSGraphQLSubscriptionTaskRunner.swift | 2 +- .../AppSyncRealTimeClientFactory.swift | 2 +- .../AppSyncRealTimeClientTests.swift | 2 +- ...egoryPlugin+InterceptorBehaviorTests.swift | 2 +- ...SAPICategoryPluginConfigurationTests.swift | 2 +- .../AWSAPIEndpointInterceptorsTests.swift | 2 +- .../AuthTokenURLRequestInterceptorTests.swift | 2 +- .../Mocks/MockSubscription.swift | 2 +- .../AWSCognitoAuthPlugin+Configure.swift | 2 +- ...AWSCognitoAuthPlugin+PluginExtension.swift | 2 +- .../Support/Utils/HttpClientEngineProxy.swift | 2 +- .../MockAWSAuthService.swift | 2 +- .../MockAWSSignatureV4Signer.swift | 2 +- .../AWSAuthCredentialsProviderBehavior.swift | 0 .../AWSAuthService+CredentialsProvider.swift | 0 .../AWSPluginExtension.swift | 0 .../AmplifyAWSCredentialsProvider.swift | 0 .../AmplifyAWSServiceConfiguration.swift | 0 .../AmplifyAWSSignatureV4Signer.swift | 0 .../AuthTokenProvider.swift | 0 .../ClientRuntimeFoundationBridge.swift | 0 .../FoundationClientEngine.swift | 0 .../FoundationClientEngineError.swift | 0 .../PluginClientEngine.swift | 0 .../SdkHttpRequest+updatingUserAgent.swift | 0 .../UserAgentSettingClientEngine.swift | 0 .../UserAgentSuffixAppender.swift | 0 .../IAMCredentialProvider.swift | 0 .../Resources/PrivacyInfo.xcprivacy | 0 ...lifyAWSServiceConfiguration+Platform.swift | 0 .../AWSPluginsSDKCore.xctestplan | 4 +- .../Auth/AWSAuthServiceTests.swift | 2 +- .../UserAgentSettingClientEngineTests.swift | 2 +- .../Utils/UserAgentSuffixAppenderTests.swift | 2 +- .../AWSLocationGeoPlugin+Configure.swift | 2 +- .../PinpointEvent+PinpointClientTypes.swift | 2 +- .../PinpointClient+CredentialsProvider.swift | 2 +- .../Utils/PinpointRequestsRegistry.swift | 2 +- ...WSCloudWatchLoggingSessionController.swift | 2 +- ...aultRemoteLoggingConstraintsProvider.swift | 2 +- .../Predictions/AWSPredictionsService.swift | 2 +- .../Service/Storage/AWSS3StorageService.swift | 2 +- Package.swift | 32 ++++----- 53 files changed, 119 insertions(+), 51 deletions(-) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AWSAuthCredentialsProviderBehavior.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AWSAuthService+CredentialsProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AWSPluginExtension.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AmplifyAWSCredentialsProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AmplifyAWSServiceConfiguration.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AmplifyAWSSignatureV4Signer.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/AuthTokenProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/FoundationClientEngine.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/FoundationClientEngineError.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/PluginClientEngine.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/UserAgentSettingClientEngine.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/CustomHttpClientEngine/UserAgentSuffixAppender.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/IAMCredentialProvider.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/Resources/PrivacyInfo.xcprivacy (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCore => AmplifyCredentials}/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift (100%) rename AmplifyPlugins/Core/{AWSPluginsSDKCoreTests => AmplifyCredentialsTests}/AWSPluginsSDKCore.xctestplan (73%) rename AmplifyPlugins/Core/{AWSPluginsSDKCoreTests => AmplifyCredentialsTests}/Auth/AWSAuthServiceTests.swift (99%) rename AmplifyPlugins/Core/{AWSPluginsSDKCoreTests => AmplifyCredentialsTests}/Utils/UserAgentSettingClientEngineTests.swift (99%) rename AmplifyPlugins/Core/{AWSPluginsSDKCoreTests => AmplifyCredentialsTests}/Utils/UserAgentSuffixAppenderTests.swift (98%) diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme index 77f9846963..5fda4b76eb 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme @@ -496,6 +496,34 @@ ReferencedContainer = "container:"> + + + + + + + + + + + + + + + + + + + + + + + + : InternalTaskRunner, InternalTaskAsyncThrowingSequence, InternalTaskThrowingChannel { diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift index b9b8d0b396..57a3708e1e 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift @@ -9,7 +9,7 @@ import Foundation import Amplify import Combine -import AWSPluginsSDKCore +import InternalAmplifyCredentials @_spi(WebSocket) import AWSPluginsCore protocol AppSyncRealTimeClientFactoryProtocol { diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift index d212211abf..c151f9c50c 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift @@ -11,7 +11,7 @@ import Combine @testable import Amplify @testable import AWSAPIPlugin @testable @_spi(WebSocket) import AWSPluginsCore -@testable import AWSPluginsSDKCore +@testable import InternalAmplifyCredentials class AppSyncRealTimeClientTests: XCTestCase { let subscriptionRequest = """ diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift index 2dc9f136ba..ac458b9992 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift @@ -8,7 +8,7 @@ import XCTest @testable import AWSAPIPlugin import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials // swiftlint:disable:next type_name class AWSAPICategoryPluginInterceptorBehaviorTests: AWSAPICategoryPluginTestBase { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift index a89d89b8c5..b71e5491a8 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift @@ -12,7 +12,7 @@ import Foundation @testable import AWSAPIPlugin @testable import AWSPluginsTestCommon import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials class AWSAPICategoryPluginConfigurationTests: XCTestCase { let graphQLAPI = "graphQLAPI" diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift index d20d4120c4..03520086ef 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift @@ -8,7 +8,7 @@ import XCTest import Amplify import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials @testable import AmplifyTestCommon @testable import AWSAPIPlugin @testable import AWSPluginsTestCommon diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift index 149fc1923b..74e1041f82 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift @@ -7,7 +7,7 @@ import XCTest import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials @testable import Amplify @testable import AmplifyTestCommon @testable import AWSAPIPlugin diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift index 7cea9cf879..51c1feea3e 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift @@ -11,7 +11,7 @@ import Amplify import Combine @testable import AWSAPIPlugin @_spi(WebSocket) import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials struct MockSubscriptionConnectionFactory: AppSyncRealTimeClientFactoryProtocol { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index d71cc35532..721880d462 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -13,7 +13,7 @@ import AWSCognitoIdentityProvider import AWSPluginsCore import ClientRuntime import AWSClientRuntime -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials @_spi(InternalHttpEngineProxy) import AWSPluginsCore extension AWSCognitoAuthPlugin { diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift index 3e0b9e8278..e617062e5c 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+PluginExtension.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalAmplifyPluginExtension) import AWSPluginsSDKCore +@_spi(InternalAmplifyPluginExtension) import InternalAmplifyCredentials import Foundation import ClientRuntime diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift index 258e42fe33..b3a5edf8d2 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/Support/Utils/HttpClientEngineProxy.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalHttpEngineProxy) @_spi(InternalAmplifyPluginExtension) import AWSPluginsSDKCore +@_spi(InternalHttpEngineProxy) @_spi(InternalAmplifyPluginExtension) import InternalAmplifyCredentials import ClientRuntime import Foundation diff --git a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift index 9e6a38e77e..7f11328ad1 100644 --- a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift +++ b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift @@ -8,7 +8,7 @@ import ClientRuntime import AWSClientRuntime import Amplify -import AWSPluginsSDKCore +import InternalAmplifyCredentials public class MockAWSAuthService: AWSAuthCredentialsProviderBehavior { diff --git a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift index 85c3178277..abeb670efb 100644 --- a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift +++ b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift @@ -8,7 +8,7 @@ import AWSPluginsCore import ClientRuntime import AWSClientRuntime -import AWSPluginsSDKCore +import InternalAmplifyCredentials import Foundation class MockAWSSignatureV4Signer: AWSSignatureV4Signer { diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthCredentialsProviderBehavior.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthCredentialsProviderBehavior.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AWSAuthCredentialsProviderBehavior.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService+CredentialsProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthService+CredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSAuthService+CredentialsProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AWSAuthService+CredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AWSPluginExtension.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSPluginExtension.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AWSPluginExtension.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AWSPluginExtension.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSCredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSCredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSServiceConfiguration.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSSignatureV4Signer.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSSignatureV4Signer.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AmplifyAWSSignatureV4Signer.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSSignatureV4Signer.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AuthTokenProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/AuthTokenProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AuthTokenProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/FoundationClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/FoundationClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/FoundationClientEngineError.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngineError.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/FoundationClientEngineError.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngineError.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/PluginClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/PluginClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/PluginClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/PluginClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSettingClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSettingClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSettingClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSettingClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSuffixAppender.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSuffixAppender.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/CustomHttpClientEngine/UserAgentSuffixAppender.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSuffixAppender.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/IAMCredentialProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/IAMCredentialProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/IAMCredentialProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy b/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/Resources/PrivacyInfo.xcprivacy rename to AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift b/AmplifyPlugins/Core/AmplifyCredentials/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsSDKCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift rename to AmplifyPlugins/Core/AmplifyCredentials/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan b/AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan similarity index 73% rename from AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan rename to AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan index ca8fdab70a..8a0c4734d0 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/AWSPluginsSDKCore.xctestplan +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan @@ -15,8 +15,8 @@ { "target" : { "containerPath" : "container:", - "identifier" : "AWSPluginsSDKCoreTests", - "name" : "AWSPluginsSDKCoreTests" + "identifier" : "InternalAmplifyCredentialsTests", + "name" : "InternalAmplifyCredentialsTests" } } ], diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift index 152a1683d9..abca5ad99c 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Auth/AWSAuthServiceTests.swift +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Amplify @testable import AWSPluginsCore -@testable import AWSPluginsSDKCore +@testable import InternalAmplifyCredentials import AWSClientRuntime class AWSAuthServiceTests: XCTestCase { diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift index b141438eb7..a6b2d3a800 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSettingClientEngineTests.swift +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift @@ -8,7 +8,7 @@ @_spi(InternalAmplifyPluginExtension) @_spi(PluginHTTPClientEngine) @_spi(InternalHttpEngineProxy) -import AWSPluginsSDKCore +import InternalAmplifyCredentials import ClientRuntime import XCTest diff --git a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSuffixAppenderTests.swift similarity index 98% rename from AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSuffixAppenderTests.swift index 5a03770137..dece4394d4 100644 --- a/AmplifyPlugins/Core/AWSPluginsSDKCoreTests/Utils/UserAgentSuffixAppenderTests.swift +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSuffixAppenderTests.swift @@ -5,7 +5,7 @@ // SPDX-License-Identifier: Apache-2.0 // -@_spi(InternalAmplifyPluginExtension) @_spi(InternalHttpEngineProxy) import AWSPluginsSDKCore +@_spi(InternalAmplifyPluginExtension) @_spi(InternalHttpEngineProxy) import InternalAmplifyCredentials import ClientRuntime import XCTest diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index acc5d5c3c6..6b194b48af 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift @@ -8,7 +8,7 @@ import Foundation @_spi(InternalAmplifyConfiguration) import Amplify import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials import AWSLocation import AWSClientRuntime diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift index ba9193d111..e2c78b496e 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Analytics/PinpointEvent+PinpointClientTypes.swift @@ -7,7 +7,7 @@ import AWSPinpoint import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials import Foundation extension PinpointEvent { diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift index cc5d7c9b22..1d7a9d7cd9 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift @@ -8,7 +8,7 @@ import AWSClientRuntime import AWSPluginsCore import AWSPinpoint -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials extension PinpointClient { convenience init(region: String, credentialsProvider: CredentialsProviding) throws { diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift index a6de992534..9a2b02fa87 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift @@ -8,7 +8,7 @@ import Foundation import AWSPinpoint import ClientRuntime -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials @globalActor actor PinpointRequestsRegistry { static let shared = PinpointRequestsRegistry() diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift index a23cc1ffa3..733dc11828 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift @@ -6,7 +6,7 @@ // import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials import Amplify import Combine import Foundation diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift index bbfce30e3a..6a11ded903 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/Configuration/DefaultRemoteLoggingConstraintsProvider.swift @@ -8,7 +8,7 @@ import Foundation import Amplify import AWSPluginsCore -import AWSPluginsSDKCore +import InternalAmplifyCredentials import AWSClientRuntime import ClientRuntime diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift index d336d39552..9470da9c2b 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift @@ -12,7 +12,7 @@ import AWSTextract import AWSComprehend import AWSPolly import AWSPluginsCore -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials import Foundation import ClientRuntime import AWSClientRuntime diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index 722c5cc066..a930bb2184 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -10,7 +10,7 @@ import AWSS3 import Amplify import AWSPluginsCore import ClientRuntime -@_spi(PluginHTTPClientEngine) import AWSPluginsSDKCore +@_spi(PluginHTTPClientEngine) import InternalAmplifyCredentials /// - Tag: AWSS3StorageService class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { diff --git a/Package.swift b/Package.swift index 0907b7fd68..c1193ff4eb 100644 --- a/Package.swift +++ b/Package.swift @@ -41,13 +41,13 @@ let amplifyTargets: [Target] = [ ] ), .target( - name: "AWSPluginsSDKCore", + name: "InternalAmplifyCredentials", dependencies: [ "Amplify", "AWSPluginsCore", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], - path: "AmplifyPlugins/Core/AWSPluginsSDKCore", + path: "AmplifyPlugins/Core/AmplifyCredentials", resources: [ .copy("Resources/PrivacyInfo.xcprivacy") ] @@ -57,7 +57,7 @@ let amplifyTargets: [Target] = [ dependencies: [ "Amplify", "CwlPreconditionTesting", - "AWSPluginsSDKCore" + "InternalAmplifyCredentials" ], path: "AmplifyTestCommon", exclude: [ @@ -100,7 +100,7 @@ let amplifyTargets: [Target] = [ dependencies: [ "Amplify", "AWSPluginsCore", - "AWSPluginsSDKCore", + "InternalAmplifyCredentials", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], path: "AmplifyPlugins/Core/AWSPluginsTestCommon", @@ -120,13 +120,13 @@ let amplifyTargets: [Target] = [ ] ), .testTarget( - name: "AWSPluginsSDKCoreTests", + name: "InternalAmplifyCredentialsTests", dependencies: [ - "AWSPluginsSDKCore", + "InternalAmplifyCredentials", "AmplifyTestCommon", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], - path: "AmplifyPlugins/Core/AWSPluginsSDKCoreTests" + path: "AmplifyPlugins/Core/AmplifyCredentialsTests" ) ] @@ -135,7 +135,7 @@ let apiTargets: [Target] = [ name: "AWSAPIPlugin", dependencies: [ .target(name: "Amplify"), - .target(name: "AWSPluginsSDKCore") + .target(name: "InternalAmplifyCredentials") ], path: "AmplifyPlugins/API/Sources/AWSAPIPlugin", exclude: [ @@ -181,7 +181,7 @@ let authTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AmplifySRP"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSClientRuntime", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentityProvider", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentity", package: "aws-sdk-swift") @@ -255,7 +255,7 @@ let storageTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSS3", package: "aws-sdk-swift")], path: "AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin", exclude: [ @@ -286,7 +286,7 @@ let geoTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSLocation", package: "aws-sdk-swift")], path: "AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin", exclude: [ @@ -318,7 +318,7 @@ let internalPinpointTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AWSCognitoAuthPlugin"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "SQLite", package: "SQLite.swift"), .product(name: "AWSPinpoint", package: "aws-sdk-swift"), .product(name: "AmplifyUtilsNotifications", package: "amplify-swift-utils-notifications") @@ -387,7 +387,7 @@ let predictionsTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .target(name: "CoreMLPredictionsPlugin"), .product(name: "AWSComprehend", package: "aws-sdk-swift"), .product(name: "AWSPolly", package: "aws-sdk-swift"), @@ -438,7 +438,7 @@ let loggingTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), - .target(name: "AWSPluginsSDKCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSCloudWatchLogs", package: "aws-sdk-swift"), ], path: "AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin", @@ -485,8 +485,8 @@ let package = Package( targets: ["AWSPluginsCore"] ), .library( - name: "AWSPluginsSDKCore", - targets: ["AWSPluginsSDKCore"] + name: "InternalAmplifyCredentials", + targets: ["InternalAmplifyCredentials"] ), .library( name: "AWSAPIPlugin", From 472f89c4fe790ee8488c5c5f073fd7c4e1b33d0e Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 17 May 2024 13:07:06 -0700 Subject: [PATCH 19/23] rewrite NondeterminsticOperation constructor with makeStream --- .../API/Operation/NondeterminsticOperation.swift | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/Amplify/Categories/API/Operation/NondeterminsticOperation.swift b/Amplify/Categories/API/Operation/NondeterminsticOperation.swift index 7c930b1924..cd17b65fe5 100644 --- a/Amplify/Categories/API/Operation/NondeterminsticOperation.swift +++ b/Amplify/Categories/API/Operation/NondeterminsticOperation.swift @@ -42,14 +42,15 @@ final class NondeterminsticOperation { shouldTryNextOnError: OnError? = nil ) { var cancellables = Set() + let (asyncStream, continuation) = AsyncStream.makeStream(of: Operation.self) + operationStream.sink { _ in + continuation.finish() + } receiveValue: { + continuation.yield($0) + }.store(in: &cancellables) + self.init( - operations: AsyncStream { continuation in - operationStream.sink { _ in - continuation.finish() - } receiveValue: { operation in - continuation.yield(operation) - }.store(in: &cancellables) - }, + operations: asyncStream, shouldTryNextOnError: shouldTryNextOnError ) self.cancellables = cancellables From a6b16841f66cef309a15d1a6651fc28a1fd07204 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 17 May 2024 13:45:26 -0700 Subject: [PATCH 20/23] resolve broken test case after merging latest orgin/main --- .../Core/AppSyncListProviderPaginationTests.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift index 45b16f808b..410077d90d 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift @@ -108,12 +108,10 @@ extension AppSyncListProviderTests { func testLoadedStateGetNextPageFailure_GraphQLErrorResponse() async { mockAPIPlugin.responders[.queryRequestResponse] = QueryRequestResponder> { request in - XCTAssertEqual(request.apiName, "apiName") XCTAssertEqual(request.authMode as? AWSAuthorizationType, .amazonCognitoUserPools) - let event: GraphQLOperation>.OperationResult = .success( - .failure(GraphQLResponseError.error([GraphQLError]()))) - return event + + return .failure(GraphQLResponseError.error([GraphQLError]())) } let elements = [Comment4(content: "content")] From 7e5883a8f8b14a5ac058e4e0e3857d6ea9b952e0 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 29 May 2024 16:50:39 +0000 Subject: [PATCH 21/23] feat(amplify): make GraphQLOperationType extends from String (#3719) --- Amplify/Categories/API/Request/GraphQLOperationType.swift | 2 +- .../GraphQLDocument/SingleDirectiveGraphQLDocument.swift | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/Amplify/Categories/API/Request/GraphQLOperationType.swift b/Amplify/Categories/API/Request/GraphQLOperationType.swift index 38160a72f2..7e9e2735ed 100644 --- a/Amplify/Categories/API/Request/GraphQLOperationType.swift +++ b/Amplify/Categories/API/Request/GraphQLOperationType.swift @@ -6,7 +6,7 @@ // /// The type of a GraphQL operation -public enum GraphQLOperationType { +public enum GraphQLOperationType: String { /// A GraphQL Query operation case query diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/SingleDirectiveGraphQLDocument.swift b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/SingleDirectiveGraphQLDocument.swift index ea5c29d342..a219bf3ad7 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/SingleDirectiveGraphQLDocument.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Model/GraphQLDocument/SingleDirectiveGraphQLDocument.swift @@ -8,12 +8,6 @@ import Amplify import Foundation -public enum GraphQLOperationType: String { - case mutation - case query - case subscription -} - public typealias GraphQLParameterName = String /// Represents a single directive GraphQL document. Concrete types that conform to this protocol must From 82f9ec5cd882b8619f7d5aae87e0ef7eb32d45b6 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 29 May 2024 17:12:53 +0000 Subject: [PATCH 22/23] refactor(datastore): new enum to represent inferred and designated authType (#3694) * refactor(datastore): new enum to represent inferred and designated auth type * resolve failed multi auth integ tests * resolve comments --- .../xcschemes/AWSPluginsSDKCore.xcscheme | 71 ------------------- .../Operation/RetryableGraphQLOperation.swift | 8 +-- .../Auth/AWSAuthModeStrategy.swift | 51 ++++++------- .../Auth/AmplifyAuthorizationType.swift | 29 ++++++++ .../Auth/AuthModeStrategyTests.swift | 51 ++++++------- .../InitialSync/InitialSyncOperation.swift | 43 +++++------ .../SyncMutationToCloudOperation.swift | 4 +- ...omingAsyncSubscriptionEventPublisher.swift | 38 +++++----- .../SyncMutationToCloudOperationTests.swift | 42 ++++++++--- .../AWSAuthorizationTypeIteratorTests.swift | 11 +-- .../API/RetryableGraphQLOperationTests.swift | 16 +++-- 11 files changed, 178 insertions(+), 186 deletions(-) delete mode 100644 .swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Auth/AmplifyAuthorizationType.swift diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme deleted file mode 100644 index f18064ad33..0000000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/AWSPluginsSDKCore.xcscheme +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift index f40229d9f9..d746ba4905 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -16,10 +16,10 @@ public final class RetryableGraphQLOperation { private let nondeterminsticOperation: NondeterminsticOperation.Success> public init( - requestStream: AnyPublisher<() async throws -> GraphQLTask.Success, Never> + requestStream: AsyncStream<() async throws -> GraphQLTask.Success> ) { self.nondeterminsticOperation = NondeterminsticOperation( - operationStream: requestStream, + operations: requestStream, shouldTryNextOnError: Self.onError(_:) ) } @@ -80,9 +80,9 @@ public final class RetryableGraphQLSubscriptionOperation { private let nondeterminsticOperation: NondeterminsticOperation> public init( - requestStream: AnyPublisher<() async throws -> AmplifyAsyncThrowingSequence, Never> + requestStream: AsyncStream<() async throws -> AmplifyAsyncThrowingSequence> ) { - self.nondeterminsticOperation = NondeterminsticOperation(operationStream: requestStream) + self.nondeterminsticOperation = NondeterminsticOperation(operations: requestStream) } deinit { diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift index 91d38c2855..d53920f158 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthModeStrategy.swift @@ -65,19 +65,23 @@ public protocol AuthorizationTypeIterator { } /// AuthorizationTypeIterator for values of type `AWSAuthorizationType` -public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator { - public typealias AuthorizationType = AWSAuthorizationType +public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator, Sequence, IteratorProtocol { + public typealias AuthorizationType = AmplifyAuthorizationType - private var values: IndexingIterator<[AWSAuthorizationType]> + private var values: IndexingIterator<[AmplifyAuthorizationType]> private var _count: Int private var _position: Int - public init(withValues values: [AWSAuthorizationType]) { + public init(withValues values: [AmplifyAuthorizationType]) { self.values = values.makeIterator() self._count = values.count self._position = 0 } + public init(withValues values: [AmplifyAuthorizationType], valuesOnEmpty defaults: [AmplifyAuthorizationType]) { + self.init(withValues: values.isEmpty ? defaults : values) + } + public var count: Int { _count } @@ -86,7 +90,7 @@ public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator { _position < _count } - public mutating func next() -> AWSAuthorizationType? { + public mutating func next() -> AmplifyAuthorizationType? { if let value = values.next() { _position += 1 return value @@ -96,19 +100,6 @@ public struct AWSAuthorizationTypeIterator: AuthorizationTypeIterator { } } -extension AuthorizationTypeIterator { - public func publisher() -> AnyPublisher { - var it = self - return Deferred { - var authTypes = [AuthorizationType]() - while let authType = it.next() { - authTypes.append(authType) - } - return Publishers.MergeMany(authTypes.map { Just($0) }) - }.eraseToAnyPublisher() - } -} - // MARK: - AWSDefaultAuthModeStrategy /// AWS default auth mode strategy. @@ -121,12 +112,12 @@ public class AWSDefaultAuthModeStrategy: AuthModeStrategy { public func authTypesFor(schema: ModelSchema, operation: ModelOperation) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: []) + return AWSAuthorizationTypeIterator(withValues: [.inferred]) } public func authTypesFor(schema: ModelSchema, operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: []) + return AWSAuthorizationTypeIterator(withValues: [.inferred]) } } @@ -141,20 +132,18 @@ public class AWSMultiAuthModeStrategy: AuthModeStrategy { required public init() {} private static func defaultAuthTypeFor(authStrategy: AuthStrategy) -> AWSAuthorizationType { - var defaultAuthType: AWSAuthorizationType switch authStrategy { case .owner: - defaultAuthType = .amazonCognitoUserPools + return .amazonCognitoUserPools case .groups: - defaultAuthType = .amazonCognitoUserPools + return .amazonCognitoUserPools case .private: - defaultAuthType = .amazonCognitoUserPools + return .amazonCognitoUserPools case .public: - defaultAuthType = .apiKey + return .apiKey case .custom: - defaultAuthType = .function + return .function } - return defaultAuthType } /// Given an auth rule, returns the corresponding AWSAuthorizationType @@ -248,10 +237,12 @@ public class AWSMultiAuthModeStrategy: AuthModeStrategy { return rule.allow == .public || rule.allow == .custom } } - let applicableAuthTypes = sortedRules.map { + + let applicableAuthTypes: [AmplifyAuthorizationType] = sortedRules.map { AWSMultiAuthModeStrategy.authTypeFor(authRule: $0) - } - return AWSAuthorizationTypeIterator(withValues: applicableAuthTypes) + }.map { .designated($0) } + + return AWSAuthorizationTypeIterator(withValues: applicableAuthTypes, valuesOnEmpty: [.inferred]) } } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/AmplifyAuthorizationType.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AmplifyAuthorizationType.swift new file mode 100644 index 0000000000..18a90e3106 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AmplifyAuthorizationType.swift @@ -0,0 +1,29 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation + +/// - Warning: Although this has `public` access, it is intended for internal use and should not be used directly +/// by host applications. The behavior of this may change without warning. +public enum AmplifyAuthorizationType { + + /// Determine the authorization method based on the amplifyconfiguration. + case inferred + + /// Specify the authentication method. + case designated(AWSAuthorizationType) + + public var awsAuthType: AWSAuthorizationType? { + switch self { + case .inferred: return nil + case .designated(let authType): return authType + } + } +} + +extension AmplifyAuthorizationType: Equatable { } diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AuthModeStrategyTests.swift b/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AuthModeStrategyTests.swift index 304aac6add..8674e962cb 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AuthModeStrategyTests.swift +++ b/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AuthModeStrategyTests.swift @@ -17,8 +17,9 @@ class AuthModeStrategyTests: XCTestCase { // Then: an empty iterator is returned func testDefaultAuthModeShouldReturnAnEmptyIterator() { let authMode = AWSDefaultAuthModeStrategy() - let authTypesIterator = authMode.authTypesFor(schema: AnyModelTester.schema, operation: .create) - XCTAssertEqual(authTypesIterator.count, 0) + var authTypesIterator = authMode.authTypesFor(schema: AnyModelTester.schema, operation: .create) + XCTAssertEqual(authTypesIterator.count, 1) + XCTAssertEqual(authTypesIterator.next(), .inferred) } // Given: multi-auth strategy and a model schema @@ -28,8 +29,8 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelWithOwnerAndPublicAuth.schema, operation: .create) XCTAssertEqual(authTypesIterator.count, 2) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } // Given: multi-auth strategy and a model schema without auth provider @@ -39,8 +40,8 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelNoProvider.schema, operation: .read) XCTAssertEqual(authTypesIterator.count, 2) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } // Given: multi-auth strategy and a model schema with 4 auth rules @@ -50,10 +51,10 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelAllStrategies.schema, operation: .read) XCTAssertEqual(authTypesIterator.count, 4) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .awsIAM) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .awsIAM) } // Given: multi-auth strategy and a model schema multiple public rules @@ -63,10 +64,10 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelWithMultiplePublicRules.schema, operation: .read) XCTAssertEqual(authTypesIterator.count, 4) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .openIDConnect) - XCTAssertEqual(authTypesIterator.next(), .awsIAM) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .openIDConnect) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .awsIAM) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } // Given: multi-auth strategy and a model schema @@ -77,8 +78,8 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelAllStrategies.schema, operation: .create) XCTAssertEqual(authTypesIterator.count, 2) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) } // Given: multi-auth strategy a model schema @@ -92,7 +93,7 @@ class AuthModeStrategyTests: XCTestCase { var authTypesIterator = await authMode.authTypesFor(schema: ModelWithOwnerAndPublicAuth.schema, operation: .create) XCTAssertEqual(authTypesIterator.count, 1) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } // Given: multi-auth model schema with a custom strategy @@ -103,9 +104,9 @@ class AuthModeStrategyTests: XCTestCase { var authTypesIterator = await authMode.authTypesFor(schema: ModelWithCustomStrategy.schema, operation: .create) XCTAssertEqual(authTypesIterator.count, 3) - XCTAssertEqual(authTypesIterator.next(), .function) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .awsIAM) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .function) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .awsIAM) } // Given: multi-auth model schema with a custom strategy @@ -119,8 +120,8 @@ class AuthModeStrategyTests: XCTestCase { var authTypesIterator = await authMode.authTypesFor(schema: ModelWithCustomStrategy.schema, operation: .create) XCTAssertEqual(authTypesIterator.count, 2) - XCTAssertEqual(authTypesIterator.next(), .function) - XCTAssertEqual(authTypesIterator.next(), .awsIAM) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .function) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .awsIAM) } // Given: multi-auth strategy and a model schema without auth provider @@ -130,8 +131,8 @@ class AuthModeStrategyTests: XCTestCase { let authMode = AWSMultiAuthModeStrategy() var authTypesIterator = await authMode.authTypesFor(schema: ModelNoProvider.schema, operations: [.read, .create]) XCTAssertEqual(authTypesIterator.count, 2) - XCTAssertEqual(authTypesIterator.next(), .amazonCognitoUserPools) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .amazonCognitoUserPools) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } // Given: multi-auth strategy and a model schema with auth provider @@ -143,7 +144,7 @@ class AuthModeStrategyTests: XCTestCase { authMode.authDelegate = delegate var authTypesIterator = await authMode.authTypesFor(schema: ModelNoProvider.schema, operations: [.read, .create]) XCTAssertEqual(authTypesIterator.count, 1) - XCTAssertEqual(authTypesIterator.next(), .apiKey) + XCTAssertEqual(authTypesIterator.next()?.awsAuthType, .apiKey) } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift index 0365b91862..b37a860eae 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/InitialSync/InitialSyncOperation.swift @@ -170,26 +170,29 @@ final class InitialSyncOperation: AsynchronousOperation { let minSyncPageSize = Int(min(syncMaxRecords - recordsReceived, syncPageSize)) let limit = minSyncPageSize < 0 ? Int(syncPageSize) : minSyncPageSize let authTypes = await authModeStrategy.authTypesFor(schema: modelSchema, operation: .read) - .publisher() - .map { Optional.some($0) } // map to optional to have nil as element - .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided - .map { authType in { [weak self] in - guard let self, let api = self.api else { - throw APIError.operationError("Operation cancelled", "") - } - - return try await api.query(request: GraphQLRequest.syncQuery( - modelSchema: self.modelSchema, - where: self.syncPredicate, - limit: limit, - nextToken: nextToken, - lastSync: lastSyncTime, - authType: authType - )) - }} - .eraseToAnyPublisher() - - switch await RetryableGraphQLOperation(requestStream: authTypes).run() { + let queryRequestsStream = AsyncStream { continuation in + for authType in authTypes { + continuation.yield({ [weak self] in + guard let self, let api = self.api else { + throw APIError.operationError( + "The initial synchronization process can no longer be accessed or referred to", + "The initial synchronization process may be cancelled or terminated" + ) + } + + return try await api.query(request: GraphQLRequest.syncQuery( + modelSchema: self.modelSchema, + where: self.syncPredicate, + limit: limit, + nextToken: nextToken, + lastSync: lastSyncTime, + authType: authType.awsAuthType + )) + }) + } + continuation.finish() + } + switch await RetryableGraphQLOperation(requestStream: queryRequestsStream).run() { case .success(let graphQLResult): await handleQueryResults(lastSyncTime: lastSyncTime, graphQLResult: graphQLResult) case .failure(let apiError): diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift index 4321411644..40f9c04f06 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift @@ -58,7 +58,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { override func main() { log.verbose(#function) - sendMutationToCloud(withAuthType: authTypesIterator?.next()) + sendMutationToCloud(withAuthType: authTypesIterator?.next()?.awsAuthType) } override func cancel() { @@ -253,7 +253,7 @@ class SyncMutationToCloudOperation: AsynchronousOperation { resolveReachabilityPublisher(request: request) if let pluginOptions = request.options?.pluginOptions as? AWSAPIPluginDataStoreOptions, pluginOptions.authType != nil, let nextAuthType = authTypesIterator?.next() { - scheduleRetry(advice: advice, withAuthType: nextAuthType) + scheduleRetry(advice: advice, withAuthType: nextAuthType.awsAuthType) } else { scheduleRetry(advice: advice) } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift index a342d12c00..e9a89800dd 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventPublisher.swift @@ -124,24 +124,26 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { ) return RetryableGraphQLSubscriptionOperation( - requestStream: authTypeProvider.publisher() - .map { Optional.some($0) } // map to optional to have nil as element - .replaceEmpty(with: nil) // use a nil element to trigger default auth if no auth provided - .map { authType in { [weak self] in - guard let self else { - throw APIError.operationError("GraphQL subscription cancelled", "") - } - - return api.subscribe(request: await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( - for: modelSchema, - subscriptionType: subscriptionType.subscriptionType, - api: api, - auth: auth, - authType: authType, - awsAuthService: self.awsAuthService - )) - }} - .eraseToAnyPublisher() + requestStream: AsyncStream { continuation in + for authType in authTypeProvider { + continuation.yield({ [weak self] in + guard let self else { + throw APIError.operationError("GraphQL subscription cancelled", "") + } + + return api.subscribe(request: await IncomingAsyncSubscriptionEventPublisher.makeAPIRequest( + for: modelSchema, + subscriptionType: subscriptionType.subscriptionType, + api: api, + auth: auth, + authType: authType.awsAuthType, + awsAuthService: self.awsAuthService + )) + }) + } + continuation.finish() + } + ) } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift index d594fe65e6..dbe8220e59 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift @@ -308,6 +308,9 @@ class SyncMutationToCloudOperationTests: XCTestCase { } func testGetRetryAdvice_OperationErrorAuthErrorWithSingleAuth_RetryFalse() async throws { + let expectation = expectation(description: "operation completed") + var numberOfTimesEntered = 0 + var error: APIError? let operation = await SyncMutationToCloudOperation( mutationEvent: try createMutationEvent(), getLatestSyncMetadata: { nil }, @@ -315,13 +318,30 @@ class SyncMutationToCloudOperationTests: XCTestCase { authModeStrategy: AWSDefaultAuthModeStrategy(), networkReachabilityPublisher: publisher, currentAttemptNumber: 1, - completion: { _ in } + completion: { result in + XCTAssertEqual(numberOfTimesEntered, 1) + switch result { + case .failure(let apiError): + error = apiError + default: + XCTFail("Wrong result") + } + expectation.fulfill() + } ) - - let authError = AuthError.notAuthorized("", "", nil) - let error = APIError.operationError("", "", authError) - let advice = operation.getRetryAdviceIfRetryable(error: error) - XCTAssertFalse(advice.shouldRetry) + + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } + let authError = AuthError.notAuthorized("", "", nil) + return .failure(.unknown("", "", APIError.operationError("", "", authError))) + } + + mockAPIPlugin.responders[.mutateRequestResponse] = responder + + let queue = OperationQueue() + queue.addOperation(operation) + await fulfillment(of: [expectation]) + XCTAssertEqual(false, operation.getRetryAdviceIfRetryable(error: error!).shouldRetry) } func testGetRetryAdvice_OperationErrorAuthErrorSessionExpired_RetryTrue() async throws { @@ -371,12 +391,18 @@ public class MockMultiAuthModeStrategy: AuthModeStrategy { public func authTypesFor(schema: ModelSchema, operation: ModelOperation) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) + return AWSAuthorizationTypeIterator(withValues: [ + .designated(.amazonCognitoUserPools), + .designated(.apiKey) + ]) } public func authTypesFor(schema: ModelSchema, operations: [ModelOperation]) -> AWSAuthorizationTypeIterator { - return AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) + return AWSAuthorizationTypeIterator(withValues: [ + .designated(.amazonCognitoUserPools), + .designated(.apiKey) + ]) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/AWSAuthorizationTypeIteratorTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/AWSAuthorizationTypeIteratorTests.swift index 15472e2ac6..f07db70e2b 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/AWSAuthorizationTypeIteratorTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/Support/AWSAuthorizationTypeIteratorTests.swift @@ -18,8 +18,8 @@ class AWSAuthorizationTypeIteratorTests: XCTestCase { } func testOneElementIterator_hasNextValue_once() throws { - var iterator = AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools]) - + var iterator = AWSAuthorizationTypeIterator(withValues: [.designated(.amazonCognitoUserPools)]) + XCTAssertTrue(iterator.hasNext) XCTAssertNotNil(iterator.next()) @@ -27,8 +27,11 @@ class AWSAuthorizationTypeIteratorTests: XCTestCase { } func testTwoElementsIterator_hasNextValue_twice() throws { - var iterator = AWSAuthorizationTypeIterator(withValues: [.amazonCognitoUserPools, .apiKey]) - + var iterator = AWSAuthorizationTypeIterator(withValues: [ + .designated(.amazonCognitoUserPools), + .designated(.apiKey) + ]) + XCTAssertTrue(iterator.hasNext) XCTAssertNotNil(iterator.next()) diff --git a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift index 0f810d9838..a9e374f9e8 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -30,8 +30,12 @@ class RetryableGraphQLOperationTests: XCTestCase { return .success("operation 2") } - let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() - let result = await RetryableGraphQLOperation(requestStream: publisher).run() + let operationStream = AsyncStream { continuation in + continuation.yield(operation1) + continuation.yield(operation2) + continuation.finish() + } + let result = await RetryableGraphQLOperation(requestStream: operationStream).run() if case .success(.success(let string)) = result { XCTAssertEqual(string, "operation 2") } else { @@ -57,8 +61,12 @@ class RetryableGraphQLOperationTests: XCTestCase { return .success("operation 2") } - let publisher = Publishers.MergeMany([operation1, operation2].map { Just($0) }).eraseToAnyPublisher() - let result = await RetryableGraphQLOperation(requestStream: publisher).run() + let operationStream = AsyncStream { continuation in + continuation.yield(operation1) + continuation.yield(operation2) + continuation.finish() + } + let result = await RetryableGraphQLOperation(requestStream: operationStream).run() if case .failure(.unknown(let description, _, _)) = result { XCTAssertEqual(description, "~Unknown~") } else { From c0ba0ae4234ccd29d41d7d966d3d102476a44485 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Mon, 3 Jun 2024 18:30:04 +0000 Subject: [PATCH 23/23] fix(datastore): use error description to produce clearer error info (#3733) --- .../IncomingAsyncSubscriptionEventToAnyModelMapper.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventToAnyModelMapper.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventToAnyModelMapper.swift index 54af20d333..2e1aef7248 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventToAnyModelMapper.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/IncomingAsyncSubscriptionEventToAnyModelMapper.swift @@ -83,7 +83,7 @@ final class IncomingAsyncSubscriptionEventToAnyModelMapper: Subscriber, AmplifyC case .success(let mutationSync): modelsFromSubscription.send(.payload(mutationSync)) case .failure(let failure): - log.error(error: failure) + log.error(failure.errorDescription) } }