From 6f95db94d766921c92fef5631cb9b867899bdeca Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 12 Sep 2023 12:56:51 -0700 Subject: [PATCH 01/11] 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 02/11] 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 03/11] 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 04/11] 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) + } } } From 7fdd7f0456c2766c2a3217374b7b07fc22999db0 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 18 Sep 2023 20:20:19 +0000 Subject: [PATCH 05/11] chore: release 2.17.2 [skip ci] --- .../ServiceConfiguration/AmplifyAWSServiceConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift index 39e6384356..cf35ec8a8d 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift @@ -16,7 +16,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.17.1" + public static let amplifyVersion = "2.17.2" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 94a3f341254a7e9c5b78ef2f1a9fe0ec289f0305 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Mon, 18 Sep 2023 20:25:08 +0000 Subject: [PATCH 06/11] chore: finalize release 2.17.2 [skip ci] --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b454fa06b3..6bfe1064ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## 2.17.2 (2023-09-18) + +### Bug Fixes + +- **datastore**: use unwrapped storageEngine to perform datastore operations (#3204) +- **datastore**: using URLProtocol monitor auth request headers (#3221) +- **api**: change request interceptors applying logic (#3190) +- **logging**: fix issue with logger namespace not being set (#3213) +- **datastore**: wrap failures to result when applying remote update events (#3187) + ## 2.17.1 (2023-09-05) ### Bug Fixes From f1b6fa11056cefc642e03eb48ec9e82711f9e733 Mon Sep 17 00:00:00 2001 From: Ian Saultz <52051793+atierian@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:10:08 -0400 Subject: [PATCH 07/11] change swift-tools-version to 5.7 (#3193) --- Package.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Package.swift b/Package.swift index a71dc5c8d4..343b7b515a 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.6 +// swift-tools-version:5.7 // The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription From 26f1f21c455b977240a7a9c3f7b8e9e7f637d955 Mon Sep 17 00:00:00 2001 From: Sebastian Villena <97059974+ruisebas@users.noreply.github.com> Date: Wed, 20 Sep 2023 09:44:25 -0400 Subject: [PATCH 08/11] feat: Setting mininum watchOS version to 9 (#3229) --- Package.swift | 2 +- README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 343b7b515a..b3b5351397 100644 --- a/Package.swift +++ b/Package.swift @@ -6,7 +6,7 @@ let platforms: [SupportedPlatform] = [ .iOS(.v13), .macOS(.v10_15), .tvOS(.v13), - .watchOS(.v7) + .watchOS(.v9) ] let dependencies: [Package.Dependency] = [ .package(url: "https://github.com/awslabs/aws-sdk-swift.git", exact: "0.13.0"), diff --git a/README.md b/README.md index ec8700f015..1111369176 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ The Amplify Library for Swift is layered on the [AWS SDK for Swift](https://aws. | iOS | 13+ | GA | | macOS | 10.15+ | GA | | tvOS | 13+ | GA | -| watchOS | 7+ | GA | +| watchOS | 9+ | GA | | visionOS | 1+ | Preview* | > To use Amplify Swift with visionOS, you'll need to target the `visionos-preview` branch. From a31af6174ecd3fa70542d70de54dd590d1d09aed Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Thu, 21 Sep 2023 03:17:04 +0000 Subject: [PATCH 09/11] chore: release 2.18.0 [skip ci] --- .../ServiceConfiguration/AmplifyAWSServiceConfiguration.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift index cf35ec8a8d..e545fb2425 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift +++ b/AmplifyPlugins/Core/AWSPluginsCore/ServiceConfiguration/AmplifyAWSServiceConfiguration.swift @@ -16,7 +16,7 @@ import Amplify public class AmplifyAWSServiceConfiguration { /// - Tag: AmplifyAWSServiceConfiguration.amplifyVersion - public static let amplifyVersion = "2.17.2" + public static let amplifyVersion = "2.18.0" /// - Tag: AmplifyAWSServiceConfiguration.platformName public static let platformName = "amplify-swift" From 999a93e598ce16fc37618e81845dee8b9d053ca5 Mon Sep 17 00:00:00 2001 From: aws-amplify-ops Date: Thu, 21 Sep 2023 03:23:06 +0000 Subject: [PATCH 10/11] chore: finalize release 2.18.0 [skip ci] --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bfe1064ed..df54f68eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 2.18.0 (2023-09-21) + +### Features + +- Setting mininum watchOS version to 9 (#3229) + ## 2.17.2 (2023-09-18) ### Bug Fixes From 6fa88d4734e127e8f4da5e73da364496076bc4a2 Mon Sep 17 00:00:00 2001 From: Ian Saultz <52051793+atierian@users.noreply.github.com> Date: Thu, 21 Sep 2023 14:00:19 -0400 Subject: [PATCH 11/11] fix(core): add Foundation HTTP client for watchOS / tvOS (#3230) --- .../AWSCognitoAuthPlugin+Configure.swift | 27 ++++++- .../ClientRuntimeFoundationBridge.swift | 64 +++++++++++++++ .../FoundationClientEngine.swift | 37 +++++++++ .../FoundationClientEngineError.swift | 81 +++++++++++++++++++ .../AWSLocationGeoPlugin+Configure.swift | 11 ++- .../PinpointClient+CredentialsProvider.swift | 7 ++ .../Utils/PinpointRequestsRegistry.swift | 5 +- ...WSCloudWatchLoggingSessionController.swift | 9 +++ .../Predictions/AWSPredictionsService.swift | 31 +++++++ .../Service/Storage/AWSS3StorageService.swift | 20 ++++- 10 files changed, 285 insertions(+), 7 deletions(-) create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift create mode 100644 AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift diff --git a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift index 99895a0d3b..ae80ff67f2 100644 --- a/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift +++ b/AmplifyPlugins/Auth/Sources/AWSCognitoAuthPlugin/AWSCognitoAuthPlugin+Configure.swift @@ -12,6 +12,7 @@ import AWSCognitoIdentity import AWSCognitoIdentityProvider import AWSPluginsCore import ClientRuntime +@_spi(FoundationClientEngine) import AWSPluginsCore extension AWSCognitoAuthPlugin { @@ -92,9 +93,23 @@ extension AWSCognitoAuthPlugin { ) if var httpClientEngineProxy = httpClientEngineProxy { - let sdkEngine = configuration.httpClientEngine - httpClientEngineProxy.target = sdkEngine + let httpClientEngine: HttpClientEngine + #if os(iOS) || os(macOS) + // networking goes through CRT + httpClientEngine = configuration.httpClientEngine + #else + // networking goes through Foundation + httpClientEngine = FoundationClientEngine() + #endif + httpClientEngineProxy.target = httpClientEngine configuration.httpClientEngine = httpClientEngineProxy + } else { + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif } return CognitoIdentityProviderClient(config: configuration) @@ -110,6 +125,14 @@ extension AWSCognitoAuthPlugin { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: identityPoolConfig.region ) + + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif + return CognitoIdentityClient(config: configuration) default: fatalError() diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift new file mode 100644 index 0000000000..b503ed5f3d --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/ClientRuntimeFoundationBridge.swift @@ -0,0 +1,64 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime + +extension Foundation.URLRequest { + init(sdkRequest: ClientRuntime.SdkHttpRequest) throws { + guard let url = sdkRequest.endpoint.url else { + throw FoundationClientEngineError.invalidRequestURL(sdkRequest: sdkRequest) + } + self.init(url: url) + httpMethod = sdkRequest.method.rawValue + + for header in sdkRequest.headers.headers { + for value in header.value { + addValue(value, forHTTPHeaderField: header.name) + } + } + + switch sdkRequest.body { + case .data(let data): httpBody = data + case .stream(let stream): httpBody = stream.toBytes().getData() + case .none: break + } + } +} + +extension ClientRuntime.HttpResponse { + private static func headers( + from allHeaderFields: [AnyHashable: Any] + ) -> ClientRuntime.Headers { + var headers = Headers() + for header in allHeaderFields { + switch (header.key, header.value) { + case let (key, value) as (String, String): + headers.add(name: key, value: value) + case let (key, values) as (String, [String]): + headers.add(name: key, values: values) + default: continue + } + } + return headers + } + + convenience init(httpURLResponse: HTTPURLResponse, data: Data) throws { + let headers = Self.headers(from: httpURLResponse.allHeaderFields) + let body = HttpBody.stream(ByteStream.from(data: data)) + + guard let statusCode = HttpStatusCode(rawValue: httpURLResponse.statusCode) else { + // This shouldn't happen, but `HttpStatusCode` only exposes a failable + // `init`. The alternative here is force unwrapping, but we can't + // make the decision to crash here on behalf on consuming applications. + throw FoundationClientEngineError.unexpectedStatusCode( + statusCode: httpURLResponse.statusCode + ) + } + self.init(headers: headers, body: body, statusCode: statusCode) + } +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift new file mode 100644 index 0000000000..3bda16f9c1 --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngine.swift @@ -0,0 +1,37 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import ClientRuntime +import Amplify + +@_spi(FoundationClientEngine) +public struct FoundationClientEngine: HttpClientEngine { + public func execute(request: ClientRuntime.SdkHttpRequest) async throws -> ClientRuntime.HttpResponse { + let urlRequest = try URLRequest(sdkRequest: request) + + let (data, response) = try await URLSession.shared.data(for: urlRequest) + guard let httpURLResponse = response as? HTTPURLResponse else { + // This shouldn't be necessary because we're only making HTTP requests. + // `URLResponse` should always be a `HTTPURLResponse`. + // But to refrain from crashing consuming applications, we're throwing here. + throw FoundationClientEngineError.invalidURLResponse(urlRequest: response) + } + + let httpResponse = try HttpResponse( + httpURLResponse: httpURLResponse, + data: data + ) + + return httpResponse + } + + public init() {} + + /// no-op + func close() async {} +} diff --git a/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift new file mode 100644 index 0000000000..09e6df49ef --- /dev/null +++ b/AmplifyPlugins/Core/AWSPluginsCore/Utils/CustomHttpClientEngine/FoundationClientEngineError.swift @@ -0,0 +1,81 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import Amplify +import ClientRuntime + +struct FoundationClientEngineError: AmplifyError { + let errorDescription: ErrorDescription + let recoverySuggestion: RecoverySuggestion + let underlyingError: Error? + + // protocol requirement + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } +} + +extension FoundationClientEngineError { + init( + errorDescription: ErrorDescription, + recoverySuggestion: RecoverySuggestion, + error: Error? + ) { + self.errorDescription = errorDescription + self.recoverySuggestion = recoverySuggestion + self.underlyingError = error + } + + static func invalidRequestURL(sdkRequest: ClientRuntime.SdkHttpRequest) -> Self { + .init( + errorDescription: """ + The SdkHttpRequest generated by ClientRuntime doesn't include a valid URL + - \(sdkRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func invalidURLResponse(urlRequest: URLResponse) -> Self { + .init( + errorDescription: """ + The URLResponse received is not an HTTPURLResponse + - \(urlRequest) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } + + static func unexpectedStatusCode(statusCode: Int) -> Self { + .init( + errorDescription: """ + The status code received isn't a valid `HttpStatusCode` value. + - status code: \(statusCode) + """, + recoverySuggestion: """ + Please open an issue at https://github.com/aws-amplify/amplify-swift + with the contents of this error message. + """, + error: nil + ) + } +} diff --git a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift index be53fc1f45..c210cdd23f 100644 --- a/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift +++ b/AmplifyPlugins/Geo/Sources/AWSLocationGeoPlugin/AWSLocationGeoPlugin+Configure.swift @@ -5,10 +5,10 @@ // SPDX-License-Identifier: Apache-2.0 // +import Foundation import Amplify import AWSPluginsCore -import Foundation - +@_spi(FoundationClientEngine) import AWSPluginsCore import AWSLocation import AWSClientRuntime @@ -35,6 +35,13 @@ extension AWSLocationGeoPlugin { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + serviceConfiguration.httpClientEngine = FoundationClientEngine() + #endif + let location = LocationClient(config: serviceConfiguration) let locationService = AWSLocationAdapter(location: location) diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift index f3ff0b4f70..447d092b61 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Extensions/PinpointClient+CredentialsProvider.swift @@ -8,6 +8,7 @@ import AWSClientRuntime import AWSPluginsCore import AWSPinpoint +@_spi(FoundationClientEngine) import AWSPluginsCore extension PinpointClient { convenience init(region: String, credentialsProvider: CredentialsProvider) throws { @@ -16,6 +17,12 @@ extension PinpointClient { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif PinpointRequestsRegistry.shared.setCustomHttpEngine(on: configuration) self.init(config: configuration) } diff --git a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift index 8c0aeba4a3..7bcd61a0a5 100644 --- a/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift +++ b/AmplifyPlugins/Internal/Sources/InternalAWSPinpoint/Support/Utils/PinpointRequestsRegistry.swift @@ -20,9 +20,10 @@ import ClientRuntime } nonisolated func setCustomHttpEngine(on configuration: PinpointClient.PinpointClientConfiguration) { - let oldHttpClientEngine = configuration.httpClientEngine + let baseHTTPClientEngine = configuration.httpClientEngine + configuration.httpClientEngine = CustomPinpointHttpClientEngine( - httpClientEngine: oldHttpClientEngine + httpClientEngine: baseHTTPClientEngine ) } diff --git a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift index 18d2483ffb..b12e9c134e 100644 --- a/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift +++ b/AmplifyPlugins/Logging/Sources/AWSCloudWatchLoggingPlugin/AWSCloudWatchLoggingSessionController.swift @@ -6,6 +6,7 @@ // import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore import Amplify import Combine import Foundation @@ -107,6 +108,14 @@ final class AWSCloudWatchLoggingSessionController { frameworkMetadata: AmplifyAWSServiceConfiguration.frameworkMetaData(), region: region ) + + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + configuration.httpClientEngine = FoundationClientEngine() + #endif + self.client = CloudWatchLogsClient(config: configuration) } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift index ad7341fede..08a3336789 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Service/Predictions/AWSPredictionsService.swift @@ -12,6 +12,7 @@ import AWSTextract import AWSComprehend import AWSPolly import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore import Foundation import ClientRuntime import AWSClientRuntime @@ -37,30 +38,60 @@ class AWSPredictionsService { credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + translateClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsTranslateClient = TranslateClient(config: translateClientConfiguration) let pollyClientConfiguration = try PollyClient.PollyClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + pollyClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsPollyClient = PollyClient(config: pollyClientConfiguration) let comprehendClientConfiguration = try ComprehendClient.ComprehendClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.convert.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + comprehendClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsComprehendClient = ComprehendClient(config: comprehendClientConfiguration) let rekognitionClientConfiguration = try RekognitionClient.RekognitionClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.identify.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + rekognitionClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsRekognitionClient = RekognitionClient(config: rekognitionClientConfiguration) let textractClientConfiguration = try TextractClient.TextractClientConfiguration( credentialsProvider: credentialsProvider, region: configuration.identify.region ) + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + textractClientConfiguration.httpClientEngine = FoundationClientEngine() + #endif let awsTextractClient = TextractClient(config: textractClientConfiguration) let awsTranscribeStreamingAdapter = AWSTranscribeStreamingAdapter( diff --git a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift index 415417bb5a..58329ddb84 100644 --- a/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift +++ b/AmplifyPlugins/Storage/Sources/AWSS3StoragePlugin/Service/Storage/AWSS3StorageService.swift @@ -10,6 +10,8 @@ import Foundation import AWSS3 import Amplify import AWSPluginsCore +@_spi(FoundationClientEngine) import AWSPluginsCore +import ClientRuntime /// - Tag: AWSS3StorageService class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { @@ -61,9 +63,25 @@ class AWSS3StorageService: AWSS3StorageServiceBehavior, StorageServiceProxy { credentialsProvider: credentialsProvider, region: region, signingRegion: region) + if var proxy = httpClientEngineProxy { - proxy.target = clientConfig.httpClientEngine + let httpClientEngine: HttpClientEngine + #if os(iOS) || os(macOS) + httpClientEngine = clientConfig.httpClientEngine + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + httpClientEngine = FoundationClientEngine() + #endif + proxy.target = httpClientEngine clientConfig.httpClientEngine = proxy + } else { + #if os(iOS) || os(macOS) // no-op + #else + // For any platform except iOS or macOS + // Use Foundation instead of CRT for networking. + clientConfig.httpClientEngine = FoundationClientEngine() + #endif } let s3Client = S3Client(config: clientConfig)