From a236f6c15180c961f601fb95d0092881883243a6 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Tue, 5 Sep 2023 16:18:45 -0700 Subject: [PATCH 1/7] test(storage): update storage integration test with longer expiration duration (#3208) --- .../AWSS3StoragePluginOptionsUsabilityTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginOptionsUsabilityTests.swift b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginOptionsUsabilityTests.swift index 34fc3e954e..192bed0f53 100644 --- a/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginOptionsUsabilityTests.swift +++ b/AmplifyPlugins/Storage/Tests/StorageHostApp/AWSS3StoragePluginIntegrationTests/AWSS3StoragePluginOptionsUsabilityTests.swift @@ -24,7 +24,7 @@ class AWSS3StoragePluginOptionsUsabilityTests: AWSS3StoragePluginTestBase { await uploadData(key: key, dataString: key) #if os(iOS) - let expires = 10 + let expires = 20 #else let expires = 1 #endif @@ -69,7 +69,7 @@ class AWSS3StoragePluginOptionsUsabilityTests: AWSS3StoragePluginTestBase { task.resume() await waitForExpectations(timeout: TestCommonConstants.networkTimeout) - try await Task.sleep(seconds: 15) + try await Task.sleep(seconds: 30) #else try await Task.sleep(seconds: 2) #endif From 659fdf994456c09621634a6dafa88ab92da43c9b Mon Sep 17 00:00:00 2001 From: Di Wu Date: Wed, 6 Sep 2023 10:05:33 -0700 Subject: [PATCH 2/7] fix(datastore): wrap failures to result when applying remote update events (#3187) --- .../ReconcileAndLocalSaveOperation.swift | 115 ++++++++---------- .../ReconcileAndLocalSaveOperationTests.swift | 78 ++++++------ 2 files changed, 85 insertions(+), 108 deletions(-) diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift index d027edc0aa..9afaf2e8a1 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/Sync/SubscriptionSync/ReconcileAndLocalSave/ReconcileAndLocalSaveOperation.swift @@ -264,79 +264,64 @@ class ReconcileAndLocalSaveOperation: AsynchronousOperation { return dispositions } + func applyRemoteModelsDisposition( + storageAdapter: StorageEngineAdapter, + disposition: RemoteSyncReconciler.Disposition + ) -> AnyPublisher, Never> { + let operation: Future + let mutationType: MutationEvent.MutationType + switch disposition { + case .create(let remoteModel): + operation = self.save(storageAdapter: storageAdapter, remoteModel: remoteModel) + mutationType = .create + case .update(let remoteModel): + operation = self.save(storageAdapter: storageAdapter, remoteModel: remoteModel) + mutationType = .update + case .delete(let remoteModel): + operation = self.delete(storageAdapter: storageAdapter, remoteModel: remoteModel) + mutationType = .delete + } + + return operation + .flatMap { applyResult in + self.saveMetadata(storageAdapter: storageAdapter, applyResult: applyResult, mutationType: mutationType) + } + .map {_ in Result.success(()) } + .catch { Just>(.failure($0))} + .eraseToAnyPublisher() + } + // TODO: refactor - move each the publisher constructions to its own utility method for readability of the // `switch` and a single method that you can invoke in the `map` func applyRemoteModelsDispositions( - _ dispositions: [RemoteSyncReconciler.Disposition]) -> Future { - Future { promise in - var result: Result = .failure(Self.unfulfilledDataStoreError()) - defer { - promise(result) - } - guard !self.isCancelled else { - self.log.info("\(#function) - cancelled, aborting") - result = .successfulVoid - return - } - guard let storageAdapter = self.storageAdapter else { - let error = DataStoreError.nilStorageAdapter() - self.notifyDropped(count: dispositions.count, error: error) - result = .failure(error) - return - } + _ dispositions: [RemoteSyncReconciler.Disposition] + ) -> Future { + guard !self.isCancelled else { + self.log.info("\(#function) - cancelled, aborting") + return Future { $0(.successfulVoid) } + } - guard !dispositions.isEmpty else { - result = .successfulVoid - return - } + guard let storageAdapter = self.storageAdapter else { + let error = DataStoreError.nilStorageAdapter() + self.notifyDropped(count: dispositions.count, error: error) + return Future { $0(.failure(error)) } + } - let publishers = dispositions.map { disposition -> - Publishers.FlatMap, - Future> in - - switch disposition { - case .create(let remoteModel): - let publisher = self.save(storageAdapter: storageAdapter, - remoteModel: remoteModel) - .flatMap { applyResult in - self.saveMetadata(storageAdapter: storageAdapter, - applyResult: applyResult, - mutationType: .create) - } - return publisher - case .update(let remoteModel): - let publisher = self.save(storageAdapter: storageAdapter, - remoteModel: remoteModel) - .flatMap { applyResult in - self.saveMetadata(storageAdapter: storageAdapter, - applyResult: applyResult, - mutationType: .update) - } - return publisher - case .delete(let remoteModel): - let publisher = self.delete(storageAdapter: storageAdapter, - remoteModel: remoteModel) - .flatMap { applyResult in - self.saveMetadata(storageAdapter: storageAdapter, - applyResult: applyResult, - mutationType: .delete) - } - return publisher - } - } + guard !dispositions.isEmpty else { + return Future { $0(.successfulVoid) } + } + + let publishers = dispositions.map { + applyRemoteModelsDisposition(storageAdapter: storageAdapter, disposition: $0) + } + return Future { promise in Publishers.MergeMany(publishers) .collect() - .sink( - receiveCompletion: { - if case .failure(let error) = $0 { - result = .failure(error) - } - }, - receiveValue: { _ in - result = .successfulVoid - } - ) + .sink { _ in + // This stream will never fail, as we wrapped error in the result type. + promise(.successfulVoid) + } receiveValue: { _ in } .store(in: &self.cancellables) } } diff --git a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift index ac7d3d0af8..2b7d4220e6 100644 --- a/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/AWSDataStorePluginTests/Sync/SubscriptionSync/ReconcileAndLocalSaveOperationTests.swift @@ -832,11 +832,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { waitForExpectations(timeout: 1) } - func testApplyRemoteModels_saveFail() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - + func testApplyRemoteModels_skipFailedOperations() throws { let dispositions: [RemoteSyncReconciler.Disposition] = [.create(anyPostMutationSync), .create(anyPostMutationSync), .update(anyPostMutationSync), @@ -846,7 +842,7 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { .create(anyPostMutationSync), .update(anyPostMutationSync), .delete(anyPostMutationSync)] - let expect = expectation(description: "should fail") + let expect = expectation(description: "should complete") let expectedDeleteSuccess = expectation(description: "delete should be successful") expectedDeleteSuccess.expectedFulfillmentCount = 3 // 3 delete depositions let expectedDropped = expectation(description: "mutationEventDropped received") @@ -881,12 +877,12 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { .sink(receiveCompletion: { completion in switch completion { case .failure: - expect.fulfill() + XCTFail("Unexpected failure completion") case .finished: - XCTFail("Unexpected successfully completion") + expect.fulfill() } }, receiveValue: { _ in - XCTFail("Unexpected value received") + }).store(in: &cancellables) waitForExpectations(timeout: 1) } @@ -949,20 +945,18 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { } func testApplyRemoteModels_deleteFail() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - - 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 fail") + 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 success") let expectedCreateAndUpdateSuccess = expectation(description: "create and updates should be successful") expectedCreateAndUpdateSuccess.expectedFulfillmentCount = 6 // 3 creates and 3 updates let expectedDropped = expectation(description: "mutationEventDropped received") @@ -997,31 +991,29 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { .sink(receiveCompletion: { completion in switch completion { case .failure: - expect.fulfill() + XCTFail("Unexpected failure completion") case .finished: - XCTFail("Unexpected successfully completion") + expect.fulfill() } }, receiveValue: { _ in - XCTFail("Unexpected value received") + }).store(in: &cancellables) waitForExpectations(timeout: 1) } func testApplyRemoteModels_saveMetadataFail() throws { - if skipBrokenTests { - throw XCTSkip("TODO: fix this test") - } - - 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 fail") + 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 success") let expectedDropped = expectation(description: "mutationEventDropped received") expectedDropped.expectedFulfillmentCount = 9 // 1 for each of the 9 dispositions let saveResponder = SaveUntypedModelResponder { _, completion in @@ -1053,12 +1045,12 @@ class ReconcileAndLocalSaveOperationTests: XCTestCase { .sink(receiveCompletion: { completion in switch completion { case .failure: - expect.fulfill() + XCTFail("Unexpected failure completion") case .finished: - XCTFail("Unexpected successfully completion") + expect.fulfill() } }, receiveValue: { _ in - XCTFail("Unexpected value received") + }).store(in: &cancellables) waitForExpectations(timeout: 1) } From 22ea4919b69b3e0cf722ca002be5539443a2c2d0 Mon Sep 17 00:00:00 2001 From: Tuan Pham <103537251+phantumcode@users.noreply.github.com> Date: Mon, 11 Sep 2023 08:07:01 -0700 Subject: [PATCH 3/7] fix(logging): fix issue with logger namespace not being set (#3213) * fix(logging): fix issue with logger namespace not being set * chore: update unit test --- .../AWSCloudWatchLoggingCategoryClient.swift | 8 ++++++++ .../AWSCloudWatchLoggingPlugin.swift | 2 +- .../AWSCloudWatchLoggingSessionController.swift | 2 +- .../AWSCloudWatchLoggingPluginTests.swift | 17 ++++++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingCategoryClient.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingCategoryClient.swift index bf38758815..9416ecae34 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingCategoryClient.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingCategoryClient.swift @@ -105,6 +105,14 @@ final class AWSCloudWatchLoggingCategoryClient { loggersByKey = [:] } } + + func getLoggerSessionController(forCategory category: String, logLevel: LogLevel) -> AWSCloudWatchLoggingSessionController? { + let key = LoggerKey(category: category, logLevel: logLevel) + if let existing = loggersByKey[key] { + return existing + } + return nil + } } extension AWSCloudWatchLoggingCategoryClient: LoggingCategoryClientBehavior { diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingPlugin.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingPlugin.swift index d4ab6d6e0e..8f40831cf7 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingPlugin.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingPlugin.swift @@ -72,7 +72,7 @@ public class AWSCloudWatchLoggingPlugin: LoggingCategoryPlugin { } public func logger(forCategory category: String, forNamespace namespace: String) -> Logger { - return loggingClient.logger(forCategory: category) + return loggingClient.logger(forCategory: category, forNamespace: namespace) } /// enable plugin diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift index d665753f51..18d2483ffb 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift @@ -20,13 +20,13 @@ import Network final class AWSCloudWatchLoggingSessionController { var client: CloudWatchLogsClientProtocol? + let namespace: String? private let logGroupName: String private let region: String private let localStoreMaxSizeInMB: Int private let credentialsProvider: CredentialsProvider private let authentication: AuthCategoryUserBehavior private let category: String - private let namespace: String? private var session: AWSCloudWatchLoggingSession? private var consumer: LogBatchConsumer? private let logFilter: AWSCloudWatchLoggingFilterBehavior diff --git a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginTests/AWSCloudWatchLoggingPluginTests.swift b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginTests/AWSCloudWatchLoggingPluginTests.swift index 4ac6cc81d1..8262d4b61c 100644 --- a/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginTests/AWSCloudWatchLoggingPluginTests.swift +++ b/AmplifyPlugins/Logging/Tests/AWSCloudWatchLoggingPluginTests/AWSCloudWatchLoggingPluginTests.swift @@ -43,6 +43,21 @@ final class AWSCloudWatchLoggingPluginTests: XCTestCase { let defaultLogger = plugin.logger(forNamespace: "test") XCTAssertEqual(defaultLogger.logLevel.rawValue, 0) - + } + + /// Given: a AWSCloudWatchLoggingPlugin + /// When: a logger is requested with a namespace + /// Then: the namespace is set in the logger session controller + func testPluginLoggerNamespace() throws { + let configuration = AWSCloudWatchLoggingPluginConfiguration(logGroupName: "testLogGroup", region: "us-east-1") + let plugin = AWSCloudWatchLoggingPlugin(loggingPluginConfiguration: configuration) + _ = plugin.logger(forCategory: "Category1") + var sessionController = plugin.loggingClient.getLoggerSessionController(forCategory: "Category1", logLevel: .error) + XCTAssertEqual(sessionController?.namespace, nil) + + _ = plugin.logger(forCategory: "Category2", forNamespace: "testNamespace") + sessionController = plugin.loggingClient.getLoggerSessionController(forCategory: "Category2", logLevel: .error) + XCTAssertEqual(sessionController?.namespace, "testNamespace") + } } From 6f95db94d766921c92fef5631cb9b867899bdeca Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 12 Sep 2023 12:56:51 -0700 Subject: [PATCH 4/7] fix(api): change request interceptors applying logic (#3190) * fix(api): apply cutomize request headers after interceptors * add integration test case * change interceptors applying logic * refactor code style * refactor code style for rest operations * add unit test case for customer header override * update interceptor names * update interceptor doc comment --- .../AWSAPICategoryPluginConfiguration.swift | 34 ++-- .../AWSAPIEndpointInterceptors.swift | 22 ++- .../AuthTokenURLRequestInterceptor.swift | 4 +- .../IAMURLRequestInterceptor.swift | 1 - .../Operation/AWSGraphQLOperation.swift | 182 ++++++++++++------ .../Operation/AWSRESTOperation.swift | 169 +++++++++------- .../Utils/GraphQLOperationRequestUtils.swift | 4 +- .../Utils/RESTOperationRequestUtils.swift | 18 +- .../Support/Utils/Result+Async.swift | 20 ++ .../GraphQLConnectionScenario4Tests.swift | 2 +- .../GraphQLModelBasedTests+List.swift | 2 +- .../RESTWithIAMIntegrationTests.swift | 18 ++ ...egoryPlugin+InterceptorBehaviorTests.swift | 8 +- ...SAPICategoryPluginConfigurationTests.swift | 22 ++- .../AWSAPIEndpointInterceptorsTests.swift | 19 +- .../AuthTokenURLRequestInterceptorTests.swift | 6 +- .../Operation/AWSRESTOperationTests.swift | 36 ++++ .../Support/Utils/RESTRequestUtilsTests.swift | 1 - .../Support/Utils/Result+AsyncTests.swift | 54 ++++++ 19 files changed, 430 insertions(+), 192 deletions(-) create mode 100644 AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Result+Async.swift create mode 100644 AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Result+AsyncTests.swift diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift index 9a4d894c41..200a813e96 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPICategoryPluginConfiguration.swift @@ -72,7 +72,7 @@ public struct AWSAPICategoryPluginConfiguration { self.authService = authService } - /// Registers an interceptor for the provided API endpoint + /// Registers an customer interceptor for the provided API endpoint /// - Parameter interceptor: operation interceptor used to decorate API requests /// - Parameter toEndpoint: API endpoint name mutating func addInterceptor(_ interceptor: URLRequestInterceptor, @@ -86,20 +86,16 @@ public struct AWSAPICategoryPluginConfiguration { /// Returns all the interceptors registered for `apiName` API endpoint /// - Parameter apiName: API endpoint name - /// - Returns: request interceptors - internal func interceptorsForEndpoint(named apiName: APIEndpointName) -> [URLRequestInterceptor] { - guard let interceptorsConfig = interceptors[apiName] else { - return [] - } - return interceptorsConfig.interceptors + /// - Returns: Optional AWSAPIEndpointInterceptors for the apiName + internal func interceptorsForEndpoint(named apiName: APIEndpointName) -> AWSAPIEndpointInterceptors? { + return interceptors[apiName] } - /// Returns interceptors for the provided endpointConfig + /// Returns the interceptors for the provided endpointConfig /// - Parameters: /// - endpointConfig: endpoint configuration - /// - Throws: PluginConfigurationError in case of failure building an instance of AWSAuthorizationConfiguration - /// - Returns: An array of URLRequestInterceptor - internal func interceptorsForEndpoint(withConfig endpointConfig: EndpointConfig) throws -> [URLRequestInterceptor] { + /// - Returns: Optional AWSAPIEndpointInterceptors for the endpointConfig + internal func interceptorsForEndpoint(withConfig endpointConfig: EndpointConfig) -> AWSAPIEndpointInterceptors? { return interceptorsForEndpoint(named: endpointConfig.name) } @@ -108,9 +104,11 @@ public struct AWSAPICategoryPluginConfiguration { /// - endpointConfig: endpoint configuration /// - authType: overrides the registered auth interceptor /// - Throws: PluginConfigurationError in case of failure building an instance of AWSAuthorizationConfiguration - /// - Returns: An array of URLRequestInterceptor - internal func interceptorsForEndpoint(withConfig endpointConfig: EndpointConfig, - authType: AWSAuthorizationType) throws -> [URLRequestInterceptor] { + /// - Returns: Optional AWSAPIEndpointInterceptors for the endpointConfig and authType + internal func interceptorsForEndpoint( + withConfig endpointConfig: EndpointConfig, + authType: AWSAuthorizationType + ) throws -> AWSAPIEndpointInterceptors? { guard let apiAuthProviderFactory = self.apiAuthProviderFactory else { return interceptorsForEndpoint(named: endpointConfig.name) @@ -126,12 +124,10 @@ public struct AWSAPICategoryPluginConfiguration { authConfiguration: authConfiguration) // retrieve current interceptors and replace auth interceptor - let currentInterceptors = interceptorsForEndpoint(named: endpointConfig.name).filter { - !isAuthInterceptor($0) - } - config.interceptors.append(contentsOf: currentInterceptors) + let currentInterceptors = interceptorsForEndpoint(named: endpointConfig.name) + config.interceptors.append(contentsOf: currentInterceptors?.interceptors ?? []) - return config.interceptors + return config } // MARK: Private diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift index 13f6ba16e8..9be9d3c05b 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Configuration/AWSAPIEndpointInterceptors.swift @@ -9,6 +9,14 @@ import Amplify import Foundation import AWSPluginsCore +/// The order of interceptor decoration is as follows: +/// 1. **prelude interceptors** +/// 2. **cutomize headers** +/// 3. **customer interceptors** +/// 4. **postlude interceptors** +/// +/// **Prelude** and **postlude** interceptors are used by library maintainers to +/// integrate essential functionality for a variety of authentication types. struct AWSAPIEndpointInterceptors { // API name let apiEndpointName: APIEndpointName @@ -16,8 +24,12 @@ struct AWSAPIEndpointInterceptors { let apiAuthProviderFactory: APIAuthProviderFactory let authService: AWSAuthServiceBehavior? + var preludeInterceptors: [URLRequestInterceptor] = [] + var interceptors: [URLRequestInterceptor] = [] + var postludeInterceptors: [URLRequestInterceptor] = [] + init(endpointName: APIEndpointName, apiAuthProviderFactory: APIAuthProviderFactory, authService: AWSAuthServiceBehavior? = nil) { @@ -42,7 +54,7 @@ struct AWSAPIEndpointInterceptors { case .apiKey(let apiKeyConfig): let provider = BasicAPIKeyProvider(apiKey: apiKeyConfig.apiKey) let interceptor = APIKeyURLRequestInterceptor(apiKeyProvider: provider) - addInterceptor(interceptor) + preludeInterceptors.append(interceptor) case .awsIAM(let iamConfig): guard let authService = authService else { throw PluginError.pluginConfigurationError("AuthService is not set for IAM", @@ -52,7 +64,7 @@ struct AWSAPIEndpointInterceptors { let interceptor = IAMURLRequestInterceptor(iamCredentialsProvider: provider, region: iamConfig.region, endpointType: endpointType) - addInterceptor(interceptor) + postludeInterceptors.append(interceptor) case .amazonCognitoUserPools: guard let authService = authService else { throw PluginError.pluginConfigurationError("AuthService not set for cognito user pools", @@ -60,7 +72,7 @@ struct AWSAPIEndpointInterceptors { } let provider = BasicUserPoolTokenProvider(authService: authService) let interceptor = AuthTokenURLRequestInterceptor(authTokenProvider: provider) - addInterceptor(interceptor) + preludeInterceptors.append(interceptor) case .openIDConnect: guard let oidcAuthProvider = apiAuthProviderFactory.oidcAuthProvider() else { throw PluginError.pluginConfigurationError("AuthService not set for OIDC", @@ -68,7 +80,7 @@ struct AWSAPIEndpointInterceptors { } let wrappedAuthProvider = AuthTokenProviderWrapper(tokenAuthProvider: oidcAuthProvider) let interceptor = AuthTokenURLRequestInterceptor(authTokenProvider: wrappedAuthProvider) - addInterceptor(interceptor) + preludeInterceptors.append(interceptor) case .function: guard let functionAuthProvider = apiAuthProviderFactory.functionAuthProvider() else { throw PluginError.pluginConfigurationError("AuthService not set for function auth", @@ -76,7 +88,7 @@ struct AWSAPIEndpointInterceptors { } let wrappedAuthProvider = AuthTokenProviderWrapper(tokenAuthProvider: functionAuthProvider) let interceptor = AuthTokenURLRequestInterceptor(authTokenProvider: wrappedAuthProvider) - addInterceptor(interceptor) + preludeInterceptors.append(interceptor) } } } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift index 0175364b7a..d7392748e8 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/AuthTokenURLRequestInterceptor.swift @@ -32,9 +32,7 @@ struct AuthTokenURLRequestInterceptor: URLRequestInterceptor { mutableRequest.setValue(amzDate, forHTTPHeaderField: URLRequestConstants.Header.xAmzDate) - mutableRequest.setValue(URLRequestConstants.ContentType.applicationJson, - forHTTPHeaderField: URLRequestConstants.Header.contentType) - mutableRequest.setValue(userAgent, + mutableRequest.addValue(userAgent, forHTTPHeaderField: URLRequestConstants.Header.userAgent) let token: String diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift index 013cac0459..0a9b53fe93 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/RequestInterceptor/IAMURLRequestInterceptor.swift @@ -36,7 +36,6 @@ struct IAMURLRequestInterceptor: URLRequestInterceptor { throw APIError.unknown("Could not get host from mutable request", "") } - request.setValue(URLRequestConstants.ContentType.applicationJson, forHTTPHeaderField: URLRequestConstants.Header.contentType) request.setValue(host, forHTTPHeaderField: "host") request.setValue(userAgent, forHTTPHeaderField: URLRequestConstants.Header.userAgent) diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift index 2f6ed4ba27..859981e321 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLOperation.swift @@ -34,6 +34,10 @@ final public class AWSGraphQLOperation: GraphQLOperation { } override public func main() { + Task { await mainAsync() } + } + + private func mainAsync() async { Amplify.API.log.debug("Starting \(request.operationType) \(id)") if isCancelled { @@ -41,43 +45,79 @@ final public class AWSGraphQLOperation: GraphQLOperation { return } - // Validate the request - do { - try request.validate() - } catch let error as APIError { + let urlRequest = validateRequest(request).flatMap(buildURLRequest(from:)) + let finalRequest = await getEndpointInterceptors(from: request).flatMapAsync { requestInterceptors in + let preludeInterceptors = requestInterceptors?.preludeInterceptors ?? [] + let customerInterceptors = requestInterceptors?.interceptors ?? [] + let postludeInterceptors = requestInterceptors?.postludeInterceptors ?? [] + + var finalResult = urlRequest + // apply prelude interceptors + for interceptor in preludeInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) + } + } + + // there is no customize headers for GraphQLOperationRequest + + // apply customer interceptors + for interceptor in customerInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) + } + } + + // apply postlude interceptor + for interceptor in postludeInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) + } + } + return finalResult + } + + switch finalRequest { + case .success(let finalRequest): + if isCancelled { + finish() + return + } + + // Begin network task + Amplify.API.log.debug("Starting network task for \(request.operationType) \(id)") + let task = session.dataTaskBehavior(with: finalRequest) + mapper.addPair(operation: self, task: task) + task.resume() + case .failure(let error): dispatch(result: .failure(error)) finish() - return - } catch { - dispatch(result: .failure(APIError.unknown("Could not validate request", "", nil))) - finish() - return } + } - // Retrieve endpoint configuration - let endpointConfig: AWSAPICategoryPluginConfiguration.EndpointConfig - let requestInterceptors: [URLRequestInterceptor] - + private func validateRequest(_ request: GraphQLOperationRequest) -> Result, APIError> { do { - endpointConfig = try pluginConfig.endpoints.getConfig(for: request.apiName, endpointType: .graphQL) - - if let pluginOptions = request.options.pluginOptions as? AWSPluginOptions, - let authType = pluginOptions.authType { - requestInterceptors = try pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig, - authType: authType) - } else { - requestInterceptors = try pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig) - } + try request.validate() + return .success(request) } catch let error as APIError { - dispatch(result: .failure(error)) - finish() - return + return .failure(error) } catch { - dispatch(result: .failure(APIError.unknown("Could not get endpoint configuration", "", nil))) - finish() - return + return .failure(APIError.unknown("Could not validate request", "", nil)) } + } + private func buildURLRequest(from request: GraphQLOperationRequest) -> Result { + getEndpointConfig(from: request).flatMap { endpointConfig in + getRequestPayload(from: request).map { requestPayload in + GraphQLOperationRequestUtils.constructRequest( + with: endpointConfig.baseURL, + requestPayload: requestPayload + ) + } + } + } + + private func getRequestPayload(from request: GraphQLOperationRequest) -> Result { // Prepare request payload let queryDocument = GraphQLOperationRequestUtils.getQueryDocument(document: request.document, variables: request.variables) @@ -87,48 +127,64 @@ final public class AWSGraphQLOperation: GraphQLOperation { let prettyPrintedQueryDocument = String(data: serializedJSON, encoding: .utf8) { Amplify.API.log.verbose("\(prettyPrintedQueryDocument)") } - let requestPayload: Data + do { - requestPayload = try JSONSerialization.data(withJSONObject: queryDocument) + return .success(try JSONSerialization.data(withJSONObject: queryDocument)) } catch { - dispatch(result: .failure(APIError.operationError("Failed to serialize query document", - "fix the document or variables", - error))) - finish() - return + return .failure(APIError.operationError( + "Failed to serialize query document", + "fix the document or variables", + error + )) } + } - // Create request - let urlRequest = GraphQLOperationRequestUtils.constructRequest(with: endpointConfig.baseURL, - requestPayload: requestPayload) - - Task { - // Intercept request - var finalRequest = urlRequest - for interceptor in requestInterceptors { - do { - finalRequest = try await interceptor.intercept(finalRequest) - } catch let error as APIError { - dispatch(result: .failure(error)) - cancel() - } catch { - dispatch(result: .failure(APIError.operationError("Failed to intercept request fully.", - "Something wrong with the interceptor", - error))) - cancel() - } - } + private func getEndpointConfig(from request: GraphQLOperationRequest) -> Result { + do { + return .success(try pluginConfig.endpoints.getConfig(for: request.apiName, endpointType: .graphQL)) + } catch let error as APIError { + return .failure(error) - if isCancelled { - finish() - return + } catch { + return .failure(APIError.unknown("Could not get endpoint configuration", "", nil)) + } + } + + private func getEndpointInterceptors(from request: GraphQLOperationRequest) -> Result { + getEndpointConfig(from: request).flatMap { endpointConfig in + do { + if let pluginOptions = request.options.pluginOptions as? AWSPluginOptions, + let authType = pluginOptions.authType + { + return .success(try pluginConfig.interceptorsForEndpoint( + withConfig: endpointConfig, + authType: authType + )) + } else { + return .success(pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig)) + } + } catch let error as APIError { + return .failure(error) + } catch { + return .failure(APIError.unknown("Could not get endpoint interceptors", "", nil)) } + } + } - // Begin network task - Amplify.API.log.debug("Starting network task for \(request.operationType) \(id)") - let task = session.dataTaskBehavior(with: finalRequest) - mapper.addPair(operation: self, task: task) - task.resume() + private func applyInterceptor(_ interceptor: URLRequestInterceptor, request: URLRequest) async -> Result { + do { + return .success(try await interceptor.intercept(request)) + } catch let error as APIError { + return .failure(error) + } catch { + return .failure( + APIError.operationError( + "Failed to intercept request with \(type(of: interceptor)). Error message: \(error.localizedDescription).", + "See underlying error for more details", + error + ) + ) } } + } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSRESTOperation.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSRESTOperation.swift index 1c8efc986d..3680b22728 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSRESTOperation.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSRESTOperation.swift @@ -40,82 +40,57 @@ final public class AWSRESTOperation: AmplifyOperation< /// The work to execute for this operation override public func main() { + Task { await mainAsync() } + } + + private func mainAsync() async { if isCancelled { finish() return } - // Validate the request - do { - try request.validate() - } catch let error as APIError { - dispatch(result: .failure(error)) - finish() - return - } catch { - dispatch(result: .failure(APIError.unknown("Could not validate request", "", nil))) - finish() - return - } + let urlRequest = validateRequest(request).flatMap(buildURLRequest(from:)) + let finalRequest = await getEndpointConfig(from: request).flatMapAsync { endpointConfig in + let interceptorConfig = pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig) + let preludeInterceptors = interceptorConfig?.preludeInterceptors ?? [] + let customerInterceptors = interceptorConfig?.interceptors ?? [] + let postludeInterceptors = interceptorConfig?.postludeInterceptors ?? [] + + var finalResult = urlRequest + // apply prelude interceptors + for interceptor in preludeInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) + } + } - // Retrieve endpoint configuration - let endpointConfig: AWSAPICategoryPluginConfiguration.EndpointConfig - let requestInterceptors: [URLRequestInterceptor] - do { - endpointConfig = try pluginConfig.endpoints.getConfig(for: request.apiName, endpointType: .rest) - requestInterceptors = try pluginConfig.interceptorsForEndpoint(withConfig: endpointConfig) - } catch let error as APIError { - dispatch(result: .failure(error)) - finish() - return - } catch { - dispatch(result: .failure(APIError.unknown("Could not get endpoint configuration", "", nil))) - finish() - return - } + // apply customize headers + finalResult = finalResult.map { urlRequest in + var mutableRequest = urlRequest + for (key, value) in request.headers ?? [:] { + mutableRequest.setValue(value, forHTTPHeaderField: key) + } + return mutableRequest + } - // Construct URL with path - let url: URL - do { - url = try RESTOperationRequestUtils.constructURL( - for: endpointConfig.baseURL, - withPath: request.path, - withParams: request.queryParameters - ) - } catch let error as APIError { - dispatch(result: .failure(error)) - finish() - return - } catch { - let apiError = APIError.operationError("Failed to construct URL", "", error) - dispatch(result: .failure(apiError)) - finish() - return - } + // apply customer interceptors + for interceptor in customerInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) + } + } - // Construct URL Request with url and request body - let urlRequest = RESTOperationRequestUtils.constructURLRequest(with: url, - operationType: request.operationType, - headers: request.headers, - requestPayload: request.body) - - Task { - // Intercept request - var finalRequest = urlRequest - for interceptor in requestInterceptors { - do { - finalRequest = try await interceptor.intercept(finalRequest) - } catch let error as APIError { - dispatch(result: .failure(error)) - cancel() - } catch { - dispatch(result: .failure(APIError.operationError("Failed to intercept request fully.", - "Something wrong with the interceptor", - error))) - cancel() + // apply postlude interceptor + for interceptor in postludeInterceptors { + finalResult = await finalResult.flatMapAsync { request in + await applyInterceptor(interceptor, request: request) } } + return finalResult + } + switch finalRequest { + case .success(let finalRequest): if isCancelled { finish() return @@ -126,6 +101,70 @@ final public class AWSRESTOperation: AmplifyOperation< let task = session.dataTaskBehavior(with: finalRequest) mapper.addPair(operation: self, task: task) task.resume() + case .failure(let error): + Amplify.API.log.debug("Dispatching error \(error)") + dispatch(result: .failure(error)) + finish() + } + } + + private func validateRequest(_ request: RESTOperationRequest) -> Result { + do { + try request.validate() + return .success(request) + } catch let error as APIError { + return .failure(error) + } catch { + return .failure(APIError.unknown("Could not validate request", "", nil)) + } + } + + private func getEndpointConfig( + from request: RESTOperationRequest + ) -> Result { + do { + return .success(try pluginConfig.endpoints.getConfig(for: request.apiName, endpointType: .rest)) + } catch let error as APIError { + return .failure(error) + } catch { + return .failure(APIError.unknown("Could not get endpoint configuration", "", nil)) + } + } + + private func buildURLRequest(from request: RESTOperationRequest) -> Result { + getEndpointConfig(from: request).flatMap { endpointConfig in + do { + let url = try RESTOperationRequestUtils.constructURL( + for: endpointConfig.baseURL, + withPath: request.path, + withParams: request.queryParameters + ) + return .success(RESTOperationRequestUtils.constructURLRequest( + with: url, + operationType: request.operationType, + requestPayload: request.body + )) + } catch let error as APIError { + return .failure(error) + } catch { + return .failure(APIError.operationError("Failed to construct URL", "", error)) + } + } + } + + private func applyInterceptor(_ interceptor: URLRequestInterceptor, request: URLRequest) async -> Result { + do { + return .success(try await interceptor.intercept(request)) + } catch let error as APIError { + return .failure(error) + } catch { + return .failure( + APIError.operationError( + "Failed to intercept request with \(type(of: interceptor)). Error message: \(error.localizedDescription).", + "See underlying error for more details", + error + ) + ) } } } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLOperationRequestUtils.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLOperationRequestUtils.swift index 18f695e68b..80e7bc74df 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLOperationRequestUtils.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/GraphQLOperationRequestUtils.swift @@ -22,8 +22,8 @@ class GraphQLOperationRequestUtils { // Construct a graphQL specific HTTP POST request with the request payload static func constructRequest(with baseUrl: URL, requestPayload: Data) -> URLRequest { var baseRequest = URLRequest(url: baseUrl) - let headers = ["content-type": "application/json", "Cache-Control": "no-store"] - baseRequest.allHTTPHeaderFields = headers + baseRequest.setValue("application/json", forHTTPHeaderField: "content-type") + baseRequest.setValue("no-store", forHTTPHeaderField: "cache-control") baseRequest.httpMethod = "POST" baseRequest.httpBody = requestPayload diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/RESTOperationRequestUtils.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/RESTOperationRequestUtils.swift index 9970ea0b4b..a73f46eaae 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/RESTOperationRequestUtils.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/RESTOperationRequestUtils.swift @@ -52,19 +52,13 @@ final class RESTOperationRequestUtils { } // Construct a request specific to the `RESTOperationType` - static func constructURLRequest(with url: URL, - operationType: RESTOperationType, - headers: [String: String]?, - requestPayload: Data?) -> URLRequest { - + static func constructURLRequest( + with url: URL, + operationType: RESTOperationType, + requestPayload: Data? + ) -> URLRequest { var baseRequest = URLRequest(url: url) - var requestHeaders = ["content-type": "application/json"] - if let headers = headers { - for (key, value) in headers { - requestHeaders[key] = value - } - } - baseRequest.allHTTPHeaderFields = requestHeaders + baseRequest.setValue("application/json", forHTTPHeaderField: "content-type") baseRequest.httpMethod = operationType.rawValue baseRequest.httpBody = requestPayload return baseRequest diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Result+Async.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Result+Async.swift new file mode 100644 index 0000000000..fbf204c971 --- /dev/null +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Support/Utils/Result+Async.swift @@ -0,0 +1,20 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import Foundation + +extension Result { + func flatMapAsync(_ f: (Success) async -> Result) async -> Result { + switch self { + case .success(let value): + return await f(value) + case .failure(let error): + return .failure(error) + } + } +} diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLConnectionScenario4Tests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLConnectionScenario4Tests.swift index 943f1911d9..cb4c3ac7a5 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLConnectionScenario4Tests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLConnectionScenario4Tests.swift @@ -213,7 +213,7 @@ class GraphQLConnectionScenario4Tests: XCTestCase { } let predicate = field("postID").eq(post.id) var results: List? - let result = try await Amplify.API.query(request: .list(Comment4.self, where: predicate, limit: 1)) + let result = try await Amplify.API.query(request: .list(Comment4.self, where: predicate, limit: 3000)) switch result { case .success(let comments): results = comments diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests+List.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests+List.swift index bf38826118..c42916e6e6 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests+List.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/GraphQLModelBasedTests+List.swift @@ -39,7 +39,7 @@ extension GraphQLModelBasedTests { let post = Post.keys let predicate = post.id == uuid1 || post.id == uuid2 var results: List? - let response = try await Amplify.API.query(request: .list(Post.self, where: predicate, limit: 1)) + let response = try await Amplify.API.query(request: .list(Post.self, where: predicate, limit: 3000)) guard case .success(let graphQLresponse) = response else { XCTFail("Missing successful response") diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginRESTIAMTests/RESTWithIAMIntegrationTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginRESTIAMTests/RESTWithIAMIntegrationTests.swift index 8f96b69425..b63c8d2fda 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginRESTIAMTests/RESTWithIAMIntegrationTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginRESTIAMTests/RESTWithIAMIntegrationTests.swift @@ -159,6 +159,24 @@ class RESTWithIAMIntegrationTests: XCTestCase { XCTAssertEqual(statusCode, 404) } } + + func testRestRequest_withCustomizeHeaders_succefullyOverride() async throws { + let request = RESTRequest(path: "/items", headers: ["Content-Type": "text/plain"]) + do { + _ = try await Amplify.API.get(request: request) + } catch { + guard let apiError = error as? APIError else { + XCTFail("Error should be APIError") + return + } + guard case let .httpStatusError(statusCode, _) = apiError else { + XCTFail("Error should be httpStatusError") + return + } + + XCTAssertEqual(statusCode, 403) + } + } } extension RESTWithIAMIntegrationTests: DefaultLogger { } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift index adbca6ccfb..2974af9f4d 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AWSAPICategoryPlugin+InterceptorBehaviorTests.swift @@ -14,12 +14,16 @@ class AWSAPICategoryPluginInterceptorBehaviorTests: AWSAPICategoryPluginTestBase func testAddInterceptor() throws { XCTAssertNotNil(apiPlugin.pluginConfig.endpoints[apiName]) - XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName).count, 0) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.preludeInterceptors.count, 0) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.interceptors.count, 0) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.postludeInterceptors.count, 0) let provider = BasicUserPoolTokenProvider(authService: authService) let requestInterceptor = AuthTokenURLRequestInterceptor(authTokenProvider: provider) try apiPlugin.add(interceptor: requestInterceptor, for: apiName) - XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName).count, 1) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.preludeInterceptors.count, 0) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.interceptors.count, 1) + XCTAssertEqual(apiPlugin.pluginConfig.interceptorsForEndpoint(named: apiName)?.postludeInterceptors.count, 0) } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift index 733065779e..a2f7503f01 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPICategoryPluginConfigurationTests.swift @@ -58,18 +58,22 @@ class AWSAPICategoryPluginConfigurationTests: XCTestCase { func testAddInterceptors() { let apiKeyInterceptor = APIKeyURLRequestInterceptor(apiKeyProvider: BasicAPIKeyProvider(apiKey: apiKey)) config?.addInterceptor(apiKeyInterceptor, toEndpoint: graphQLAPI) - XCTAssertEqual(config?.interceptorsForEndpoint(named: graphQLAPI).count, 1) + XCTAssertEqual(config?.interceptorsForEndpoint(named: graphQLAPI)?.preludeInterceptors.count, 0) + XCTAssertEqual(config?.interceptorsForEndpoint(named: graphQLAPI)?.interceptors.count, 1) + XCTAssertEqual(config?.interceptorsForEndpoint(named: graphQLAPI)?.postludeInterceptors.count, 0) } /// Given: multiple interceptors conforming to URLRequestInterceptor and an EndpointConfig /// When: interceptorsForEndpoint is called with the given EndpointConfig /// Then: the registered interceptors are returned - func testInterceptorsForEndpointWithConfig() throws { + func testInterceptorsForEndpointWithConfig() { let apiKeyInterceptor = APIKeyURLRequestInterceptor(apiKeyProvider: BasicAPIKeyProvider(apiKey: apiKey)) config?.addInterceptor(apiKeyInterceptor, toEndpoint: graphQLAPI) config?.addInterceptor(CustomURLInterceptor(), toEndpoint: graphQLAPI) - let interceptors = try config?.interceptorsForEndpoint(withConfig: endpointConfig!) - XCTAssertEqual(interceptors!.count, 2) + let interceptors = config?.interceptorsForEndpoint(withConfig: endpointConfig!) + XCTAssertEqual(interceptors!.preludeInterceptors.count, 0) + XCTAssertEqual(interceptors!.interceptors.count, 2) + XCTAssertEqual(interceptors!.postludeInterceptors.count, 0) } /// Given: multiple interceptors conforming to URLRequestInterceptor @@ -84,9 +88,9 @@ class AWSAPICategoryPluginConfigurationTests: XCTestCase { let interceptors = try config?.interceptorsForEndpoint(withConfig: endpointConfig!, authType: .amazonCognitoUserPools) - XCTAssertEqual(interceptors!.count, 2) - XCTAssertNotNil(interceptors![0] as? AuthTokenURLRequestInterceptor) - XCTAssertNotNil(interceptors![1] as? CustomURLInterceptor) + XCTAssertEqual(interceptors!.preludeInterceptors.count, 1) + XCTAssertNotNil(interceptors!.preludeInterceptors[0] as? AuthTokenURLRequestInterceptor) + XCTAssertNotNil(interceptors!.interceptors[1] as? CustomURLInterceptor) } /// Given: an auth interceptor conforming to URLRequestInterceptor @@ -99,8 +103,8 @@ class AWSAPICategoryPluginConfigurationTests: XCTestCase { let interceptors = try config?.interceptorsForEndpoint(withConfig: endpointConfig!, authType: .apiKey) - XCTAssertEqual(interceptors!.count, 1) - XCTAssertNotNil(interceptors![0] as? APIKeyURLRequestInterceptor) + XCTAssertEqual(interceptors!.preludeInterceptors.count, 1) + XCTAssertNotNil(interceptors!.preludeInterceptors[0] as? APIKeyURLRequestInterceptor) } // MARK: - Helpers diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift index 5cab1d4a91..64443e9ffc 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Configuration/AWSAPIEndpointInterceptorsTests.swift @@ -31,8 +31,9 @@ class AWSAPIEndpointInterceptorsTests: XCTestCase { try interceptorConfig.addAuthInterceptorsToEndpoint(endpointType: .graphQL, authConfiguration: config!) - XCTAssertEqual(interceptorConfig.interceptors.count, 1) - + XCTAssertEqual(interceptorConfig.preludeInterceptors.count, 1) + XCTAssertEqual(interceptorConfig.interceptors.count, 0) + XCTAssertEqual(interceptorConfig.postludeInterceptors.count, 0) } /// Given: an AWSAPIEndpointInterceptors @@ -44,7 +45,9 @@ class AWSAPIEndpointInterceptorsTests: XCTestCase { try interceptorConfig.addAuthInterceptorsToEndpoint(endpointType: .graphQL, authConfiguration: config!) interceptorConfig.addInterceptor(CustomInterceptor()) - XCTAssertEqual(interceptorConfig.interceptors.count, 2) + XCTAssertEqual(interceptorConfig.preludeInterceptors.count, 1) + XCTAssertEqual(interceptorConfig.interceptors.count, 1) + XCTAssertEqual(interceptorConfig.postludeInterceptors.count, 0) } func testaddMultipleAuthInterceptors() throws { @@ -69,10 +72,12 @@ class AWSAPIEndpointInterceptorsTests: XCTestCase { try interceptorConfig.addAuthInterceptorsToEndpoint(endpointType: .graphQL, authConfiguration: userPoolConfig) - XCTAssertEqual(interceptorConfig.interceptors.count, 3) - XCTAssertNotNil(interceptorConfig.interceptors[0] as? APIKeyURLRequestInterceptor) - XCTAssertNotNil(interceptorConfig.interceptors[1] as? IAMURLRequestInterceptor) - XCTAssertNotNil(interceptorConfig.interceptors[2] as? AuthTokenURLRequestInterceptor) + XCTAssertEqual(interceptorConfig.preludeInterceptors.count, 2) + XCTAssertEqual(interceptorConfig.interceptors.count, 0) + XCTAssertEqual(interceptorConfig.postludeInterceptors.count, 1) + XCTAssertNotNil(interceptorConfig.preludeInterceptors[0] as? APIKeyURLRequestInterceptor) + XCTAssertNotNil(interceptorConfig.preludeInterceptors[1] as? AuthTokenURLRequestInterceptor) + XCTAssertNotNil(interceptorConfig.postludeInterceptors[0] as? IAMURLRequestInterceptor) } // MARK: - Test Helpers diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift index ed1df2905b..25370ac15b 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/AuthTokenURLRequestInterceptorTests.swift @@ -15,7 +15,11 @@ class AuthTokenURLRequestInterceptorTests: XCTestCase { func testAuthTokenInterceptor() async throws { let mockTokenProvider = MockTokenProvider() let interceptor = AuthTokenURLRequestInterceptor(authTokenProvider: mockTokenProvider) - let request = URLRequest(url: URL(string: "http://anapiendpoint.ca")!) + let request = RESTOperationRequestUtils.constructURLRequest( + with: URL(string: "http://anapiendpoint.ca")!, + operationType: .get, + requestPayload: nil + ) guard let headers = try await interceptor.intercept(request).allHTTPHeaderFields else { XCTFail("Failed retrieving headers") diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift index 6162d63898..c65f1257ac 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSRESTOperationTests.swift @@ -96,4 +96,40 @@ class AWSRESTOperationTests: OperationTestBase { wait(for: [callbackInvoked], timeout: 1.0) } + func testRESTOperation_withCustomHeader_shouldOverrideDefaultAmplifyHeaders() throws { + let expectedHeaderValue = "text/plain" + let sentData = Data([0x00, 0x01, 0x02, 0x03]) + try setUpPluginForSingleResponse(sending: sentData, for: .rest) + + let validated = expectation(description: "Header override is validated") + try apiPlugin.add(interceptor: TestURLRequestInterceptor(validate: { request in + defer { validated.fulfill() } + return request.allHTTPHeaderFields?["Content-Type"] == expectedHeaderValue + }), for: "Valid") + + let callbackInvoked = expectation(description: "Callback was invoked") + let request = RESTRequest(apiName: "Valid", path: "/path", headers: ["Content-Type": expectedHeaderValue]) + _ = apiPlugin.get(request: request) { event in + switch event { + case .success(let data): + XCTAssertEqual(data, sentData) + case .failure(let error): + XCTFail("Unexpected failure: \(error)") + } + callbackInvoked.fulfill() + } + wait(for: [callbackInvoked, validated], timeout: 1.0) + } + +} + +fileprivate struct TestURLRequestInterceptor: URLRequestInterceptor { + let validate: (URLRequest) -> Bool + + func intercept(_ request: URLRequest) async throws -> URLRequest { + XCTAssertTrue(validate(request)) + return request + } + + } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/RESTRequestUtilsTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/RESTRequestUtilsTests.swift index 8e1e5ec1ef..7d3af18917 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/RESTRequestUtilsTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/RESTRequestUtilsTests.swift @@ -70,7 +70,6 @@ class RESTRequestUtilsTests: XCTestCase { let urlRequest = RESTOperationRequestUtils.constructURLRequest( with: url, operationType: .get, - headers: nil, requestPayload: nil ) diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Result+AsyncTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Result+AsyncTests.swift new file mode 100644 index 0000000000..5ae473b899 --- /dev/null +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Support/Utils/Result+AsyncTests.swift @@ -0,0 +1,54 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + + +import XCTest +@testable import AWSAPIPlugin + +class ResultAsyncTests: XCTestCase { + + func testFlatMapAsync_withSuccess_applyFunction() async { + func plus1(_ number: Int) async -> Int { + return number + 1 + } + + let result = Result.success(0) + let plus1Result = await result.flatMapAsync { + .success(await plus1($0)) + } + + switch plus1Result { + case .success(let plus1Result): + XCTAssertEqual(plus1Result, 1) + case .failure(let error): + XCTFail("Failed with error \(error)") + } + } + + func testFlatMapAsync_withFailure_notApplyFunction() async { + func arrayCount(_ array: [Int]) async -> Int { + return array.count + } + + + let expectedError = TestError() + let result = Result<[Int], Error>.failure(expectedError) + let count = await result.flatMapAsync { + .success(await arrayCount($0)) + } + + switch count { + case .success: + XCTFail("Should fail") + case .failure(let error): + XCTAssertTrue(error is TestError) + XCTAssertEqual(ObjectIdentifier(expectedError), ObjectIdentifier(error as! TestError)) + } + } +} + +fileprivate class TestError: Error { } From 514ce123be973e7312ac05c43a727a14830bf84d Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 14 Sep 2023 14:23:18 -0700 Subject: [PATCH 5/7] ci: add dependency review workflow (#3132) * ci: add dependency review workflow * Update dependency-review.yml --- .github/workflows/dependency-review.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/dependency-review.yml diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000000..421225b2cf --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,25 @@ +name: Dependency Review + +on: + pull_request: + branches: + - main + - v1 + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + steps: + - name: Checkout Code + uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 + with: + persist-credentials: false + + - name: Dependency Review + uses: actions/dependency-review-action@7d90b4f05fea31dde1c4a1fb3fa787e197ea93ab # v3.0.7 + with: + config-file: aws-amplify/amplify-ci-support/.github/dependency-review-config.yml@main From 5aaa0daa883236560e1832d02677d833aefc0ebf Mon Sep 17 00:00:00 2001 From: Di Wu Date: Thu, 14 Sep 2023 14:50:21 -0700 Subject: [PATCH 6/7] fix(datastore): using URLProtocol monitor auth request headers (#3221) * fix(datastore): using URLProtocol monitor multiAuth request headers * test(datastore): update integration test xcode version * test(datastore): update integration test for auth IAM * remove redundant semicolons --- .../integ_test_datastore_auth_iam.yml | 6 +- .../integ_test_datastore_multi_auth.yml | 4 +- .../AWSDataStoreAuthBaseTest.swift | 128 +++++++++++------ ...ategoryPluginIAMAuthIntegrationTests.swift | 18 ++- .../AWSDataStoreAuthBaseTest.swift | 131 ++++++++++++------ ...WSDataStoreMultiAuthCombinationTests.swift | 27 +++- ...AWSDataStoreMultiAuthSingleRuleTests.swift | 53 ++++--- ...AWSDataStoreMultiAuthThreeRulesTests.swift | 80 ++++++++--- .../AWSDataStoreMultiAuthTwoRulesTests.swift | 131 ++++++++++++------ 9 files changed, 404 insertions(+), 174 deletions(-) diff --git a/.github/workflows/integ_test_datastore_auth_iam.yml b/.github/workflows/integ_test_datastore_auth_iam.yml index 0d48a503c4..dd0da0afe2 100644 --- a/.github/workflows/integ_test_datastore_auth_iam.yml +++ b/.github/workflows/integ_test_datastore_auth_iam.yml @@ -5,12 +5,12 @@ on: permissions: id-token: write - contents: read + contents: read jobs: datastore-integration-auth-iam-test-iOS: timeout-minutes: 30 - runs-on: macos-12 + runs-on: macos-13 environment: IntegrationTest steps: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b @@ -33,6 +33,8 @@ jobs: with: project_path: ./AmplifyPlugins/DataStore/Tests/DataStoreHostApp scheme: AWSDataStorePluginAuthIAMTests + destination: 'platform=iOS Simulator,name=iPhone 14,OS=latest' + xcode_path: '/Applications/Xcode_14.3.app' datastore-integration-auth-iam-test-tvOS: timeout-minutes: 30 diff --git a/.github/workflows/integ_test_datastore_multi_auth.yml b/.github/workflows/integ_test_datastore_multi_auth.yml index 5b4815ec76..4417800b48 100644 --- a/.github/workflows/integ_test_datastore_multi_auth.yml +++ b/.github/workflows/integ_test_datastore_multi_auth.yml @@ -10,7 +10,7 @@ permissions: jobs: datastore-integration-multi-auth-test-iOS: timeout-minutes: 30 - runs-on: macos-12 + runs-on: macos-13 environment: IntegrationTest steps: - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b @@ -33,6 +33,8 @@ jobs: with: project_path: ./AmplifyPlugins/DataStore/Tests/DataStoreHostApp scheme: AWSDataStorePluginMultiAuthTests + destination: 'platform=iOS Simulator,name=iPhone 14,OS=latest' + xcode_path: '/Applications/Xcode_14.3.app' datastore-integration-multi-auth-test-tvOS: timeout-minutes: 30 diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreAuthBaseTest.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreAuthBaseTest.swift index 926acaf9f5..ca4ea667e9 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreAuthBaseTest.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreAuthBaseTest.swift @@ -23,46 +23,77 @@ struct TestUser { let password: String } -class AuthRecorderInterceptor: URLRequestInterceptor { - let awsAuthService: AWSAuthService = AWSAuthService() - var consumedAuthTypes: Set = [] - private let accessQueue = DispatchQueue(label: "com.amazon.AuthRecorderInterceptor.consumedAuthTypes") - - private func recordAuthType(_ authType: AWSAuthorizationType) { - accessQueue.async { - self.consumedAuthTypes.insert(authType) - } - } +class DataStoreAuthBaseTestURLSessionFactory: URLSessionBehaviorFactory { + static let testIdHeaderKey = "x-amplify-test" - func intercept(_ request: URLRequest) throws -> URLRequest { - guard let headers = request.allHTTPHeaderFields else { - fatalError("No headers found in request \(request)") - } + static let subject = PassthroughSubject<(String, Set), Never>() - let authHeaderValue = headers["Authorization"] - let apiKeyHeaderValue = headers["x-api-key"] + class Sniffer: URLProtocol { - if apiKeyHeaderValue != nil { - recordAuthType(.apiKey) - } + override class func canInit(with request: URLRequest) -> Bool { + guard let headers = request.allHTTPHeaderFields else { + fatalError("No headers found in request \(request)") + } + + guard let testId = headers[DataStoreAuthBaseTestURLSessionFactory.testIdHeaderKey] else { + return false + } + + var result: Set = [] + let authHeaderValue = headers["Authorization"] + let apiKeyHeaderValue = headers["x-api-key"] + + if apiKeyHeaderValue != nil { + result.insert(.apiKey) + } + + if let authHeaderValue = authHeaderValue, + case let .success(claims) = AWSAuthService().getTokenClaims(tokenString: authHeaderValue), + let cognitoIss = claims["iss"] as? String, cognitoIss.contains("cognito") { + result.insert(.amazonCognitoUserPools) + } - if let authHeaderValue = authHeaderValue, - case let .success(claims) = awsAuthService.getTokenClaims(tokenString: authHeaderValue), - let cognitoIss = claims["iss"] as? String, cognitoIss.contains("cognito") { - recordAuthType(.amazonCognitoUserPools) + if let authHeaderValue = authHeaderValue, + authHeaderValue.starts(with: "AWS4-HMAC-SHA256") { + result.insert(.awsIAM) + } + + DataStoreAuthBaseTestURLSessionFactory.subject.send((testId, result)) + return false } - if let authHeaderValue = authHeaderValue, - authHeaderValue.starts(with: "AWS4-HMAC-SHA256") { - recordAuthType(.awsIAM) + } + + class Interceptor: URLRequestInterceptor { + let testId: String? + + init(testId: String?) { + self.testId = testId } - return request + func intercept(_ request: URLRequest) async throws -> URLRequest { + if let testId { + var mutableRequest = request + mutableRequest.setValue(testId, forHTTPHeaderField: DataStoreAuthBaseTestURLSessionFactory.testIdHeaderKey) + return mutableRequest + } + return request + } } - func reset() { - consumedAuthTypes = [] + func makeSession(withDelegate delegate: URLSessionBehaviorDelegate?) -> URLSessionBehavior { + let urlSessionDelegate = delegate?.asURLSessionDelegate + let configuration = URLSessionConfiguration.default + configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 + configuration.tlsMaximumSupportedProtocolVersion = .TLSv13 + configuration.protocolClasses?.insert(Sniffer.self, at: 0) + + let session = URLSession(configuration: configuration, + delegate: urlSessionDelegate, + delegateQueue: nil) + return AmplifyURLSession(session: session) } + } class AWSDataStoreAuthBaseTest: XCTestCase { @@ -71,7 +102,6 @@ class AWSDataStoreAuthBaseTest: XCTestCase { var amplifyConfig: AmplifyConfiguration! var user1: TestUser? var user2: TestUser? - var authRecorderInterceptor: AuthRecorderInterceptor! override func setUp() { continueAfterFailure = false @@ -138,8 +168,6 @@ class AWSDataStoreAuthBaseTest: XCTestCase { self.user1 = TestUser(username: user1, password: passwordUser1) self.user2 = TestUser(username: user2, password: passwordUser2) - authRecorderInterceptor = AuthRecorderInterceptor() - amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: configFile) } catch { @@ -161,7 +189,8 @@ class AWSDataStoreAuthBaseTest: XCTestCase { func setup( withModels models: AmplifyModelRegistration, testType: DataStoreAuthTestType, - apiPluginFactory: () -> AWSAPIPlugin = { AWSAPIPlugin(sessionFactory: AmplifyURLSessionFactory()) } + testId: String? = nil, + apiPluginFactory: () -> AWSAPIPlugin = { AWSAPIPlugin(sessionFactory: DataStoreAuthBaseTestURLSessionFactory()) } ) async { do { setupCredentials(forAuthStrategy: testType) @@ -182,7 +211,10 @@ class AWSDataStoreAuthBaseTest: XCTestCase { // register auth recorder interceptor let apiName = try apiEndpointName() - try apiPlugin.add(interceptor: authRecorderInterceptor, for: apiName) + try apiPlugin.add( + interceptor: DataStoreAuthBaseTestURLSessionFactory.Interceptor(testId: testId), + for: apiName + ) await signOut() } catch { @@ -486,13 +518,27 @@ extension AWSDataStoreAuthBaseTest { await waitForExpectations([expectations.mutationDelete, expectations.mutationDeleteProcessed], timeout: 60) } - func assertUsedAuthTypes(_ authTypes: [AWSAuthorizationType], - file: StaticString = #file, - line: UInt = #line) { - XCTAssertEqual(authRecorderInterceptor.consumedAuthTypes, - Set(authTypes), - file: file, - line: line) + func assertUsedAuthTypes( + testId: String, + authTypes: [AWSAuthorizationType], + file: StaticString = #file, + line: UInt = #line + ) -> XCTestExpectation { + let expectation = expectation(description: "Should have expected auth types") + expectation.assertForOverFulfill = false + DataStoreAuthBaseTestURLSessionFactory.subject + .filter { $0.0 == testId } + .map { $0.1 } + .collect(.byTime(DispatchQueue.global(), .milliseconds(3500))) + .sink { + let result = $0.reduce(Set()) { partialResult, data in + partialResult.union(data) + } + XCTAssertEqual(result, Set(authTypes), file: file, line: line) + expectation.fulfill() + } + .store(in: &requests) + return expectation } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreCategoryPluginIAMAuthIntegrationTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreCategoryPluginIAMAuthIntegrationTests.swift index 9600830ac0..9216c48bcb 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreCategoryPluginIAMAuthIntegrationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginAuthIAMTests/AWSDataStoreCategoryPluginIAMAuthIntegrationTests.swift @@ -16,8 +16,10 @@ class AWSDataStoreCategoryPluginIAMAuthIntegrationTests: AWSDataStoreAuthBaseTes /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for authenticated users func testIAMAllowPrivate() async { + let testId = UUID().uuidString await setup(withModels: IAMPrivateModelRegistration(), - testType: .defaultAuthIAM) + testType: .defaultAuthIAM, + testId: testId) await signIn(user: user1) @@ -25,6 +27,8 @@ class AWSDataStoreCategoryPluginIAMAuthIntegrationTests: AWSDataStoreAuthBaseTes await assertDataStoreReady(expectations) + let authTypeExpectation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: TodoIAMPrivate.self, expectations) { error in @@ -38,21 +42,25 @@ class AWSDataStoreCategoryPluginIAMAuthIntegrationTests: AWSDataStoreAuthBaseTes XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpectation], timeout: 5) } /// Given: a guest user, a model with `allow public` auth rule with IAM as provider /// When: DataStore query/mutation operations are sent with IAM /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for unauthenticated users - func testIAMAllowPublic() async{ + func testIAMAllowPublic() async { + let testId = UUID().uuidString await setup(withModels: IAMPublicModelRegistration(), - testType: .defaultAuthIAM) + testType: .defaultAuthIAM, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpectation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: TodoIAMPublic.self, expectations) { error in @@ -66,7 +74,7 @@ class AWSDataStoreCategoryPluginIAMAuthIntegrationTests: AWSDataStoreAuthBaseTes XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpectation], timeout: 5) } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreAuthBaseTest.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreAuthBaseTest.swift index 318e14f054..5149203716 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreAuthBaseTest.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreAuthBaseTest.swift @@ -10,7 +10,7 @@ import XCTest import Combine import AWSDataStorePlugin import AWSPluginsCore -import AWSAPIPlugin +@testable import AWSAPIPlugin import AWSCognitoAuthPlugin #if !os(watchOS) @@ -23,55 +23,88 @@ struct TestUser { let password: String } -class AuthRecorderInterceptor: URLRequestInterceptor { - let awsAuthService: AWSAuthService = AWSAuthService() - var consumedAuthTypes: Set = [] - private let accessQueue = DispatchQueue(label: "com.amazon.AuthRecorderInterceptor.consumedAuthTypes") - private func recordAuthType(_ authType: AWSAuthorizationType) { - accessQueue.async { - self.consumedAuthTypes.insert(authType) - } - } +class DataStoreAuthBaseTestURLSessionFactory: URLSessionBehaviorFactory { + static let testIdHeaderKey = "x-amplify-test" - func intercept(_ request: URLRequest) throws -> URLRequest { - guard let headers = request.allHTTPHeaderFields else { - fatalError("No headers found in request \(request)") - } + static let subject = PassthroughSubject<(String, Set), Never>() - let authHeaderValue = headers["Authorization"] - let apiKeyHeaderValue = headers["x-api-key"] + class Sniffer: URLProtocol { - if apiKeyHeaderValue != nil { - recordAuthType(.apiKey) - } + override class func canInit(with request: URLRequest) -> Bool { + guard let headers = request.allHTTPHeaderFields else { + fatalError("No headers found in request \(request)") + } + + guard let testId = headers[DataStoreAuthBaseTestURLSessionFactory.testIdHeaderKey] else { + return false + } + + var result: Set = [] + let authHeaderValue = headers["Authorization"] + let apiKeyHeaderValue = headers["x-api-key"] + + if apiKeyHeaderValue != nil { + result.insert(.apiKey) + } + + if let authHeaderValue = authHeaderValue, + case let .success(claims) = AWSAuthService().getTokenClaims(tokenString: authHeaderValue), + let cognitoIss = claims["iss"] as? String, cognitoIss.contains("cognito") { + result.insert(.amazonCognitoUserPools) + } + + if let authHeaderValue = authHeaderValue, + authHeaderValue.starts(with: "AWS4-HMAC-SHA256") { + result.insert(.awsIAM) + } - if let authHeaderValue = authHeaderValue, - case let .success(claims) = awsAuthService.getTokenClaims(tokenString: authHeaderValue), - let cognitoIss = claims["iss"] as? String, cognitoIss.contains("cognito") { - recordAuthType(.amazonCognitoUserPools) + DataStoreAuthBaseTestURLSessionFactory.subject.send((testId, result)) + return false } - if let authHeaderValue = authHeaderValue, - authHeaderValue.starts(with: "AWS4-HMAC-SHA256") { - recordAuthType(.awsIAM) + } + + class Interceptor: URLRequestInterceptor { + let testId: String? + + init(testId: String?) { + self.testId = testId } - return request + func intercept(_ request: URLRequest) async throws -> URLRequest { + if let testId { + var mutableRequest = request + mutableRequest.setValue(testId, forHTTPHeaderField: DataStoreAuthBaseTestURLSessionFactory.testIdHeaderKey) + return mutableRequest + } + return request + } } - func reset() { - consumedAuthTypes = [] + func makeSession(withDelegate delegate: URLSessionBehaviorDelegate?) -> URLSessionBehavior { + let urlSessionDelegate = delegate?.asURLSessionDelegate + let configuration = URLSessionConfiguration.default + configuration.tlsMinimumSupportedProtocolVersion = .TLSv12 + configuration.tlsMaximumSupportedProtocolVersion = .TLSv13 + configuration.protocolClasses?.insert(Sniffer.self, at: 0) + + let session = URLSession(configuration: configuration, + delegate: urlSessionDelegate, + delegateQueue: nil) + return AmplifyURLSession(session: session) } + + } + class AWSDataStoreAuthBaseTest: XCTestCase { var requests: Set = [] var amplifyConfig: AmplifyConfiguration! var user1: TestUser? var user2: TestUser? - var authRecorderInterceptor: AuthRecorderInterceptor! override func setUp() { continueAfterFailure = false @@ -138,8 +171,6 @@ class AWSDataStoreAuthBaseTest: XCTestCase { self.user1 = TestUser(username: user1, password: passwordUser1) self.user2 = TestUser(username: user2, password: passwordUser2) - authRecorderInterceptor = AuthRecorderInterceptor() - amplifyConfig = try TestConfigHelper.retrieveAmplifyConfiguration(forResource: configFile) } catch { @@ -161,7 +192,8 @@ class AWSDataStoreAuthBaseTest: XCTestCase { func setup( withModels models: AmplifyModelRegistration, testType: DataStoreAuthTestType, - apiPluginFactory: () -> AWSAPIPlugin = { AWSAPIPlugin(sessionFactory: AmplifyURLSessionFactory()) } + testId: String? = nil, + apiPluginFactory: () -> AWSAPIPlugin = { AWSAPIPlugin(sessionFactory: DataStoreAuthBaseTestURLSessionFactory()) } ) async { do { setupCredentials(forAuthStrategy: testType) @@ -182,7 +214,10 @@ class AWSDataStoreAuthBaseTest: XCTestCase { // register auth recorder interceptor let apiName = try apiEndpointName() - try apiPlugin.add(interceptor: authRecorderInterceptor, for: apiName) + try apiPlugin.add( + interceptor: DataStoreAuthBaseTestURLSessionFactory.Interceptor(testId: testId), + for: apiName + ) await signOut() } catch { @@ -487,13 +522,27 @@ extension AWSDataStoreAuthBaseTest { await waitForExpectations([expectations.mutationDelete, expectations.mutationDeleteProcessed], timeout: 60) } - func assertUsedAuthTypes(_ authTypes: [AWSAuthorizationType], - file: StaticString = #file, - line: UInt = #line) { - XCTAssertEqual(authRecorderInterceptor.consumedAuthTypes, - Set(authTypes), - file: file, - line: line) + func assertUsedAuthTypes( + testId: String, + authTypes: [AWSAuthorizationType], + file: StaticString = #file, + line: UInt = #line + ) -> XCTestExpectation { + let expectation = expectation(description: "Should have expected auth types") + expectation.assertForOverFulfill = false + DataStoreAuthBaseTestURLSessionFactory.subject + .filter { $0.0 == testId } + .map { $0.1 } + .collect(.byTime(DispatchQueue.global(), .milliseconds(3500))) + .sink { + let result = $0.reduce(Set()) { partialResult, data in + partialResult.union(data) + } + XCTAssertEqual(result, Set(authTypes), file: file, line: line) + expectation.fulfill() + } + .store(in: &requests) + return expectation } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthCombinationTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthCombinationTests.swift index 77d24a1ff8..560c3abb70 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthCombinationTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthCombinationTests.swift @@ -18,7 +18,7 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized. func testDataStoreReadyState() async { await setup(withModels: PrivatePublicComboModels(), - testType: .multiAuth) + testType: .multiAuth) await signIn(user: user1) let expectations = makeExpectations() @@ -58,14 +58,17 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests f /// or PrivatePublicComboUPPost are sent with IAM auth for authenticated users. func testOperationsForPrivatePublicComboUPPost() async { + let testId = UUID().uuidString await setup(withModels: PrivatePublicComboModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: PrivatePublicComboUPPost.self, expectations, onFailure: { error in @@ -78,7 +81,7 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey, .amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a user signed in with API key @@ -87,8 +90,12 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests /// for PrivatePublicComboAPIPost are sent with API key auth for authenticated users. func testOperationsForPrivatePublicComboAPIPostAuthenticatedUser() async { + let testId = UUID().uuidString + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey, .amazonCognitoUserPools]) + await setup(withModels: PrivatePublicComboModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() @@ -101,12 +108,14 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { XCTFail("Error query \(error)") }) + // Mutation await assertMutations(model: PrivatePublicComboAPIPost(name: "name"), expectations) { error in XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools, .apiKey]) + + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -118,14 +127,17 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { /// PrivatePublicComboUPPost does not sync for unauthenticated users, but it does not block the other models /// from syncing and DataStore getting to a “ready” state. func testOperationsForPrivatePublicComboAPIPost() async { + let testId = UUID().uuidString await setup(withModels: PrivatePublicComboModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() // PrivatePublicComboUPPost won't sync for unauthenticated users await assertDataStoreReady(expectations, expectedModelSynced: 1) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: PrivatePublicComboAPIPost.self, expectations, onFailure: { error in @@ -137,7 +149,8 @@ class AWSDataStoreMultiAuthCombinationTests: AWSDataStoreAuthBaseTest { expectations) { error in XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + + await fulfillment(of: [authTypeExpecation], timeout: 5) } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthSingleRuleTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthSingleRuleTests.swift index cfca0c4aa7..9e4b71e10d 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthSingleRuleTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthSingleRuleTests.swift @@ -17,7 +17,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for an authenticated users func testOwnerUserPools() async { - await setup(withModels: UserPoolsOwnerModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: UserPoolsOwnerModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) @@ -25,6 +26,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: OwnerUPPost.self, expectations) { error in @@ -36,7 +39,7 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a user signed in with OIDC @@ -53,7 +56,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for an authenticated users func testGroupUserPools() async { - await setup(withModels: UserPoolsGroupModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: UserPoolsGroupModels(), testType: .multiAuth, testId: testId) // user1 is part of the "Admins" group await signIn(user: user1) @@ -61,6 +65,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: GroupUPPost.self, expectations) { error in @@ -73,14 +79,17 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a user who doesn't belong to "Admins" groups signed in with CognitoUserPools /// When: DataStore.start is called /// Then: DataStore is successfully initialized func testGroupUserPoolsWithNonAdminsUser() async { - await setup(withModels: UserPoolsGroupModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: UserPoolsGroupModels(), testType: .multiAuth, testId: testId) + + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // user2 is not part of the "Admins" group await signIn(user: user2) @@ -99,14 +108,14 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { await expectations.modelsSynced.fulfill() await assertDataStoreReady(expectations, expectedModelSynced: 0) - assertUsedAuthTypes([.amazonCognitoUserPools]) - + await fulfillment(of: [authTypeExpecation], timeout: 5) await waitForExpectations([ expectations.query, expectations.mutationSave, expectations.mutationSaveProcessed, expectations.mutationDelete, expectations.mutationDeleteProcessed], timeout: TestCommonConstants.networkTimeout) + } func testGroupOIDC() throws { @@ -119,7 +128,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for authenticated users func testPrivateUserPools() async { - await setup(withModels: UserPoolsPrivateModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: UserPoolsPrivateModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) @@ -127,6 +137,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + await assertQuerySuccess(modelType: PrivateUPPost.self, expectations) { error in XCTFail("Error query \(error)") @@ -138,7 +150,7 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a user signed in with IAM @@ -146,7 +158,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for authenticated users func testPrivateIAM() async { - await setup(withModels: IAMPrivateModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: IAMPrivateModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) @@ -154,6 +167,8 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + await assertQuerySuccess(modelType: PrivateIAMPost.self, expectations, onFailure: { error in XCTFail("Error query \(error)") @@ -165,7 +180,7 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a schema with a single IAM rule @@ -173,12 +188,15 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed for all users func testPublicIAM() async { - await setup(withModels: IAMPublicModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: IAMPublicModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: PublicIAMPost.self, expectations, onFailure: { error in @@ -191,20 +209,23 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: a schema with a single API key rule /// When: DataStore query/mutation operations are sent with API key for all users /// Then: DataStore is successfully initialized, query returns a result, /// mutation is processed - func testPublicAPIKey() async{ - await setup(withModels: APIKeyPublicModels(), testType: .multiAuth) + func testPublicAPIKey() async { + let testId = UUID().uuidString + await setup(withModels: APIKeyPublicModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) + // Query await assertQuerySuccess(modelType: PublicAPIPost.self, expectations, onFailure: { error in @@ -217,7 +238,7 @@ class AWSDataStoreMultiAuthSingleRuleTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthThreeRulesTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthThreeRulesTests.swift index ccac1b18fa..1ac6d4a481 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthThreeRulesTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthThreeRulesTests.swift @@ -23,14 +23,18 @@ class AWSDataStoreMultiAuthThreeRulesTests: AWSDataStoreAuthBaseTest { /// Note: IAM auth would likely not be used on the client, since it is unlikely that the request would /// fail with User Pool auth but succeed with IAM auth for an authenticated user. func testOwnerPrivatePublicUserPoolsIAMAPIKeyAuthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: OwnerPrivatePublicUserPoolsAPIKeyModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: OwnerPrivatePublicUPIAMAPIPost.self, expectations, @@ -44,7 +48,7 @@ class AWSDataStoreMultiAuthThreeRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -52,13 +56,17 @@ class AWSDataStoreMultiAuthThreeRulesTests: AWSDataStoreAuthBaseTest { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API Key func testOwnerPrivatePublicUserPoolsIAMAPIKeyUnauthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: OwnerPrivatePublicUserPoolsAPIKeyModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) + // Query await assertQuerySuccess(modelType: OwnerPrivatePublicUPIAMAPIPost.self, expectations, @@ -72,7 +80,7 @@ class AWSDataStoreMultiAuthThreeRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -85,14 +93,18 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito auth for authenticated users func testGroupPrivatePublicUserPoolsIAMAPIKeyAuthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: GroupPrivatePublicUserPoolsAPIKeyModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: GroupPrivatePublicUPIAMAPIPost.self, expectations, @@ -106,7 +118,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -114,13 +126,17 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API Key func testGroupPrivatePublicUserPoolsIAMAPIKeyUnauthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: GroupPrivatePublicUserPoolsAPIKeyModels(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) + // Query await assertQuerySuccess(modelType: GroupPrivatePublicUPIAMAPIPost.self, expectations) { error in @@ -133,7 +149,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -145,14 +161,18 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito func testPrivatePrivatePublicUserPoolsIAMIAMAuthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePrivatePublicUserPoolsIAMIAM(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: PrivatePrivatePublicUPIAMIAMPost.self, expectations) { error in @@ -165,7 +185,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -173,13 +193,17 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM func testPrivatePrivatePublicUserPoolsIAMIAMUnauthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePrivatePublicUserPoolsIAMIAM(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: PrivatePrivatePublicUPIAMIAMPost.self, expectations, @@ -193,7 +217,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -205,14 +229,18 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito func testPrivatePrivatePublicUserPoolsIAMApiKeyAuthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePrivatePublicUserPoolsIAMAPiKey(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: PrivatePrivatePublicUPIAMAPIPost.self, expectations, @@ -226,7 +254,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -236,13 +264,17 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// Note: IAM auth would likely not be used on the client, since it is unlikely that the request would fail with /// User Pool auth but succeed with IAM auth for an authenticated user. func testPrivatePrivatePublicUserPoolsIAMApiKeyUnauthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePrivatePublicUserPoolsIAMAPiKey(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) + // Query await assertQuerySuccess(modelType: PrivatePrivatePublicUPIAMAPIPost.self, expectations, @@ -256,7 +288,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -268,14 +300,18 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito func testPrivatePublicPublicUserPoolsAPIKeyIAMAuthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePublicPublicUserPoolsAPIKeyIAM(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) + // Query await assertQuerySuccess(modelType: PrivatePublicPublicUPAPIIAMPost.self, expectations, @@ -289,7 +325,7 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -299,13 +335,17 @@ extension AWSDataStoreMultiAuthThreeRulesTests { /// Note: API key auth would likely not be used on the client, since it is unlikely that the request would fail with /// public IAM auth but succeed with API key auth. func testPrivatePublicPublicUserPoolsAPIKeyIAMUnauthenticatedUsers() async { + let testId = UUID().uuidString await setup(withModels: PrivatePublicPublicUserPoolsAPIKeyIAM(), - testType: .multiAuth) + testType: .multiAuth, + testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: PrivatePublicPublicUPAPIIAMPost.self, expectations, @@ -319,6 +359,6 @@ extension AWSDataStoreMultiAuthThreeRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } diff --git a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthTwoRulesTests.swift b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthTwoRulesTests.swift index bb3021d4d0..29e7979f38 100644 --- a/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthTwoRulesTests.swift +++ b/AmplifyPlugins/DataStore/Tests/DataStoreHostApp/AWSDataStorePluginMultiAuthTests/AWSDataStoreMultiAuthTwoRulesTests.swift @@ -20,7 +20,8 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// When: DataStore query/mutation operations /// Then: DataStore is successfully initialized, are sent with CognitoUserPools auth for authenticated users. func testOwnerPrivateUserPoolsIAM() async { - await setup(withModels: OwnerPrivateUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: OwnerPrivateUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() @@ -28,6 +29,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) await assertQuerySuccess(modelType: OwnerPrivateUPIAMPost.self, expectations, onFailure: { error in @@ -40,7 +42,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } // MARK: - owner/public - User Pools & API Key @@ -51,13 +53,15 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with Cognito /// for authenticated users func testOwnerPublicUserPoolsAPIKeyAuthenticatedUsers() async { - await setup(withModels: OwnerPublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: OwnerPublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: OwnerPublicUPAPIPost.self, expectations, @@ -71,7 +75,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -79,12 +83,14 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API key func testOwnerPublicUserPoolsAPIKeyUnauthenticatedUsers() async { - await setup(withModels: OwnerPublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: OwnerPublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: OwnerPublicUPAPIPost.self, expectations, @@ -98,7 +104,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } // MARK: - owner/public - User Pools & IAM @@ -109,13 +115,15 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito auth for authenticated users func testOwnerPublicUserPoolsIAMAuthenticatedUsers() async { - await setup(withModels: OwnerPublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: OwnerPublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: OwnerPublicUPIAMPost.self, expectations, @@ -128,7 +136,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { expectations) { error in XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -136,12 +144,14 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM func testOwnerPublicUserPoolsIAMUnauthenticatedUsers() async { - await setup(withModels: OwnerPublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: OwnerPublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) // Query await assertQuerySuccess(modelType: OwnerPublicUPIAMPost.self, expectations, @@ -155,7 +165,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } // MARK: - owner/public - OIDC & API KEY @@ -176,14 +186,24 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with API key for unauthenticated users. func testOwnerPublicOIDCAPIUnauthenticatedUsers() async { - await setup(withModels: OwnerPublicOIDCAPIModels(), - testType: .multiAuth, - apiPluginFactory: { AWSAPIPlugin(apiAuthProviderFactory: TestAuthProviderFactory()) }) + let testId = UUID().uuidString + await setup( + withModels: OwnerPublicOIDCAPIModels(), + testType: .multiAuth, + testId: testId, + apiPluginFactory: { + AWSAPIPlugin( + sessionFactory: DataStoreAuthBaseTestURLSessionFactory(), + apiAuthProviderFactory: TestAuthProviderFactory() + ) + } + ) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: OwnerPublicOIDAPIPost.self, expectations, @@ -197,7 +217,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } // MARK: - group/private - UserPools & IAM @@ -208,13 +228,15 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito User Pools auth for authenticated users in the “Admins” group. func testGroupPrivateUserPoolsIAM() async { - await setup(withModels: GroupPrivateUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: GroupPrivateUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: GroupPrivateUPIAMPost.self, expectations, @@ -228,7 +250,7 @@ class AWSDataStoreMultiAuthTwoRulesTests: AWSDataStoreAuthBaseTest { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -240,13 +262,15 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito auth for authenticated users func testGroupPublicUserPoolsAPIKeyAuthenticatedUsers() async { - await setup(withModels: GroupPublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: GroupPublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: GroupPublicUPAPIPost.self, expectations, @@ -260,7 +284,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -268,12 +292,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API Key func testGroupPublicUserPoolsAPIKeyUnauthenticatedUsers() async { - await setup(withModels: GroupPublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: GroupPublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: GroupPublicUPAPIPost.self, expectations, @@ -287,7 +313,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -299,13 +325,15 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito auth for authenticated users func testGroupPublicUserPoolsIAMAuthenticatedUsers() async { - await setup(withModels: GroupPublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: GroupPublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: GroupPublicUPIAMPost.self, expectations, @@ -319,7 +347,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -327,12 +355,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM func testGroupPublicUserPoolsIAMUnauthenticatedUsers() async { - await setup(withModels: GroupPublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: GroupPublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) // Query await assertQuerySuccess(modelType: GroupPublicUPIAMPost.self, expectations, @@ -346,7 +376,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -358,12 +388,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent /// with Cognito auth for authenticated users func testPrivatePrivateUserPoolsIAMAuthenticatedUsers() async { - await setup(withModels: PrivateUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivateUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: PrivatePrivateUPIAMPost.self, expectations, @@ -377,7 +409,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -389,13 +421,15 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with Cognito /// for authenticated users func testPrivatePublicUserPoolsAPIKeyAuthenticatedUsers() async { - await setup(withModels: PrivatePublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivatePublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: PrivatePublicUPAPIPost.self, expectations, @@ -409,7 +443,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -417,12 +451,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API key func testPrivatePublicUserPoolsAPIKeyUnauthenticatedUsers() async { - await setup(withModels: PrivatePublicUserPoolsAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivatePublicUserPoolsAPIModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: PrivatePublicUPAPIPost.self, expectations, @@ -436,7 +472,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -449,13 +485,15 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with Cognito /// for authenticated users func testPrivatePublicUserPoolsIAMAuthenticatedUsers() async{ - await setup(withModels: PrivatePublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivatePublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.amazonCognitoUserPools]) // Query await assertQuerySuccess(modelType: PrivatePublicUPIAMPost.self, expectations, @@ -469,7 +507,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.amazonCognitoUserPools]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } /// Given: an unauthenticated user @@ -477,12 +515,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM func testPrivatePublicUserPoolsIAMUnauthenticatedUsers() async { - await setup(withModels: PrivatePublicUserPoolsIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivatePublicUserPoolsIAMModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) // Query await assertQuerySuccess(modelType: PrivatePublicUPIAMPost.self, expectations, @@ -496,7 +536,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) } } @@ -509,13 +549,15 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM /// for authenticated users func testPrivatePublicIAMAPIKeyAuthenticatedUsers() async { - await setup(withModels: PrivatePublicIAMAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PrivatePublicIAMAPIModels(), testType: .multiAuth, testId: testId) await signIn(user: user1) let expectations = makeExpectations() await assertDataStoreReady(expectations) + let authTypeExpecation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) // Query await assertQuerySuccess(modelType: PrivatePublicIAMAPIPost.self, expectations, @@ -529,7 +571,8 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpecation], timeout: 5) + } /// Given: an unauthenticated user @@ -537,12 +580,14 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// Then: /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with API Key func testPrivatePublicIAMAPIKeyUnauthenticatedUsers() async { - await setup(withModels: PrivatePublicIAMAPIModels(), testType: .multiAuth) + let testId = UUID().uuidString + + await setup(withModels: PrivatePublicIAMAPIModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) - + let authTypeExpectations = assertUsedAuthTypes(testId: testId, authTypes: [.apiKey]) // Query await assertQuerySuccess(modelType: PrivatePublicIAMAPIPost.self, expectations, @@ -556,7 +601,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.apiKey]) + await fulfillment(of: [authTypeExpectations], timeout: 5) } /// Given: an unauthenticated user @@ -565,12 +610,16 @@ extension AWSDataStoreMultiAuthTwoRulesTests { /// - DataStore is successfully initialized, sync/mutation/subscription network requests are sent with IAM /// for unauthenticated users func testPublicPublicAPIKeyIAMUnauthenticatedUsers() async { - await setup(withModels: PublicPublicAPIIAMModels(), testType: .multiAuth) + let testId = UUID().uuidString + await setup(withModels: PublicPublicAPIIAMModels(), testType: .multiAuth, testId: testId) let expectations = makeExpectations() await assertDataStoreReady(expectations) + + let authTypeExpectation = assertUsedAuthTypes(testId: testId, authTypes: [.awsIAM]) + // Query await assertQuerySuccess(modelType: PublicPublicIAMAPIPost.self, expectations, @@ -584,7 +633,7 @@ extension AWSDataStoreMultiAuthTwoRulesTests { XCTFail("Error mutation \(error)") } - assertUsedAuthTypes([.awsIAM]) + await fulfillment(of: [authTypeExpectation], timeout: 5) } } From 9a05bdd23a583832b3b1b8fad4f23797dd501162 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Fri, 15 Sep 2023 09:33:23 -0700 Subject: [PATCH 7/7] fix(datastore): use unwrapped storageEngine to perform datastore operations (#3204) * fix(datastore): use unwrapped storageEngine to perform datastore operations * update datastore local only operations with seperate init method --- .../xcshareddata/swiftpm/Package.resolved | 90 ------- ...ataStorePlugin+DataStoreBaseBehavior.swift | 230 ++++++++++-------- ...orePlugin+DataStoreSubscribeBehavior.swift | 47 ++-- .../AWSDataStorePlugin.swift | 31 ++- 4 files changed, 183 insertions(+), 215 deletions(-) diff --git a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 2ef4d7901c..580630742c 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/AmplifyPlugins/API/Tests/APIHostApp/APIHostApp.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,41 +1,5 @@ { "pins" : [ - { - "identity" : "amplify-swift-utils-notifications", - "kind" : "remoteSourceControl", - "location" : "https://github.com/aws-amplify/amplify-swift-utils-notifications.git", - "state" : { - "revision" : "f970384ad1035732f99259255cd2f97564807e41", - "version" : "1.1.0" - } - }, - { - "identity" : "aws-appsync-realtime-client-ios", - "kind" : "remoteSourceControl", - "location" : "https://github.com/aws-amplify/aws-appsync-realtime-client-ios.git", - "state" : { - "revision" : "b036e83716789c13a3480eeb292b70caa54114f2", - "version" : "3.1.0" - } - }, - { - "identity" : "aws-crt-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-crt-swift", - "state" : { - "revision" : "6feec6c3787877807aa9a00fad09591b96752376", - "version" : "0.6.1" - } - }, - { - "identity" : "aws-sdk-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/aws-sdk-swift.git", - "state" : { - "revision" : "24bae88a2391fe75da8a940a544d1ef6441f5321", - "version" : "0.13.0" - } - }, { "identity" : "cwlcatchexception", "kind" : "remoteSourceControl", @@ -53,60 +17,6 @@ "revision" : "a23ded2c91df9156628a6996ab4f347526f17b6b", "version" : "2.1.2" } - }, - { - "identity" : "smithy-swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/awslabs/smithy-swift", - "state" : { - "revision" : "7b28da158d92cd06a3549140d43b8fbcf64a94a6", - "version" : "0.15.0" - } - }, - { - "identity" : "sqlite.swift", - "kind" : "remoteSourceControl", - "location" : "https://github.com/stephencelis/SQLite.swift.git", - "state" : { - "revision" : "5f5ad81ac0d0a0f3e56e39e646e8423c617df523", - "version" : "0.13.2" - } - }, - { - "identity" : "starscream", - "kind" : "remoteSourceControl", - "location" : "https://github.com/daltoniam/Starscream", - "state" : { - "revision" : "df8d82047f6654d8e4b655d1b1525c64e1059d21", - "version" : "4.0.4" - } - }, - { - "identity" : "swift-collections", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-collections", - "state" : { - "revision" : "937e904258d22af6e447a0b72c0bc67583ef64a2", - "version" : "1.0.4" - } - }, - { - "identity" : "swift-log", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-log.git", - "state" : { - "revision" : "32e8d724467f8fe623624570367e3d50c5638e46", - "version" : "1.5.2" - } - }, - { - "identity" : "xmlcoder", - "kind" : "remoteSourceControl", - "location" : "https://github.com/MaxDesiatov/XMLCoder.git", - "state" : { - "revision" : "b1e944cbd0ef33787b13f639a5418d55b3bed501", - "version" : "0.17.1" - } } ], "version" : 2 diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift index e6cfcffd1e..36eeeca8c2 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreBaseBehavior.swift @@ -22,54 +22,39 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { try await save(model, modelSchema: model.schema, where: condition) } - public func save(_ model: M, - modelSchema: ModelSchema, - where condition: QueryPredicate? = nil, - completion: @escaping DataStoreCallback) { + public func save( + _ model: M, + modelSchema: ModelSchema, + where condition: QueryPredicate? = nil, + completion: @escaping DataStoreCallback + ) { log.verbose("Saving: \(model) with condition: \(String(describing: condition))") - initStorageEngineAndStartSync() - - // TODO: Refactor this into a proper request/result where the result includes metadata like the derived - // mutation type - let modelExists: Bool - do { - guard let engine = storageEngine as? StorageEngine else { - throw DataStoreError.configuration("Unable to get storage adapter", - "") - } - modelExists = try engine.storageAdapter.exists(modelSchema, - withIdentifier: model.identifier(schema: modelSchema), - predicate: nil) - } catch { - if let dataStoreError = error as? DataStoreError { - completion(.failure(dataStoreError)) - return - } - - let dataStoreError = DataStoreError.invalidOperation(causedBy: error) - completion(.failure(dataStoreError)) - return + let prepareSaveResult = initStorageEngineAndTryStartSync().flatMap { storageEngineBehavior in + mutationTypeOfModel(model, modelSchema: modelSchema, storageEngine: storageEngineBehavior) + .map { (storageEngineBehavior, $0) } } - let mutationType = modelExists ? MutationEvent.MutationType.update : .create - - let publishingCompletion: DataStoreCallback = { result in - switch result { - case .success(let model): - // TODO: Differentiate between save & update - // TODO: Handle errors from mutation event creation - self.publishMutationEvent(from: model, modelSchema: modelSchema, mutationType: mutationType) - case .failure: - break + switch prepareSaveResult { + case .success(let (storageEngineBehavior, mutationType)): + storageEngineBehavior.save( + model, + modelSchema: modelSchema, + condition: condition, + eagerLoad: configuration.isEagerLoad + ) { result in + switch result { + case .success(let model): + // TODO: Differentiate between save & update + // TODO: Handle errors from mutation event creation + self.publishMutationEvent(from: model, modelSchema: modelSchema, mutationType: mutationType) + case .failure: + break + } + completion(result) } - - completion(result) + case .failure(let error): + completion(.failure(error)) } - storageEngine.save(model, - modelSchema: modelSchema, - condition: condition, - eagerLoad: configuration.isEagerLoad, - completion: publishingCompletion) } public func save(_ model: M, @@ -88,7 +73,6 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { public func query(_ modelType: M.Type, byId id: String, completion: DataStoreCallback) { - initStorageEngineAndStartSync() let predicate: QueryPredicate = field("id") == id query(modelType, where: predicate, paginate: .firstResult) { switch $0 { @@ -153,7 +137,6 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { modelSchema: ModelSchema, identifier: ModelIdentifierProtocol, completion: DataStoreCallback) { - initStorageEngineAndStartSync() query(modelType, modelSchema: modelSchema, where: identifier.predicate, @@ -206,20 +189,28 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { paginate: paginationInput) } - public func query(_ modelType: M.Type, - modelSchema: ModelSchema, - where predicate: QueryPredicate? = nil, - sort sortInput: [QuerySortDescriptor]? = nil, - paginate paginationInput: QueryPaginationInput? = nil, - completion: DataStoreCallback<[M]>) { - initStorageEngineAndStartSync() - storageEngine.query(modelType, - modelSchema: modelSchema, - predicate: predicate, - sort: sortInput, - paginationInput: paginationInput, - eagerLoad: configuration.isEagerLoad, - completion: completion) + public func query( + _ modelType: M.Type, + modelSchema: ModelSchema, + where predicate: QueryPredicate? = nil, + sort sortInput: [QuerySortDescriptor]? = nil, + paginate paginationInput: QueryPaginationInput? = nil, + completion: DataStoreCallback<[M]> + ) { + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + storageEngineBehavior.query( + modelType, + modelSchema: modelSchema, + predicate: predicate, + sort: sortInput, + paginationInput: paginationInput, + eagerLoad: configuration.isEagerLoad, + completion: completion + ) + case .failure(let error): + completion(.failure(error)) + } } public func query(_ modelType: M.Type, @@ -249,12 +240,17 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { withId id: String, where predicate: QueryPredicate? = nil) async throws { try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - initStorageEngineAndStartSync() - storageEngine.delete(modelType, modelSchema: modelSchema, withId: id, condition: predicate) { result in - self.onDeleteCompletion(result: result, modelSchema: modelSchema) { result in - continuation.resume(with: result) + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + storageEngineBehavior.delete(modelType, modelSchema: modelSchema, withId: id, condition: predicate) { result in + self.onDeleteCompletion(result: result, modelSchema: modelSchema) { result in + continuation.resume(with: result) + } } + case .failure(let error): + continuation.resume(with: .failure(error)) } + } } public func delete(_ modelType: M.Type, @@ -304,15 +300,24 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { identifier: ModelIdentifierProtocol, where predicate: QueryPredicate?, completion: @escaping DataStoreCallback) where M: ModelIdentifiable { - initStorageEngineAndStartSync() - storageEngine.delete(modelType, - modelSchema: modelSchema, - withIdentifier: identifier, - condition: predicate) { result in - self.onDeleteCompletion(result: result, - modelSchema: modelSchema, - completion: completion) - } + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + storageEngineBehavior.delete( + modelType, + modelSchema: modelSchema, + withIdentifier: identifier, + condition: predicate + ) { result in + self.onDeleteCompletion( + result: result, + modelSchema: modelSchema, + completion: completion + ) + } + case .failure(let error): + completion(.failure(error)) + } + } private func deleteByIdentifier(_ modelType: M.Type, @@ -341,13 +346,20 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { modelSchema: ModelSchema, where predicate: QueryPredicate? = nil, completion: @escaping DataStoreCallback) { - initStorageEngineAndStartSync() - storageEngine.delete(type(of: model), - modelSchema: modelSchema, - withIdentifier: model.identifier(schema: modelSchema), - condition: predicate) { result in - self.onDeleteCompletion(result: result, modelSchema: modelSchema, completion: completion) + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + storageEngineBehavior.delete( + type(of: model), + modelSchema: modelSchema, + withIdentifier: model.identifier(schema: modelSchema), + condition: predicate + ) { result in + self.onDeleteCompletion(result: result, modelSchema: modelSchema, completion: completion) + } + case .failure(let error): + completion(.failure(error)) } + } public func delete(_ model: M, @@ -375,22 +387,26 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { modelSchema: ModelSchema, where predicate: QueryPredicate, completion: @escaping DataStoreCallback) { - initStorageEngineAndStartSync() - let onCompletion: DataStoreCallback<[M]> = { result in - switch result { - case .success(let models): - for model in models { - self.publishMutationEvent(from: model, modelSchema: modelSchema, mutationType: .delete) + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + storageEngineBehavior.delete( + modelType, + modelSchema: modelSchema, + filter: predicate + ) { result in + switch result { + case .success(let models): + for model in models { + self.publishMutationEvent(from: model, modelSchema: modelSchema, mutationType: .delete) + } + completion(.emptyResult) + case .failure(let error): + completion(.failure(error)) } - completion(.emptyResult) - case .failure(let error): - completion(.failure(error)) } + case .failure(let error): + completion(.failure(error)) } - storageEngine.delete(modelType, - modelSchema: modelSchema, - filter: predicate, - completion: onCompletion) } public func delete(_ modelType: M.Type, @@ -404,10 +420,9 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { } public func start(completion: @escaping DataStoreCallback) { - initStorageEngineAndStartSync { result in - self.queue.async { - completion(result) - } + let result = initStorageEngineAndStartSync().map { _ in () } + self.queue.async { + completion(result) } } @@ -498,6 +513,31 @@ extension AWSDataStorePlugin: DataStoreBaseBehavior { } } + private func mutationTypeOfModel( + _ model: M, + modelSchema: ModelSchema, + storageEngine: StorageEngineBehavior + ) -> Result { + let modelExists: Bool + do { + guard let engine = storageEngine as? StorageEngine else { + throw DataStoreError.configuration("Unable to get storage adapter", "") + } + modelExists = try engine.storageAdapter.exists(modelSchema, + withIdentifier: model.identifier(schema: modelSchema), + predicate: nil) + } catch { + if let dataStoreError = error as? DataStoreError { + return .failure(dataStoreError) + } + + let dataStoreError = DataStoreError.invalidOperation(causedBy: error) + return .failure(dataStoreError) + } + + return .success(modelExists ? MutationEvent.MutationType.update : .create) + } + private func publishMutationEvent(from model: M, modelSchema: ModelSchema, mutationType: MutationEvent.MutationType) { diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreSubscribeBehavior.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreSubscribeBehavior.swift index 21784c71b1..76302653f2 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreSubscribeBehavior.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin+DataStoreSubscribeBehavior.swift @@ -11,7 +11,7 @@ import Combine extension AWSDataStorePlugin: DataStoreSubscribeBehavior { public var publisher: AnyPublisher { - initStorageEngineAndStartSync() + _ = initStorageEngineAndStartSync() // Force-unwrapping: The optional 'dataStorePublisher' is expected // to exist for deployment targets >=iOS13.0 return dataStorePublisher!.publisher @@ -29,27 +29,32 @@ extension AWSDataStorePlugin: DataStoreSubscribeBehavior { public func observeQuery(for modelType: M.Type, where predicate: QueryPredicate?, sort sortInput: QuerySortInput?) -> AmplifyAsyncThrowingSequence> { - initStorageEngineAndStartSync() - - let modelSchema = modelType.schema - guard let dataStorePublisher = dataStorePublisher else { - return Fatal.preconditionFailure("`dataStorePublisher` is expected to exist for deployment targets >=iOS13.0") - } - guard let dispatchedModelSyncedEvent = dispatchedModelSyncedEvents[modelSchema.name] else { - return Fatal.preconditionFailure("`dispatchedModelSyncedEvent` is expected to exist for \(modelSchema.name)") + switch initStorageEngineAndTryStartSync() { + case .success(let storageEngineBehavior): + let modelSchema = modelType.schema + guard let dataStorePublisher = dataStorePublisher else { + return Fatal.preconditionFailure("`dataStorePublisher` is expected to exist for deployment targets >=iOS13.0") + } + guard let dispatchedModelSyncedEvent = dispatchedModelSyncedEvents[modelSchema.name] else { + return Fatal.preconditionFailure("`dispatchedModelSyncedEvent` is expected to exist for \(modelSchema.name)") + } + let request = ObserveQueryRequest(options: []) + let taskRunner = ObserveQueryTaskRunner(request: request, + modelType: modelType, + modelSchema: modelType.schema, + predicate: predicate, + sortInput: sortInput?.asSortDescriptors(), + storageEngine: storageEngineBehavior, + dataStorePublisher: dataStorePublisher, + dataStoreConfiguration: configuration.pluginConfiguration, + dispatchedModelSyncedEvent: dispatchedModelSyncedEvent, + dataStoreStatePublisher: dataStoreStateSubject.eraseToAnyPublisher()) + return taskRunner.sequence + case .failure(let error): + return Fatal.preconditionFailure("Unable to get storage adapter \(error.localizedDescription)") } - let request = ObserveQueryRequest(options: []) - let taskRunner = ObserveQueryTaskRunner(request: request, - modelType: modelType, - modelSchema: modelType.schema, - predicate: predicate, - sortInput: sortInput?.asSortDescriptors(), - storageEngine: storageEngine, - dataStorePublisher: dataStorePublisher, - dataStoreConfiguration: configuration.pluginConfiguration, - dispatchedModelSyncedEvent: dispatchedModelSyncedEvent, - dataStoreStatePublisher: dataStoreStateSubject.eraseToAnyPublisher()) - return taskRunner.sequence + + } } diff --git a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin.swift b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin.swift index 1129d719c8..c929c872fe 100644 --- a/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin.swift +++ b/AmplifyPlugins/DataStore/Sources/AWSDataStorePlugin/AWSDataStorePlugin.swift @@ -145,23 +145,36 @@ final public class AWSDataStorePlugin: DataStoreCategoryPlugin { } /// Initializes the underlying storage engine and starts the syncing process - /// - Parameter completion: completion handler called with a success if the sync process started - /// or with a DataStoreError in case of failure - func initStorageEngineAndStartSync(completion: @escaping DataStoreCallback = { _ in }) { + /// - Returns: The StorageEngineBehavior instance just get initialized + func initStorageEngineAndStartSync() -> Result { storageEngineInitQueue.sync { - completion( - initStorageEngine().flatMap { $0.startSync() }.flatMap { result in + initStorageEngine().flatMap { storageEngine in + storageEngine.startSync().flatMap { result in switch result { case .alreadyInitialized: - return .successfulVoid + return .success(storageEngine) case .successfullyInitialized: - self.dataStoreStateSubject.send(.start(storageEngine: self.storageEngine)) - return .successfulVoid + self.dataStoreStateSubject.send(.start(storageEngine: storageEngine)) + return .success(storageEngine) case .failure(let error): return .failure(error) } } - ) + } + } + } + + /// Initializes the underlying storage engine and try starts the syncing process + /// If the start sync process failed due to missing other plugin configurations, we recover from failure for local only operations. + /// - Returns: The StorageEngineBehavior instance just get initialized + func initStorageEngineAndTryStartSync() -> Result { + initStorageEngineAndStartSync().flatMapError { error in + switch error { + case .configuration: + return .success(storageEngine) + default: + return .failure(error) + } } }