diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme index 6970be959c..5fda4b76eb 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme @@ -482,6 +482,48 @@ ReferencedContainer = "container:"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + { + /// 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() + let (asyncStream, continuation) = AsyncStream.makeStream(of: Operation.self) + operationStream.sink { _ in + continuation.finish() + } receiveValue: { + continuation.yield($0) + }.store(in: &cancellables) + + self.init( + operations: asyncStream, + 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 ed2a6e2753..d746ba4905 100644 --- a/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift +++ b/Amplify/Categories/API/Operation/RetryableGraphQLOperation.swift @@ -6,183 +6,150 @@ // 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 - - /// GraphQLOperation concrete type - associatedtype OperationType: AnyGraphQLOperation - - typealias RequestFactory = () async -> GraphQLRequest - typealias OperationFactory = (GraphQLRequest, @escaping OperationResultListener) -> OperationType - typealias OperationResultListener = OperationType.ResultListener - - /// Operation unique identifier - var id: UUID { get } - - /// Number of attempts (min 1) - var attempts: Int { get set } - - /// Underlying GraphQL operation instantiated by `operationFactory` - var underlyingOperation: AtomicValue { get set } - /// Maximum number of allowed retries - var maxRetries: Int { get } - - /// GraphQLRequest factory, invoked to create a new operation - var requestFactory: RequestFactory { get } - - /// GraphQL operation factory, invoked with a newly created GraphQL request - /// and a wrapped result listener. - var operationFactory: OperationFactory { get } +// MARK: - RetryableGraphQLOperation +public final class RetryableGraphQLOperation { + public typealias Payload = Payload - var resultListener: OperationResultListener { get } + private let nondeterminsticOperation: NondeterminsticOperation.Success> - init(requestFactory: @escaping RequestFactory, - maxRetries: Int, - resultListener: @escaping OperationResultListener, - _ operationFactory: @escaping OperationFactory) + public init( + requestStream: AsyncStream<() async throws -> GraphQLTask.Success> + ) { + self.nondeterminsticOperation = NondeterminsticOperation( + operations: requestStream, + shouldTryNextOnError: Self.onError(_:) + ) + } - func start(request: GraphQLRequest) + deinit { + cancel() + } - func shouldRetry(error: APIError?) -> Bool -} + static func onError(_ error: Error) -> Bool { + guard let error = error as? APIError, + let authError = error.underlyingError as? AuthError + else { + return false + } -extension RetryableGraphQLOperationBehavior { - public static var log: Logger { - Amplify.Logging.logger(forCategory: CategoryType.api.displayName, forNamespace: String(describing: self)) - } - public var log: Logger { - Self.log + switch authError { + case .notAuthorized: return true + default: return false + } } -} -// 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 - } - - if case let .failure(error) = result { - self.log.debug("\(error)") - self.log.debug("[\(self.id)] - Failed") + 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() + } - if case .success = result { - self.log.debug("[Operation \(self.id)] - Success") + public func run() async -> Result.Success, APIError> { + do { + let result = try await nondeterminsticOperation.run() + return .success(result) + } catch { + if let apiError = error as? APIError { + return .failure(apiError) + } else { + return .failure(.operationError("Failed to execute GraphQL operation", "", error)) } - self.resultListener(result) } - underlyingOperation.set(operationFactory(request, wrappedResultListener)) } + + public func cancel() { + nondeterminsticOperation.cancel() + } + } -// MARK: - RetryableGraphQLOperation -public final class RetryableGraphQLOperation: Operation, RetryableGraphQLOperationBehavior { +public final class RetryableGraphQLSubscriptionOperation { + 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 typealias SubscriptionEvents = GraphQLSubscriptionEvent + private var task: Task? + private let nondeterminsticOperation: NondeterminsticOperation> + + public init( + requestStream: AsyncStream<() async throws -> AmplifyAsyncThrowingSequence> + ) { + self.nondeterminsticOperation = NondeterminsticOperation(operations: requestStream) } - public override func main() { - Task { - start(request: await requestFactory()) - } + deinit { + cancel() } - override public func cancel() { - self.underlyingOperation.get()?.cancel() + public func subscribe() -> AnyPublisher { + let subject = PassthroughSubject() + self.task = Task { await self.trySubscribe(subject) } + return subject.eraseToAnyPublisher() } - public func shouldRetry(error: APIError?) -> Bool { - guard case let .operationError(_, _, underlyingError) = error, - let authError = underlyingError as? AuthError else { - return false - } + private func trySubscribe(_ subject: PassthroughSubject) async { + var apiError: APIError? + do { + try Task.checkCancellation() + let sequence = try await self.nondeterminsticOperation.run() + defer { sequence.cancel() } + for try await event in sequence { + try Task.checkCancellation() + 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)") + } - switch authError { - case .signedOut, .notAuthorized: - return attempts < maxRetries - default: - return false + if apiError != nil { + subject.send(completion: .failure(apiError!)) + } else { + subject.send(completion: .finished) } } -} - -// 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 func cancel() { + self.task?.cancel() + self.nondeterminsticOperation.cancel() } - 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/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/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift index 3bb58d9f64..5ff7d388eb 100644 --- a/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift +++ b/Amplify/Core/Support/AmplifyAsyncThrowingSequence.swift @@ -47,4 +47,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+Configure.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin+Configure.swift index ec27a34b41..e63ed08dcb 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 InternalAmplifyCredentials 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 e7ea03dc09..dcdcd7095a 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AWSAPIPlugin.swift @@ -7,9 +7,10 @@ import Amplify import AWSPluginsCore +import InternalAmplifyCredentials import Foundation -final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQLBehaviorExtended, AWSAPIAuthInformation { +final public class AWSAPIPlugin: NSObject, APICategoryPlugin, AWSAPIAuthInformation { /// Used for the default GraphQL API represented by the `data` category in `amplify_outputs.json` /// This constant is not used for APIs present in `amplifyconfiguration.json` since they always have names. public static let defaultGraphQLAPI = "defaultGraphQLAPI" @@ -25,7 +26,7 @@ final public class AWSAPIPlugin: NSObject, APICategoryPlugin, APICategoryGraphQL /// 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..07005a7034 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 InternalAmplifyCredentials // 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..99191e40dc 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 InternalAmplifyCredentials /// 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..225d597c04 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 InternalAmplifyCredentials 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..9137c018b5 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 InternalAmplifyCredentials 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..9ed5783c28 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 InternalAmplifyCredentials 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..5b354abb1d 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 InternalAmplifyCredentials 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..cd023676c7 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 InternalAmplifyCredentials 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 44e2cf378d..96347697d4 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 InternalAmplifyCredentials 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..57a3708e1e 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 InternalAmplifyCredentials @_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..c151f9c50c 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 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 2974af9f4d..ac458b9992 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 InternalAmplifyCredentials // swiftlint:disable:next type_name class AWSAPICategoryPluginInterceptorBehaviorTests: AWSAPICategoryPluginTestBase { 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/Configuration/AWSAPICategoryPluginConfigurationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift index a2f7503f01..b71e5491a8 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 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 a48d5fa96e..03520086ef 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 InternalAmplifyCredentials @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/Core/AppSyncListProviderPaginationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift index 57843d9c92..410077d90d 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderPaginationTests.swift @@ -32,11 +32,11 @@ extension AppSyncListProviderTests { } XCTAssertFalse(provider.hasNextPage()) } - + func testNotLoadedStateHasNextPageFalse() { let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], - apiName: "apiName", + apiName: "apiName", authMode: nil) let provider = AppSyncListProvider(metadata: modelMetadata) guard case .notLoaded = provider.loadedState else { @@ -53,8 +53,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") @@ -85,10 +84,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") @@ -97,7 +94,7 @@ extension AppSyncListProviderTests { XCTFail("Should be loaded") return } - + do { _ = try await provider.getNextPage() XCTFail("Should have failed") @@ -111,20 +108,19 @@ 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")] let provider = AppSyncListProvider(elements: elements, nextToken: "nextToken", apiName: "apiName", authMode: .amazonCognitoUserPools) guard case .loaded = provider.loadedState else { XCTFail("Should be loaded") return } - + do { _ = try await provider.getNextPage() XCTFail("Should have failed") @@ -138,11 +134,11 @@ extension AppSyncListProviderTests { XCTFail("Unexpected error type \(error)") } } - + func testNotLoadedStateGetNextPageFailure() async { let modelMetadata = AppSyncListDecoder.Metadata(appSyncAssociatedIdentifiers: ["postId"], appSyncAssociatedFields: ["post"], - apiName: "apiName", + apiName: "apiName", authMode: nil) let provider = AppSyncListProvider(metadata: modelMetadata) guard case .notLoaded = provider.loadedState else { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift index 1e02c9beaf..e8c7abd661 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Core/AppSyncListProviderTests.swift @@ -142,8 +142,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"], @@ -178,10 +177,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"], @@ -228,8 +225,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"], @@ -264,10 +260,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"], @@ -300,11 +294,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"], @@ -353,8 +344,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/Interceptor/AuthTokenURLRequestInterceptorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift index 5f25a0dc9a..74e1041f82 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 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 2ba9f97779..51c1feea3e 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 InternalAmplifyCredentials 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/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 6f76a6aa72..e6917c412c 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift @@ -49,7 +49,7 @@ class AWSRESTOperationTests: OperationTestBase { } XCTAssertNotNil(operation.request) - await fulfillment(of: [listenerWasInvoked], timeout: 1) + await fulfillment(of: [listenerWasInvoked], timeout: 3) } func testGetFailsWithBadAPIName() async throws { @@ -76,7 +76,7 @@ class AWSRESTOperationTests: OperationTestBase { /// - 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 +92,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 +117,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 cce395ccd3..9777f3b291 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/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index 4581f1d799..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 AWSPluginsCore +@_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 81c0d9a77e..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 AWSPluginsCore +@_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 2a8864e5a1..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 AWSPluginsCore +@_spi(InternalHttpEngineProxy) @_spi(InternalAmplifyPluginExtension) import InternalAmplifyCredentials import ClientRuntime import Foundation 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..d53920f158 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 @@ -64,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 } @@ -85,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 @@ -107,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]) } } @@ -127,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 @@ -234,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/AWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthService.swift index d0c279314c..b15b4b7d6c 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/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/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AWSAuthServiceBehavior.swift index 6c302cc928..1c23190588 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/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/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/AWSPluginsCore/Auth/Provider/AuthAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthAWSCredentialsProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthAWSCredentialsProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoIdentityProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoIdentityProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoIdentityProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoIdentityProvider.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoTokensProvider.swift b/AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoTokensProvider.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthCognitoTokensProvider.swift rename to AmplifyPlugins/Core/AWSPluginsCore/Auth/AuthCognitoTokensProvider.swift 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 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/Core/AWSPluginsTestCommon/MockAWSAuthService.swift b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSAuthService.swift index d0d87929a9..7f11328ad1 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 InternalAmplifyCredentials -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..abeb670efb 100644 --- a/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift +++ b/AmplifyPlugins/Core/AWSPluginsTestCommon/MockAWSSignatureV4Signer.swift @@ -8,6 +8,7 @@ import AWSPluginsCore import ClientRuntime import AWSClientRuntime +import InternalAmplifyCredentials import Foundation class MockAWSSignatureV4Signer: AWSSignatureV4Signer { diff --git a/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthCredentialsProviderBehavior.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthCredentialsProviderBehavior.swift new file mode 100644 index 0000000000..6d6cfe9277 --- /dev/null +++ b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthCredentialsProviderBehavior.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import AWSClientRuntime +import AWSPluginsCore + +public protocol AWSAuthCredentialsProviderBehavior: AWSAuthServiceBehavior { + func getCredentialsProvider() -> CredentialsProviding +} + + diff --git a/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthService+CredentialsProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthService+CredentialsProvider.swift new file mode 100644 index 0000000000..ec8babe3d5 --- /dev/null +++ b/AmplifyPlugins/Core/AmplifyCredentials/AWSAuthService+CredentialsProvider.swift @@ -0,0 +1,17 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import AWSClientRuntime +import AWSPluginsCore + +extension AWSAuthService: AWSAuthCredentialsProviderBehavior { + public func getCredentialsProvider() -> AWSClientRuntime.CredentialsProviding { + return AmplifyAWSCredentialsProvider() + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/AWSPluginExtension.swift b/AmplifyPlugins/Core/AmplifyCredentials/AWSPluginExtension.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/AWSPluginExtension.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AWSPluginExtension.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSCredentialsProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSCredentialsProvider.swift similarity index 95% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSCredentialsProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSCredentialsProvider.swift index 1959aa58b5..63d1197a6d 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSCredentialsProvider.swift +++ b/AmplifyPlugins/Core/AmplifyCredentials/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/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSSignatureV4Signer.swift b/AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSSignatureV4Signer.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AmplifyAWSSignatureV4Signer.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSSignatureV4Signer.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthTokenProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/AuthTokenProvider.swift similarity index 96% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthTokenProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/AuthTokenProvider.swift index 4c79d1d77b..39f1bd43f2 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/AuthTokenProvider.swift +++ b/AmplifyPlugins/Core/AmplifyCredentials/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/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngineError.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/FoundationClientEngineError.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/PluginClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/PluginClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/PluginClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/SdkHttpRequest+updatingUserAgent.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSettingClientEngine.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSettingClientEngine.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSettingClientEngine.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSettingClientEngine.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSuffixAppender.swift b/AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSuffixAppender.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/UserAgentSuffixAppender.swift rename to AmplifyPlugins/Core/AmplifyCredentials/CustomHttpClientEngine/UserAgentSuffixAppender.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/IAMCredentialProvider.swift b/AmplifyPlugins/Core/AmplifyCredentials/IAMCredentialProvider.swift similarity index 78% rename from AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/IAMCredentialProvider.swift rename to AmplifyPlugins/Core/AmplifyCredentials/IAMCredentialProvider.swift index 3ceee7167e..1265a9130f 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/Auth/Provider/IAMCredentialProvider.swift +++ b/AmplifyPlugins/Core/AmplifyCredentials/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/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy b/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..74f8af8564 --- /dev/null +++ b/AmplifyPlugins/Core/AmplifyCredentials/Resources/PrivacyInfo.xcprivacy @@ -0,0 +1,8 @@ + + + + + NSPrivacyAccessedAPITypes + + + diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift b/AmplifyPlugins/Core/AmplifyCredentials/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift rename to AmplifyPlugins/Core/AmplifyCredentials/ServiceConfiguration/AmplifyAWSServiceConfiguration+Platform.swift diff --git a/AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan b/AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan new file mode 100644 index 0000000000..8a0c4734d0 --- /dev/null +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/AWSPluginsSDKCore.xctestplan @@ -0,0 +1,24 @@ +{ + "configurations" : [ + { + "id" : "9D7C41C1-F847-4136-AB74-D1E17831BCDD", + "name" : "Configuration 1", + "options" : { + + } + } + ], + "defaultOptions" : { + + }, + "testTargets" : [ + { + "target" : { + "containerPath" : "container:", + "identifier" : "InternalAmplifyCredentialsTests", + "name" : "InternalAmplifyCredentialsTests" + } + } + ], + "version" : 1 +} diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift index 1dc1c19582..abca5ad99c 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Auth/AWSAuthServiceTests.swift +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/Auth/AWSAuthServiceTests.swift @@ -9,6 +9,7 @@ import XCTest @testable import Amplify @testable import AWSPluginsCore +@testable import InternalAmplifyCredentials import AWSClientRuntime class AWSAuthServiceTests: XCTestCase { diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift similarity index 99% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift index f395f6ef18..a6b2d3a800 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSettingClientEngineTests.swift +++ b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSettingClientEngineTests.swift @@ -8,7 +8,7 @@ @_spi(InternalAmplifyPluginExtension) @_spi(PluginHTTPClientEngine) @_spi(InternalHttpEngineProxy) -import AWSPluginsCore +import InternalAmplifyCredentials import ClientRuntime import XCTest diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSuffixAppenderTests.swift b/AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSuffixAppenderTests.swift similarity index 98% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/Utils/UserAgentSuffixAppenderTests.swift rename to AmplifyPlugins/Core/AmplifyCredentialsTests/Utils/UserAgentSuffixAppenderTests.swift index 3b6b167a92..dece4394d4 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/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 AWSPluginsCore +@_spi(InternalAmplifyPluginExtension) @_spi(InternalHttpEngineProxy) import InternalAmplifyCredentials import ClientRuntime import XCTest 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..b37a860eae 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,47 @@ 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) + 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): + 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 +244,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 +296,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 d517b170af..e2840f7e47 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 } @@ -29,7 +29,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { /// A DispatchQueue for synchronizing state on the mutation queue private let mutationDispatchQueue = TaskQueue() - private weak var api: APICategoryGraphQLBehaviorExtended? + private weak var api: APICategoryGraphQLBehavior? private weak var reconciliationQueue: IncomingEventReconciliationQueue? private var subscription: Subscription? @@ -81,7 +81,7 @@ final class OutgoingMutationQueue: OutgoingMutationQueueBehavior { // MARK: - Public API - func startSyncingToCloud(api: APICategoryGraphQLBehaviorExtended, + func startSyncingToCloud(api: APICategoryGraphQLBehavior, mutationEventPublisher: MutationEventPublisher, reconciliationQueue: IncomingEventReconciliationQueue?) { log.verbose(#function) @@ -127,7 +127,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) @@ -223,7 +223,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, @@ -272,7 +272,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..40f9c04f06 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/MutationSync/OutgoingMutationQueue/SyncMutationToCloudOperation.swift @@ -17,23 +17,21 @@ 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 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 +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) { @@ -61,22 +58,19 @@ class SyncMutationToCloudOperation: AsynchronousOperation { override func main() { log.verbose(#function) - sendMutationToCloud(withAuthType: authTypesIterator?.next()) + sendMutationToCloud(withAuthType: authTypesIterator?.next()?.awsAuthType) } override func cancel() { log.verbose(#function) - lock.execute { - mutationOperation?.cancel() - mutationRetryNotifier?.cancel() - mutationRetryNotifier = nil - } + 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 @@ -209,58 +203,66 @@ 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?.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) + 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 } 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) } return } - finish(result: cloudResult) + 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 { @@ -277,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) @@ -319,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)") @@ -338,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?.removeResultListener() - mutationOperation = nil - } + 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..e9a89800dd 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,83 @@ 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( + 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() + } + + ) } func onCreateValueListenerHandler(event: Event) { @@ -183,9 +195,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 +208,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 +238,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 +266,7 @@ final class IncomingAsyncSubscriptionEventPublisher: AmplifyCancellable { func cancel() { consistencyQueue.sync { - genericCompletionListenerHandler(result: .successfulVoid) + genericCompletionListenerHandler(result: .finished) onCreateOperation?.cancel() onCreateOperation = nil @@ -287,30 +299,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/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) } } 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/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/Core/AWSDataStorePluginTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift index 869b62d24d..c4bbc384dc 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Core/AWSDataStorePluginTests.swift @@ -77,7 +77,8 @@ class AWSDataStorePluginTests: XCTestCase { } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") } - await fulfillment(of: [startExpectation], timeout: 1.0) } + await fulfillment(of: [startExpectation], timeout: 1) + } func testStorageEngineStartsOnQuery() async throws { let startExpectation = expectation(description: "Start Sync should be called with Query") @@ -111,22 +112,22 @@ class AWSDataStorePluginTests: XCTestCase { timeout: 1 ) } - + 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") var currCount = 0 let storageEngine = MockStorageEngineBehavior() - + storageEngine.responders[.stopSync] = StopSyncResponder { _ in stopExpectation.fulfill() } - + storageEngine.responders[.startSync] = StartSyncResponder { _ in currCount = self.expect(startExpectation, currCount, 1) } - + let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in return storageEngine } @@ -136,11 +137,11 @@ class AWSDataStorePluginTests: XCTestCase { dataStorePublisher: dataStorePublisher, validAPIPluginKey: "MockAPICategoryPlugin", validAuthPluginKey: "MockAuthCategoryPlugin") - + do { try plugin.configure(using: nil) XCTAssertNil(plugin.storageEngine) - + plugin.stop(completion: { _ in plugin.start(completion: { _ in }) }) @@ -149,21 +150,21 @@ class AWSDataStorePluginTests: XCTestCase { } await fulfillment(of: [startExpectation, stopExpectation], timeout: 1.0) } - + func testStorageEngineStartsOnPluginClearStart() async throws { let clearExpectation = expectation(description: "Clear should be called") let startExpectation = expectation(description: "Start Sync should be called") var currCount = 0 - + let storageEngine = MockStorageEngineBehavior() storageEngine.responders[.clear] = ClearResponder { _ in currCount = self.expect(clearExpectation, currCount, 1) } - + storageEngine.responders[.startSync] = StartSyncResponder { _ in currCount = self.expect(startExpectation, currCount, 2) } - + let storageEngineBehaviorFactory: StorageEngineBehaviorFactory = {_, _, _, _, _, _ throws in return storageEngine } @@ -173,11 +174,11 @@ class AWSDataStorePluginTests: XCTestCase { dataStorePublisher: dataStorePublisher, validAPIPluginKey: "MockAPICategoryPlugin", validAuthPluginKey: "MockAuthCategoryPlugin") - + do { try plugin.configure(using: nil) XCTAssertNil(plugin.storageEngine) - + plugin.clear(completion: { _ in plugin.start(completion: { _ in }) }) @@ -251,7 +252,8 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.storageEngine) XCTAssertNotNil(plugin.dataStorePublisher) }) - await fulfillment(of: [startExpectation, stopExpectation, startExpectationOnSecondStart], + await fulfillment( + of: [startExpectation, stopExpectation, startExpectationOnSecondStart], timeout: 1, enforceOrder: true ) @@ -326,7 +328,8 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) }) - await fulfillment(of: [startExpectation, clearExpectation, startExpectationOnSecondStart], + await fulfillment( + of: [startExpectation, clearExpectation, startExpectationOnSecondStart], timeout: 1, enforceOrder: true ) @@ -402,7 +405,12 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.storageEngine) XCTAssertNotNil(plugin.dataStorePublisher) }) - await fulfillment(of: [startExpectation, clearExpectation, startExpectationOnQuery, finishNotReceived], 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)") @@ -491,7 +499,12 @@ class AWSDataStorePluginTests: XCTestCase { modelSchema: mockModel.schema, mutationType: .create)) - await fulfillment(of: [startExpectation, clearExpectation, publisherReceivedValue, finishNotReceived], timeout: 1) + await fulfillment(of: [ + startExpectation, + clearExpectation, + finishNotReceived, + publisherReceivedValue + ], timeout: 1) sink.cancel() } catch { XCTFail("DataStore configuration should not fail with nil configuration. \(error)") @@ -563,9 +576,10 @@ class AWSDataStorePluginTests: XCTestCase { XCTAssertNotNil(plugin.dataStorePublisher) stopCompleted.fulfill() }) - await fulfillment(of: [stopCompleted], timeout: 1.0) + await fulfillment(of: [stopCompleted], timeout: 1.0) - await fulfillment(of: [startExpectation, stopExpectation], + await fulfillment( + of: [startExpectation, stopExpectation], timeout: 1, enforceOrder: true ) 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/InitialSync/InitialSyncOperationSyncExpressionTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/InitialSync/InitialSyncOperationSyncExpressionTests.swift index 298fcdb8b9..e118693987 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,13 +36,13 @@ 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, + let configuration = DataStoreConfiguration.custom(syncPageSize: 10, syncExpressions: [syncExpression], disableSubscriptions: { false }) #else - let configuration = DataStoreConfiguration.custom(syncPageSize: 10, + let configuration = DataStoreConfiguration.custom(syncPageSize: 10, syncExpressions: [syncExpression]) #endif return InitialSyncOperation( @@ -55,7 +55,7 @@ class InitialSyncOperationSyncExpressionTests: XCTestCase { } func testBaseQueryWithBasicSyncExpression() async 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() async 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() async 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 bea2d03aeb..07cdd95ecb 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() async { - 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() async { 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() async { - 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() async 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]) @@ -615,22 +603,20 @@ class InitialSyncOperationTests: XCTestCase { /// - Then: /// - The method completes with a failure result, error handler is called. func testQueriesAPIReturnSignedOutError() async throws { - let responder = QueryRequestListenerResponder> { _, listener in + 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)) let reconciliationQueue = MockReconciliationQueue() let expectErrorHandlerCalled = expectation(description: "Expect error handler called") - + #if os(watchOS) let configuration = DataStoreConfiguration.custom(errorHandler: { error in guard let dataStoreError = error as? DataStoreError, @@ -704,7 +690,12 @@ class InitialSyncOperationTests: XCTestCase { operation.main() - await fulfillment(of: [syncStartedReceived, syncCompletionReceived, finishedReceived, expectErrorHandlerCalled], timeout: 1) + await fulfillment(of: [ + expectErrorHandlerCalled, + syncStartedReceived, + syncCompletionReceived, + finishedReceived + ], timeout: 1) sink.cancel() } @@ -734,19 +725,17 @@ class InitialSyncOperationTests: XCTestCase { await fulfillment(of: [syncMetadataSaved], timeout: 1) 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 7e62bb0743..1e4a7ca208 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..63f1acd748 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..8802e832ff 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTests.swift @@ -24,50 +24,57 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { await tryOrFail { try setUpStorageAdapter() - try setUpDataStore(mutationQueue: OutgoingMutationQueue(storageAdapter: storageAdapter, - dataStoreConfiguration: .testDefault(), - authModeStrategy: AWSDefaultAuthModeStrategy())) + try setUpDataStore( + mutationQueue: OutgoingMutationQueue( + storageAdapter: storageAdapter, + dataStoreConfiguration: .testDefault(), + authModeStrategy: AWSDefaultAuthModeStrategy() + ) + ) } - let post = Post(title: "Post title", - content: "Post content", - createdAt: .now()) + 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 + } - if outboxStatusReceivedCurrentCount.get() == 1 { - XCTAssertTrue(outboxStatusEvent.isEmpty) - outboxStatusOnStart.fulfill() - } else { - XCTAssertFalse(outboxStatusEvent.isEmpty) - outboxStatusOnMutationEnqueued.fulfill() - } + 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 } @@ -79,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") @@ -86,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, @@ -97,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 @@ -112,10 +133,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() } @@ -123,21 +145,46 @@ 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 }) + { + try? await Task.sleep(seconds: timeout + 1) + 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): @@ -146,7 +193,6 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { mutationEventSaved.fulfill() } } - } await fulfillment(of: [mutationEventSaved], timeout: 1.0) @@ -163,13 +209,17 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { return } - if outboxStatusReceivedCurrentCount == 1 { + switch outboxStatusReceivedCurrentCount { + case 1: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnStart.fulfill() - } else { + case 2: XCTAssertFalse(outboxStatusEvent.isEmpty) outboxStatusOnMutationEnqueued.fulfill() + default: + XCTFail("Should not trigger outbox status event") } + } guard try await HubListenerTestUtilities.waitForListener(with: hubListener, timeout: 5.0) else { @@ -195,17 +245,12 @@ class OutgoingMutationQueueTests: SyncEngineTestBase { try await startAmplify() } - - - await fulfillment( - of: [ - outboxStatusOnStart, - outboxStatusOnMutationEnqueued, - mutation1Sent, - mutation2Sent - ], - timeout: 5.0 - ) + await fulfillment(of: [ + outboxStatusOnStart, + outboxStatusOnMutationEnqueued, + mutation1Sent, + mutation2Sent + ], timeout: timeout) Amplify.Hub.removeListener(hubListener) } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/OutgoingMutationQueueTestsWithMockStateMachine.swift index 0699e41032..722a3821c2 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 { await fulfillment(of: [expect], 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() async { @@ -135,7 +132,7 @@ class OutgoingMutationQueueMockStateTest: XCTestCase { await fulfillment(of: [expect], 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..fc2133fa69 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/ProcessMutationErrorFromCloudOperationTests.swift @@ -530,7 +530,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - MutationType is `delete`, remote model is an update, conflict handler returns `.retryLocal` /// - Then: /// - API is called to delete with local model - func testConflictUnhandledForDeleteMutationAndUpdatedRemoteModelReturnsRetryLocal() throws { + 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) @@ -546,19 +546,32 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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 + 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") @@ -585,21 +598,9 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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) + await fulfillment(of: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) } /// - Given: Conflict Unhandled error @@ -607,7 +608,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - 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 { + 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) @@ -624,19 +625,28 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { } 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 + 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") @@ -663,21 +673,9 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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) + await fulfillment(of: [expectConflicthandlerCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [apiMutateCalled], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) } /// - Given: Conflict Unhandled error @@ -685,7 +683,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - 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 { + 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) @@ -742,9 +740,9 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { completion: completion) queue.addOperation(operation) - wait(for: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectHubEvent], timeout: defaultAsyncWaitTimeout) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [modelSavedEvent], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectHubEvent], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) Amplify.Hub.removeListener(hubListener) } @@ -753,13 +751,15 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - MutationType is `update`, remote model is deleted /// - Then: /// - Local model is deleted, result mutationEvent is `delete` - func testConflictUnhandledForUpdateMutationAndDeletedRemoteModel() throws { + 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 { + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: true, + version: 2 + ) else { XCTFail("Couldn't get GraphQL response with remote post") return } @@ -797,19 +797,23 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { expectHubEvent.fulfill() } } - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: .testDefault(), - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) + 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) + await fulfillment(of: [ + modelDeletedEvent, + metadataSavedEvent, + expectHubEvent, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) Amplify.Hub.removeListener(hubListener) } @@ -818,13 +822,15 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - 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 { + 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 { + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: false, + version: 2 + ) else { XCTFail("Couldn't get GraphQL response with remote post") return } @@ -879,19 +885,24 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { expectConflicthandlerCalled.fulfill() resolve(.applyRemote) }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - completion: completion) + + 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) + await fulfillment(of: [ + expectConflicthandlerCalled, + modelSavedEvent, + metadataSavedEvent, + expectHubEvent, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) Amplify.Hub.removeListener(hubListener) } @@ -900,13 +911,15 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - 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 { + 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 { + guard let graphQLResponseError = try getGraphQLResponseError( + withRemote: remotePost, + deleted: false, + version: 2 + ) else { XCTFail("Couldn't get GraphQL response with remote post") return } @@ -920,19 +933,32 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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 + 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") @@ -948,30 +974,23 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { expectConflicthandlerCalled.fulfill() resolve(.retryLocal) }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) + 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) + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) } /// - Given: Conflict Unhandled error @@ -979,7 +998,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - 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 { + 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) @@ -1000,19 +1019,30 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { } 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 + 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") @@ -1028,29 +1058,22 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { expectConflicthandlerCalled.fulfill() resolve(.retry(retryModel)) }) - let operation = ProcessMutationErrorFromCloudOperation(dataStoreConfiguration: configuration, - mutationEvent: mutationEvent, - api: mockAPIPlugin, - storageAdapter: storageAdapter, - graphQLResponseError: graphQLResponseError, - reconciliationQueue: reconciliationQueue, - completion: completion) + 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) + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) } /// - Given: Conflict Unhandled error @@ -1059,7 +1082,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - API is called to update with local model and response contains error /// - Then: /// - `DataStoreErrorHandler` is called - func testConflictUnhandledSyncToCloudReturnsError() throws { + 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) @@ -1079,19 +1102,18 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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 + 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") @@ -1118,18 +1140,12 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { 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) + await fulfillment(of: [ + expectConflicthandlerCalled, + apiMutateCalled, + expectErrorHandlerCalled, + expectCompletion + ], timeout: defaultAsyncWaitTimeout) } /// Given: GraphQL "OperationDisabled" error @@ -1137,7 +1153,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { /// - API is called and response contains an "OperationDisabled" error /// - Then: /// - Completion handler is successfully called - func testProcessOperationDisabledError() throws { + 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") @@ -1164,7 +1180,7 @@ class ProcessMutationErrorFromCloudOperationTests: XCTestCase { completion: completion) queue.addOperation(operation) - wait(for: [expectCompletion], timeout: defaultAsyncWaitTimeout) + await fulfillment(of: [expectCompletion], timeout: defaultAsyncWaitTimeout) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift index 71baed1a34..dbe8220e59 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/MutationQueue/SyncMutationToCloudOperationTests.swift @@ -38,38 +38,38 @@ class SyncMutationToCloudOperationTests: XCTestCase { 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 listenerFromFirstRequestOptional: GraphQLOperation>.ResultListener? - var listenerFromSecondRequestOptional: GraphQLOperation>.ResultListener? - var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { request, eventListener in + 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) - listenerFromFirstRequestOptional = eventListener expectFirstCallToAPIMutate.fulfill() - } else if numberOfTimesEntered == 1 { - listenerFromSecondRequestOptional = eventListener + 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)) } - 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 + mockAPIPlugin.responders[.mutateRequestResponse] = responder let completion: GraphQLOperation>.ResultListener = { _ in expectMutationRequestCompletion.fulfill() } - let model = MockSynced(id: "id-1") let operation = await SyncMutationToCloudOperation( mutationEvent: mutationEvent, getLatestSyncMetadata: { @@ -89,32 +89,11 @@ class SyncMutationToCloudOperationTests: XCTestCase { ) 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) + await fulfillment(of: [ + expectFirstCallToAPIMutate, + expectSecondCallToAPIMutate, + expectMutationRequestCompletion + ], timeout: defaultAsyncWaitTimeout) } func testRetryOnChangeReachability() async throws { @@ -127,28 +106,30 @@ class SyncMutationToCloudOperationTests: XCTestCase { 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? + let model = MockSynced(id: "id-1") var numberOfTimesEntered = 0 - let responder = MutateRequestListenerResponder> { _, eventListener in + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } if numberOfTimesEntered == 0 { - listenerFromFirstRequestOptional = eventListener expectFirstCallToAPIMutate.fulfill() - } else if numberOfTimesEntered == 1 { - listenerFromSecondRequestOptional = eventListener + 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)) } - 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 + mockAPIPlugin.responders[.mutateRequestResponse] = responder let completion: GraphQLOperation>.ResultListener = { _ in expectMutationRequestCompletion.fulfill() @@ -166,30 +147,10 @@ class SyncMutationToCloudOperationTests: XCTestCase { 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) } @@ -203,23 +164,20 @@ class SyncMutationToCloudOperationTests: XCTestCase { 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 + let responder = MutateRequestResponder> { _ in + defer { numberOfTimesEntered += 1 } if numberOfTimesEntered == 0 { - listenerFromFirstRequestOptional = eventListener 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)) } - 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 + mockAPIPlugin.responders[.mutateRequestResponse] = responder let completion: GraphQLOperation>.ResultListener = { asyncEvent in switch asyncEvent { @@ -242,13 +200,6 @@ class SyncMutationToCloudOperationTests: XCTestCase { 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() @@ -326,28 +277,13 @@ class SyncMutationToCloudOperationTests: XCTestCase { } ) - 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 + let responder = MutateRequestResponder> { request in + defer { numberOfTimesEntered += 1 } + return .failure(.unknown("", "", error)) } - mockAPIPlugin.responders[.mutateRequestListener] = responder - + mockAPIPlugin.responders[.mutateRequestResponse] = responder + let queue = OperationQueue() queue.addOperation(operation) @@ -372,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 }, @@ -379,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 { @@ -435,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/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/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift index 52447a2ff4..0b83465e7e 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift @@ -758,20 +758,29 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in expect.fulfill() }).store(in: &cancellables) - await fulfillment(of: [expect, storageExpect, storageMetadataExpect, notifyExpect, hubExpect], timeout: 1) + await fulfillment(of: [ + expect, + storageExpect, + storageMetadataExpect, + notifyExpect, + hubExpect + ], timeout: 1) } 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 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 storageExpect = expectation(description: "storage save/delete should be called") @@ -835,7 +844,13 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in expect.fulfill() }).store(in: &cancellables) - await fulfillment(of: [expect, storageExpect, notifyExpect, storageMetadataExpect, hubExpect], timeout: 1) + await fulfillment(of: [ + expect, + storageExpect, + storageMetadataExpect, + notifyExpect, + hubExpect + ], timeout: 1) } func testApplyRemoteModels_skipFailedOperations() async throws { @@ -890,7 +905,12 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { }, receiveValue: { _ in }).store(in: &cancellables) - await fulfillment(of: [expect, expectedDropped, expectedDeleteSuccess], timeout: 1) + + await fulfillment(of: [ + expect, + expectedDropped, + expectedDeleteSuccess + ], timeout: 1) } func testApplyRemoteModels_failWithConstraintViolationShouldBeSuccessful() async { 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/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/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/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 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/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index 7a59c3c809..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 AWSPluginsCore +@_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 b198b5e45d..e2c78b496e 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 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 e93baf21c2..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 AWSPluginsCore +@_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 fd10c9a9fa..9a2b02fa87 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 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 76b32228ce..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 AWSPluginsCore +@_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 90bebdf058..6a11ded903 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 InternalAmplifyCredentials import AWSClientRuntime import ClientRuntime diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift index b90c7e0a03..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 AWSPluginsCore +@_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 14dd8aca32..a930bb2184 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 InternalAmplifyCredentials /// - 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 cce7df5f2a..d317df8ea8 100644 --- a/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift +++ b/AmplifyPlugins/Storage/Tests/AWSS3StoragePluginTests/Support/Internal/DefaultStorageTransferDatabaseTests.swift @@ -73,7 +73,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key1" })) XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key2" })) } - + /// Given: A DefaultStorageTransferDatabase /// When: linkTasksWithSessions is invoked with tasks containing multipart uploads but without a sessionTask, and a session /// Then: A StorageTransferTaskPairs linking the tasks with the session is returned @@ -127,7 +127,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { uploadId: "uploadId", uploadFile: uploadFile ) - + let transferTask1 = StorageTransferTask( transferType: .multiPartUploadPart( uploadId: "uploadId", @@ -159,7 +159,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { bytes: Bytes.megabytes(6).bytes, eTag: "eTag" ) - + let transferTask2 = StorageTransferTask( transferType: .multiPartUploadPart( uploadId: "uploadId", @@ -184,7 +184,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { bytesTransferred: Bytes.megabytes(3).bytes, taskIdentifier: 1 ) - + let pairs = database.linkTasksWithSessions( persistableTransferTasks: [ "taskId0": .init(task: transferTask0), @@ -195,7 +195,7 @@ class DefaultStorageTransferDatabaseTests: XCTestCase { session ] ) - + XCTAssertEqual(pairs.count, 3) XCTAssertTrue(pairs.contains(where: { $0.transferTask.key == "key1" })) XCTAssertFalse(pairs.contains(where: { $0.transferTask.key == "key2" })) diff --git a/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift b/AmplifyTestCommon/Mocks/MockAPICategoryPlugin.swift index 2b65491429..d1336f66a1 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? @@ -51,101 +51,26 @@ 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 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) 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 + return .failure(.unknown("", "", nil)) } func subscribe(request: GraphQLRequest) -> AmplifyAsyncThrowingSequence> { @@ -155,7 +80,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/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/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/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..a9e374f9e8 100644 --- a/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift +++ b/AmplifyTests/CategoryTests/API/RetryableGraphQLOperationTests.swift @@ -7,150 +7,71 @@ 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() - } - - let requestFactory: RequestFactory = { - requestFactoryExpectation.fulfill() - return self.makeTestRequest() - - } - - 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() + /// 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("", "")) } - operation.main() - - wait(for: [requestFactoryExpectation, resultExpectation], timeout: 10) - } - - /// 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() + let expectation2 = expectation(description: "Operation 2 successfully finished") + let operation2: () async throws -> GraphQLResponse = { + expectation2.fulfill() + return .success("operation 2") } - let requestFactory: RequestFactory = { - requestFactoryExpectation.fulfill() - return self.makeTestRequest() - + let operationStream = AsyncStream { continuation in + continuation.yield(operation1) + continuation.yield(operation2) + continuation.finish() } - - let operation = RetryableGraphQLOperation(requestFactory: requestFactory, - maxRetries: maxRetries, - resultListener: resultListener) { _, wrappedListener in - - wrappedListener( - .failure(self.makeSignedOutAuthError()) - ) - return self.makeTestOperation() + let result = await RetryableGraphQLOperation(requestStream: operationStream).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 operationStream = AsyncStream { continuation in + continuation.yield(operation1) + continuation.yield(operation2) + continuation.finish() } - 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) - } - - func makeSignedOutAuthError() -> APIError { - return APIError.operationError("Error", "", AuthError.signedOut("AuthError", "")) + let result = await RetryableGraphQLOperation(requestStream: operationStream).run() + if case .failure(.unknown(let description, _, _)) = result { + XCTAssertEqual(description, "~Unknown~") + } else { + XCTFail("Wrong result") + } + await fulfillment(of: [expectation1, expectation2], timeout: 0.3) } - - /// Convenience type alias - private typealias Payload = String - private typealias ResultListener = RetryableGraphQLOperation.OperationResultListener - private typealias RequestFactory = RetryableGraphQLOperation.RequestFactory } diff --git a/Package.swift b/Package.swift index afe1ff72bc..0fe21039e6 100644 --- a/Package.swift +++ b/Package.swift @@ -30,8 +30,7 @@ let amplifyTargets: [Target] = [ .target( name: "AWSPluginsCore", dependencies: [ - "Amplify", - .product(name: "AWSClientRuntime", package: "aws-sdk-swift") + "Amplify" ], path: "AmplifyPlugins/Core/AWSPluginsCore", exclude: [ @@ -41,12 +40,24 @@ let amplifyTargets: [Target] = [ .copy("Resources/PrivacyInfo.xcprivacy") ] ), + .target( + name: "InternalAmplifyCredentials", + dependencies: [ + "Amplify", + "AWSPluginsCore", + .product(name: "AWSClientRuntime", package: "aws-sdk-swift") + ], + path: "AmplifyPlugins/Core/AmplifyCredentials", + resources: [ + .copy("Resources/PrivacyInfo.xcprivacy") + ] + ), .target( name: "AmplifyTestCommon", dependencies: [ "Amplify", "CwlPreconditionTesting", - "AWSPluginsCore" + "InternalAmplifyCredentials" ], path: "AmplifyTestCommon", exclude: [ @@ -89,6 +100,7 @@ let amplifyTargets: [Target] = [ dependencies: [ "Amplify", "AWSPluginsCore", + "InternalAmplifyCredentials", .product(name: "AWSClientRuntime", package: "aws-sdk-swift") ], path: "AmplifyPlugins/Core/AWSPluginsTestCommon", @@ -100,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: "InternalAmplifyCredentialsTests", + dependencies: [ + "InternalAmplifyCredentials", + "AmplifyTestCommon", + .product(name: "AWSClientRuntime", package: "aws-sdk-swift") + ], + path: "AmplifyPlugins/Core/AmplifyCredentialsTests" ) ] @@ -115,7 +135,7 @@ let apiTargets: [Target] = [ name: "AWSAPIPlugin", dependencies: [ .target(name: "Amplify"), - .target(name: "AWSPluginsCore") + .target(name: "InternalAmplifyCredentials") ], path: "AmplifyPlugins/API/Sources/AWSAPIPlugin", exclude: [ @@ -161,6 +181,7 @@ let authTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AmplifySRP"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSClientRuntime", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentityProvider", package: "aws-sdk-swift"), .product(name: "AWSCognitoIdentity", package: "aws-sdk-swift") @@ -234,6 +255,7 @@ let storageTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSS3", package: "aws-sdk-swift")], path: "AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin", exclude: [ @@ -264,6 +286,7 @@ let geoTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSLocation", package: "aws-sdk-swift")], path: "AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin", exclude: [ @@ -295,6 +318,7 @@ let internalPinpointTargets: [Target] = [ .target(name: "Amplify"), .target(name: "AWSCognitoAuthPlugin"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "SQLite", package: "SQLite.swift"), .product(name: "AWSPinpoint", package: "aws-sdk-swift"), .product(name: "AmplifyUtilsNotifications", package: "amplify-swift-utils-notifications") @@ -363,6 +387,7 @@ let predictionsTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .target(name: "CoreMLPredictionsPlugin"), .product(name: "AWSComprehend", package: "aws-sdk-swift"), .product(name: "AWSPolly", package: "aws-sdk-swift"), @@ -413,6 +438,7 @@ let loggingTargets: [Target] = [ dependencies: [ .target(name: "Amplify"), .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyCredentials"), .product(name: "AWSCloudWatchLogs", package: "aws-sdk-swift"), ], path: "AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin", @@ -458,6 +484,10 @@ let package = Package( name: "AWSPluginsCore", targets: ["AWSPluginsCore"] ), + .library( + name: "InternalAmplifyCredentials", + targets: ["InternalAmplifyCredentials"] + ), .library( name: "AWSAPIPlugin", targets: ["AWSAPIPlugin"] diff --git a/fastlane/Fastfile b/fastlane/Fastfile index d8e9410469..8906cbec32 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -46,7 +46,8 @@ platform :ios do desc "Increment versions" private_lane :increment_versions do |options| version = options[:version].to_s - set_key_value(file: "AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift", key: "amplifyVersion", value: version) + configuration_file_path = "AmplifyPlugins/Core/AmplifyCredentials/AmplifyAWSServiceConfiguration.swift" + set_key_value(file: configuration_file_path, key: "amplifyVersion", value: version) end desc "Commit and push" @@ -77,7 +78,7 @@ platform :ios do version = options[:version].to_s changelog = options[:changelog] tag = "#{version}" - + sh('bundle', 'exec', 'swift', 'package', 'update') write_changelog(changelog: changelog, path: 'CHANGELOG.md')