From a416892cf85a1f8e8de8464cf7107f0b57268c37 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Thu, 22 Jul 2021 14:44:30 -0700 Subject: [PATCH 01/12] feat: AWS Lambda authorizer support --- AWSAppSyncClient.xcodeproj/project.pbxproj | 4 ++ AWSAppSyncClient/AWSAppSyncAuthProvider.swift | 30 ++++++++-- AWSAppSyncClient/AWSAppSyncAuthType.swift | 2 + .../AWSAppSyncClientConfiguration.swift | 58 +++++++++++++++++-- .../AWSAppSyncHTTPNetworkTransport.swift | 42 ++++++++++++++ .../BasicSubscriptionConnectionFactory.swift | 3 + .../LambdaBasedConnectionPool.swift | 34 +++++++++++ 7 files changed, 161 insertions(+), 12 deletions(-) create mode 100644 AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift diff --git a/AWSAppSyncClient.xcodeproj/project.pbxproj b/AWSAppSyncClient.xcodeproj/project.pbxproj index 54655cb9..d3aec8bd 100644 --- a/AWSAppSyncClient.xcodeproj/project.pbxproj +++ b/AWSAppSyncClient.xcodeproj/project.pbxproj @@ -121,6 +121,7 @@ 21D5286A2416A2B3005186BA /* IAMAuthInterceptorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D528692416A2B3005186BA /* IAMAuthInterceptorTests.swift */; }; 3D9BF115227836800079F52F /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9BF114227836800079F52F /* NetworkReachability.swift */; }; 70C68E4D132FE62623DB8C07 /* Pods_AWSAppSyncTestHostApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */; }; + 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */; }; 8032C5415EF414C038394D69 /* Pods_AWSAppSyncTestCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */; }; 90DE0C49240A304D000E875B /* AWSAppSyncAuthTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE0C48240A304D000E875B /* AWSAppSyncAuthTypeTests.swift */; }; A70604C0C722923A70C937A1 /* Pods_AWSAppSyncTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F5A94352E1ABE35159489D /* Pods_AWSAppSyncTestApp.framework */; }; @@ -510,6 +511,7 @@ 6878E4A0F7372438C7043A88 /* Pods_AWSAppSync_AWSAppSyncTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSync_AWSAppSyncTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 741880AF213878B400523CA8 /* AuthProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthProviderTests.swift; sourceTree = ""; }; + 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPool.swift; sourceTree = ""; }; 82E714A3E9BFB80BD9E5EF90 /* Pods-ApolloTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApolloTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApolloTests/Pods-ApolloTests.debug.xcconfig"; sourceTree = ""; }; 8623D545D4837963CF2FFF02 /* Pods-AWSAppSyncUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAppSyncUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AWSAppSyncUnitTests/Pods-AWSAppSyncUnitTests.debug.xcconfig"; sourceTree = ""; }; 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestHostApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -958,6 +960,7 @@ 2171807B23FDB22B00E520C9 /* SubscriptionConnectionFactory.swift */, 2171808123FDB22B00E520C9 /* UserPoolsBasedConnectionPool.swift */, 21D38B91240C099900EC2A8D /* AppSyncRealTimeClientOIDCAuthProvider.swift */, + 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */, ); path = SubscriptionFactory; sourceTree = ""; @@ -2003,6 +2006,7 @@ CCEF79DD21DE7EED004AD64D /* AWSAppSyncClientError.swift in Sources */, FA0C12BB21CD308A00B438CB /* AWSAppSyncClientConfiguration.swift in Sources */, 178B31081FCDB34100EA4619 /* AWSAppSyncClientConflictResolutionExtensions.swift in Sources */, + 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */, 1729A0D01FA1365900F88594 /* AWSAppSyncSubscriptionWatcher.swift in Sources */, FA0D82582230D0AF00E0EA82 /* AWSAppSyncSubscriptionError.swift in Sources */, FAAACC2421DD7AC600D24B37 /* InternalS3ObjectDetails.swift in Sources */, diff --git a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift index d7d986e6..1773c062 100644 --- a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift +++ b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift @@ -3,6 +3,7 @@ // AWSAppSync // +// MARK: AWSOIDCAuthProvider // For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. // Use this for cases where the OIDC token needs to be fetched asynchronously and requires a callback public protocol AWSOIDCAuthProviderAsync: AWSOIDCAuthProvider { @@ -14,6 +15,13 @@ extension AWSOIDCAuthProviderAsync { public func getLatestAuthToken() -> String { fatalError("Callback method required") } } +// For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. +public protocol AWSOIDCAuthProvider { + /// The method should fetch the token and return it to the client for using in header request. + func getLatestAuthToken() -> String +} + +// MARK: - AWSCognitoUserPoolsProvider // For using User Pool based authorization, this protocol needs to be implemented and passed to configuration object. // Use this for cases where the UserPool auth token needs to be fetched asynchronously and requires a callback public protocol AWSCognitoUserPoolsAuthProviderAsync: AWSCognitoUserPoolsAuthProvider { @@ -25,17 +33,27 @@ extension AWSCognitoUserPoolsAuthProviderAsync { public func getLatestAuthToken() -> String { fatalError("Callback method required") } } -// For using OIDC based authorization, this protocol needs to be implemented and passed to configuration object. -public protocol AWSOIDCAuthProvider { - /// The method should fetch the token and return it to the client for using in header request. - func getLatestAuthToken() -> String -} - // For using Cognito User Pools based authorization, this protocol needs to be implemented and passed to configuration object. public protocol AWSCognitoUserPoolsAuthProvider: AWSOIDCAuthProvider { } +// MARK: - AWSLambdaAuthProvider +// For using Lambda based authorization, this protocol needs to be implemented and passed to configuration object. +// Use this for cases where the authorization token needs to be fetched asynchronously and requires a callback +public protocol AWSLambdaAuthProviderAsync: AWSLambdaAuthProvider { + func getLatestAuthToken(_ callback: @escaping (String?, Error?) -> Void) +} + +// For AWSLambdaAuthProvider that use a callback, the getLatestAuthToken is defaulted to return an empty string +extension AWSLambdaAuthProviderAsync { + public func getLatestAuthToken() -> String { fatalError("Callback method required") } +} + +// For using AWS Lambda based authorization, this protocol needs to be implemented and passed to configuration object. +public protocol AWSLambdaAuthProvider: AWSOIDCAuthProvider { } + +// MARK: - AWSAPIKeyAuthProvider // For using API Key based authorization, this protocol needs to be implemented and passed to configuration object. public protocol AWSAPIKeyAuthProvider { func getAPIKey() -> String diff --git a/AWSAppSyncClient/AWSAppSyncAuthType.swift b/AWSAppSyncClient/AWSAppSyncAuthType.swift index a75174d2..5e9c19ec 100644 --- a/AWSAppSyncClient/AWSAppSyncAuthType.swift +++ b/AWSAppSyncClient/AWSAppSyncAuthType.swift @@ -17,6 +17,8 @@ public enum AWSAppSyncAuthType: String, Codable, Hashable { /// User directory based authentication case amazonCognitoUserPools = "AMAZON_COGNITO_USER_POOLS" + + case awsLambda = "AWS_LAMBDA" /// Convenience method to use instead of `AuthType(rawValue:)` public static func getAuthType(rawValue: String) throws -> AWSAppSyncAuthType { diff --git a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift index b8e2d8ee..c4ddca23 100644 --- a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift +++ b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift @@ -109,6 +109,7 @@ public class AWSAppSyncClientConfiguration { /// `credentialsProvider`. /// - `oidcAuthProvider` is specified, and `appSyncClientInfo.authType` is "OPENID_CONNECT" /// - `userPoolsAuthProvider` is specified, and `appSyncClientInfo.authType` is "AMAZON_COGNITO_USER_POOLS" + /// - `awsLambdaAuthProvider` is specified, and `appSyncClientInfo.authType` is "AWS_LAMBDA" /// /// If none of those conditions are met, or if more than provider is specified, the initializer will throw an error. /// @@ -117,6 +118,7 @@ public class AWSAppSyncClientConfiguration { /// - apiKeyAuthProvider: A `AWSAPIKeyAuthProvider` protocol object for API Key based authorization. /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization. /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for User Pool based authorization. + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for OIDC based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration. /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. @@ -131,6 +133,7 @@ public class AWSAppSyncClientConfiguration { credentialsProvider: AWSCredentialsProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider? = nil, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider? = nil, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration? = nil, connectionStateChangeHandler: ConnectionStateChangeHandler? = nil, @@ -150,6 +153,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, urlSessionConfiguration: urlSessionConfiguration, cacheConfiguration: cacheConfiguration, @@ -177,6 +181,7 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for OIDC based authorization /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for User Pool based authorization + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. /// - connectionStateChangeHandler: The delegate object to be notified when client network state changes @@ -191,6 +196,7 @@ public class AWSAppSyncClientConfiguration { credentialsProvider: AWSCredentialsProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider? = nil, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider? = nil, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration? = nil, connectionStateChangeHandler: ConnectionStateChangeHandler? = nil, @@ -207,6 +213,8 @@ public class AWSAppSyncClientConfiguration { authType = .oidcToken } else if userPoolsAuthProvider != nil { authType = .amazonCognitoUserPools + } else if awsLambdaAuthProvider != nil { + authType = .awsLambda } else { throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("Invalid auth provider configuration. Exactly one of the supported auth providers must be passed") } @@ -240,6 +248,7 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: A `AWSCredentialsProvider` object for AWS_IAM based authorization. /// - userPoolsAuthProvider: A `AWSCognitoUserPoolsAuthProvider` protocol object for Cognito User Pools based authorization. /// - oidcAuthProvider: A `AWSOIDCAuthProvider` protocol object for any OpenId Connect based authorization. + /// - awsLambdaAuthProvider: A `AWSLambdaAuthProvider` protocol object for AWS Lambda based authorization. /// - urlSessionConfiguration: A `URLSessionConfiguration` configuration object for custom HTTP configuration. /// - cacheConfiguration: Configuration for local queries, mutations, and subscriptions caches. /// - connectionStateChangeHandler: The delegate object to be notified when client network state changes. @@ -254,6 +263,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider? = nil, oidcAuthProvider: AWSOIDCAuthProvider?, urlSessionConfiguration: URLSessionConfiguration = URLSessionConfiguration.default, cacheConfiguration: AWSAppSyncCacheConfiguration?, @@ -298,6 +308,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, retryStrategy: retryStrategy ) @@ -315,24 +326,28 @@ public class AWSAppSyncClientConfiguration { /// - credentialsProvider: Should be `nil` unless `authType` is `.awsIAM` /// - oidcAuthProvider: Should be `nil` unless `authType` is `.oidcToken` /// - userPoolsAuthProvider: Should be `nil` unless `authType` is `.amazonCognitoUserPools` + /// - awsLambdaAuthProvider: Should be `nil` unless `authType` is `.awsLambda` /// - Returns: `true` if the auth providers not required for `authType` are all `nil`, `false` otherwise. private static func unusedAuthProvidersAreNil(for authType: AWSAppSyncAuthType, apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, - userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?) -> Bool { + userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?) -> Bool { let unneededProviders: [Any?] switch authType { case .apiKey: - unneededProviders = [credentialsProvider, userPoolsAuthProvider, oidcAuthProvider] + unneededProviders = [credentialsProvider, userPoolsAuthProvider, oidcAuthProvider, awsLambdaAuthProvider] case .amazonCognitoUserPools: - unneededProviders = [apiKeyAuthProvider, credentialsProvider, oidcAuthProvider] + unneededProviders = [apiKeyAuthProvider, credentialsProvider, oidcAuthProvider, awsLambdaAuthProvider] case .awsIAM: - unneededProviders = [apiKeyAuthProvider, oidcAuthProvider, userPoolsAuthProvider] + unneededProviders = [apiKeyAuthProvider, oidcAuthProvider, userPoolsAuthProvider, awsLambdaAuthProvider] case .oidcToken: - unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider] + unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider, awsLambdaAuthProvider] + case .awsLambda: + unneededProviders = [apiKeyAuthProvider, credentialsProvider, userPoolsAuthProvider, oidcAuthProvider] } return unneededProviders.allSatisfy { $0 == nil } @@ -361,6 +376,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: AWSAPIKeyAuthProvider?, credentialsProvider: AWSCredentialsProvider?, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, retryStrategy: AWSAppSyncRetryStrategy) throws -> AWSAppSyncHTTPNetworkTransport { @@ -370,7 +386,8 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, oidcAuthProvider: oidcAuthProvider, - userPoolsAuthProvider: userPoolsAuthProvider + userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider ) guard unusedProvidersAreNil else { @@ -405,6 +422,11 @@ public class AWSAppSyncClientConfiguration { urlSessionConfiguration: urlSessionConfiguration, authProvider: oidcAuthProvider, retryStrategy: retryStrategy) + case .awsLambda: + networkTransport = try makeNetworkTransportForAWSLambda(url: url, + urlSessionConfiguration: urlSessionConfiguration, + authProvider: awsLambdaAuthProvider, + retryStrategy: retryStrategy) } return networkTransport @@ -506,6 +528,30 @@ public class AWSAppSyncClientConfiguration { retryStrategy: retryStrategy) return networkTransport } + + /// Returns an AWSAppSyncHTTPNetworkTransport configured to use the provided auth provider + /// + /// - Parameters: + /// - url: The endpoint URL + /// - urlSessionConfiguration: The URLSessionConfiguration to use for network connections managed by the transport + /// - authProvider: The auth provider to use for authenticating network requests + /// - retryStrategy: The retry strategy specified in client configuration + /// - Returns: The AWSAppSyncHTTPNetworkTransport + /// - Throws: An AWSAppSyncClientConfigurationError if the auth provider is nil + private static func makeNetworkTransportForAWSLambda(url: URL, + urlSessionConfiguration: URLSessionConfiguration, + authProvider: AWSLambdaAuthProvider?, + retryStrategy: AWSAppSyncRetryStrategy) throws -> AWSAppSyncHTTPNetworkTransport { + // No default OIDC provider available + guard let authProvider = authProvider else { + throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("AWSOIDCAuthProvider cannot be nil.") + } + let networkTransport = AWSAppSyncHTTPNetworkTransport(url: url, + awsLambdaAuthProvider: authProvider, + configuration: urlSessionConfiguration, + retryStrategy: retryStrategy) + return networkTransport + } /// Given at least one non-nil parameter, resolves and returns an AWSAPIKeyAuthProvider to use for creating an /// AWSHTTPNetworkTransport. If `apiKeyAuthProvider` is not nil, returns that object. If it is nil, but `apiKey` is not nil, diff --git a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift index 21f9e7c0..ca105ac1 100644 --- a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift +++ b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift @@ -13,6 +13,7 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { case apiKey(AWSAPIKeyAuthProvider) case oidcToken(AWSOIDCAuthProvider) case amazonCognitoUserPools(AWSCognitoUserPoolsAuthProvider) + case awsLambda(AWSLambdaAuthProvider) public var appSyncAuthType: AWSAppSyncAuthType { switch self { @@ -24,6 +25,8 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { return .amazonCognitoUserPools case .oidcToken: return .oidcToken + case .awsLambda: + return .awsLambda } } } @@ -132,6 +135,28 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { retryStrategy: retryStrategy ) } + + /// Creates a network transport with the specified server URL and session configuration. + /// + /// - Parameters: + /// - url: The URL of a GraphQL server to connect to. + /// - awsLambdaAuthProvider: An implementation of `AWSLambdaAuthProvider` protocol. + /// - configuration: A session configuration used to configure the session. Defaults to `URLSessionConfiguration.default`. + /// - sendOperationIdentifiers: Whether to send operation identifiers rather than full operation text, for use with servers that support query persistence. Defaults to false. + /// - retryStrategy: The retry strategy to be followed by HTTP client + public convenience init(url: URL, + awsLambdaAuthProvider: AWSLambdaAuthProvider, + configuration: URLSessionConfiguration = URLSessionConfiguration.default, + sendOperationIdentifiers: Bool = false, + retryStrategy: AWSAppSyncRetryStrategy = .exponential) { + self.init( + url: url, + urlSession: URLSession(configuration: configuration), + authProvider: .awsLambda(awsLambdaAuthProvider), + sendOperationIdentifiers: sendOperationIdentifiers, + retryStrategy: retryStrategy + ) + } /// Creates a network transport with the specified server URL and session configuration. /// @@ -345,6 +370,23 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") completionHandler(.success(())) } + + case .awsLambda(let provider): + if let provider = provider as? AWSLambdaAuthProviderAsync { + provider.getLatestAuthToken { (token, error) in + if let error = error { + completionHandler(.failure(error)) + } else if let token = token { + mutableRequest.setValue(token, forHTTPHeaderField: "authorization") + completionHandler(.success(())) + } else { + fatalError("Invalid data returned in token callback") + } + } + } else { + mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") + completionHandler(.success(())) + } } } diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift index 9a437164..04196fd2 100644 --- a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift @@ -14,6 +14,7 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { var userpoolsBasedPool: UserPoolsBasedConnectionPool? var iamBasedPool: IAMBasedConnectionPool? var oidcBasedPool: OIDCBasedConnectionPool? + var lambdaBasedPool: LambdaBasedConnectionPool? let url: URL let retryStrategy: AWSAppSyncRetryStrategy @@ -70,6 +71,8 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { return userpoolsBasedPool case .oidcToken: return oidcBasedPool + case .awsLambda: + return lambdaBasedPool } } } diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift new file mode 100644 index 00000000..de32ac7c --- /dev/null +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift @@ -0,0 +1,34 @@ +// +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import Foundation +import AppSyncRealTimeClient + +class LambdaBasedConnectionPool: SubscriptionConnectionPool { + + private let tokenProvider: AWSLambdaAuthProvider + var endPointToProvider: [String: ConnectionProvider] + + init(_ tokenProvider: AWSLambdaAuthProvider) { + self.tokenProvider = tokenProvider + self.endPointToProvider = [:] + } + + func connection(for url: URL, connectionType: SubscriptionConnectionType) -> SubscriptionConnection { + if let connectionProvider = endPointToProvider[url.absoluteString] { + return AppSyncSubscriptionConnection(provider: connectionProvider) + } + + let authProvider = AppSyncRealTimeClientOIDCAuthProvider(authProvider: tokenProvider) + let authInterceptor = OIDCAuthInterceptor(authProvider) + let connectionProvider = ConnectionProviderFactory.createConnectionProvider(for: url, + authInterceptor: authInterceptor, + connectionType: connectionType) + endPointToProvider[url.absoluteString] = connectionProvider + + return AppSyncSubscriptionConnection(provider: connectionProvider) + } +} From 06a6c8ea8d52dd8a972cb74e346c0656737b8e1f Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Thu, 22 Jul 2021 14:51:13 -0700 Subject: [PATCH 02/12] feat: update error messages/comments --- AWSAppSyncClient/AWSAppSyncClientConfiguration.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift index c4ddca23..27df8ba2 100644 --- a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift +++ b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift @@ -542,9 +542,9 @@ public class AWSAppSyncClientConfiguration { urlSessionConfiguration: URLSessionConfiguration, authProvider: AWSLambdaAuthProvider?, retryStrategy: AWSAppSyncRetryStrategy) throws -> AWSAppSyncHTTPNetworkTransport { - // No default OIDC provider available + // No default AWS Lambda provider available guard let authProvider = authProvider else { - throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("AWSOIDCAuthProvider cannot be nil.") + throw AWSAppSyncClientConfigurationError.invalidAuthConfiguration("AWSLambdaAuthProvider cannot be nil.") } let networkTransport = AWSAppSyncHTTPNetworkTransport(url: url, awsLambdaAuthProvider: authProvider, From 2ab1d196b32da7a8cbe9b09919cd91945ff54a7b Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 11:30:45 -0700 Subject: [PATCH 03/12] create subscription pool --- AWSAppSyncClient/AWSAppSyncClientConfiguration.swift | 1 + .../ConnectionPool/BasicSubscriptionConnectionFactory.swift | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift index 27df8ba2..4dbadbaf 100644 --- a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift +++ b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift @@ -297,6 +297,7 @@ public class AWSAppSyncClientConfiguration { region: serviceRegion, apiKeyProvider: apikeyProvider, cognitoUserPoolProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, iamAuthProvider: credentialsProvider) self.networkTransport = try AWSAppSyncClientConfiguration.getNetworkTransport( diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift index 04196fd2..e5b8dbcb 100644 --- a/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/ConnectionPool/BasicSubscriptionConnectionFactory.swift @@ -26,6 +26,7 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { region: AWSRegionType?, apiKeyProvider: AWSAPIKeyAuthProvider?, cognitoUserPoolProvider: AWSCognitoUserPoolsAuthProvider?, + awsLambdaAuthProvider: AWSLambdaAuthProvider?, oidcAuthProvider: AWSOIDCAuthProvider?, iamAuthProvider: AWSCredentialsProvider?) { @@ -45,6 +46,10 @@ class BasicSubscriptionConnectionFactory: SubscriptionConnectionFactory { if let oidcAuthProvider = oidcAuthProvider { self.oidcBasedPool = OIDCBasedConnectionPool(oidcAuthProvider) } + if let awsLambdaAuthProvider = awsLambdaAuthProvider { + self.lambdaBasedPool = LambdaBasedConnectionPool(awsLambdaAuthProvider) + } + } func connection(connectionType: SubscriptionConnectionType) -> SubscriptionConnection? { From b0965a00680ea80bef3ae7366701f4cb25945aae Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 12:37:08 -0700 Subject: [PATCH 04/12] subscription connection factory tests --- .../AWSAppSyncClientConfiguration.swift | 1 + .../AWSAppSyncLambdaAuthTests.swift | 31 +++++++++++++++++++ .../AppSyncClientTestHelper.swift | 9 ++++++ AWSAppSyncTestCommon/MockAuthProviders.swift | 13 ++++++++ .../SubscriptionConnectionFactoryTests.swift | 1 + 5 files changed, 55 insertions(+) create mode 100644 AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift diff --git a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift index 4dbadbaf..e476fad3 100644 --- a/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift +++ b/AWSAppSyncClient/AWSAppSyncClientConfiguration.swift @@ -226,6 +226,7 @@ public class AWSAppSyncClientConfiguration { apiKeyAuthProvider: apiKeyAuthProvider, credentialsProvider: credentialsProvider, userPoolsAuthProvider: userPoolsAuthProvider, + awsLambdaAuthProvider: awsLambdaAuthProvider, oidcAuthProvider: oidcAuthProvider, urlSessionConfiguration: urlSessionConfiguration, cacheConfiguration: cacheConfiguration, diff --git a/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift new file mode 100644 index 00000000..c547f009 --- /dev/null +++ b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift @@ -0,0 +1,31 @@ +// +// Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import XCTest + +class AWSAppSyncLambdaAuthTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift index 1e92f751..c71fbac6 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift @@ -48,6 +48,7 @@ public class AppSyncClientTestHelper: NSObject { case invalidAPIKey case invalidOIDC case invalidStaticCredentials + case lambda /// Delay set at 120 seconds case delayedInvalidOIDC } @@ -232,6 +233,14 @@ public class AppSyncClientTestHelper: NSObject { s3ObjectManager: s3ObjectManager ) + case .lambda: + appSyncConfig = try AWSAppSyncClientConfiguration( + url: testConfiguration.apiKeyEndpointURL, + serviceRegion: testConfiguration.apiKeyEndpointRegion, + awsLambdaAuthProvider: MockLambdaAuthProvider(), + cacheConfiguration: cacheConfiguration, + s3ObjectManager: s3ObjectManager + ) } return appSyncConfig diff --git a/AWSAppSyncTestCommon/MockAuthProviders.swift b/AWSAppSyncTestCommon/MockAuthProviders.swift index 10067841..dbdfc801 100644 --- a/AWSAppSyncTestCommon/MockAuthProviders.swift +++ b/AWSAppSyncTestCommon/MockAuthProviders.swift @@ -82,3 +82,16 @@ struct MockAWSCognitoUserPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider { return token } } + + +struct MockLambdaAuthProvider: AWSLambdaAuthProvider { + var token: String + + init(with token: String = "custom-lambda-token") { + self.token = token + } + + func getLatestAuthToken() -> String { + return token + } +} diff --git a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift index 82ca09fb..9371f1f7 100644 --- a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift +++ b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/SubscriptionConnectionFactoryTests.swift @@ -20,6 +20,7 @@ class SubscriptionConnectionFactoryTests: XCTestCase { region: .USWest2, apiKeyProvider: MockAPIKeyAuthProvider(), cognitoUserPoolProvider: MockUserPoolsAuthProvider(), + awsLambdaAuthProvider: MockLambdaAuthProvider(), oidcAuthProvider: MockUserPoolsAuthProvider(), iamAuthProvider: MockIAMAuthProvider()) } From 109d10851e3cf7a3c1a7b91d314dbc43fe08df05 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 13:43:25 -0700 Subject: [PATCH 05/12] integration tests --- AWSAppSyncClient.xcodeproj/project.pbxproj | 4 + .../AWSAppSyncLambdaAuthTests.swift | 90 ++++++++++++++++--- .../AppSyncClientTestConfiguration.swift | 32 ++++++- ...pSyncClientTestConfigurationDefaults.swift | 6 ++ .../AppSyncClientTestHelper.swift | 4 +- 5 files changed, 119 insertions(+), 17 deletions(-) diff --git a/AWSAppSyncClient.xcodeproj/project.pbxproj b/AWSAppSyncClient.xcodeproj/project.pbxproj index d3aec8bd..91c1a21e 100644 --- a/AWSAppSyncClient.xcodeproj/project.pbxproj +++ b/AWSAppSyncClient.xcodeproj/project.pbxproj @@ -122,6 +122,7 @@ 3D9BF115227836800079F52F /* NetworkReachability.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D9BF114227836800079F52F /* NetworkReachability.swift */; }; 70C68E4D132FE62623DB8C07 /* Pods_AWSAppSyncTestHostApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */; }; 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */; }; + 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */; }; 8032C5415EF414C038394D69 /* Pods_AWSAppSyncTestCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */; }; 90DE0C49240A304D000E875B /* AWSAppSyncAuthTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE0C48240A304D000E875B /* AWSAppSyncAuthTypeTests.swift */; }; A70604C0C722923A70C937A1 /* Pods_AWSAppSyncTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F5A94352E1ABE35159489D /* Pods_AWSAppSyncTestApp.framework */; }; @@ -512,6 +513,7 @@ 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestCommon.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 741880AF213878B400523CA8 /* AuthProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthProviderTests.swift; sourceTree = ""; }; 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPool.swift; sourceTree = ""; }; + 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAppSyncLambdaAuthTests.swift; sourceTree = ""; }; 82E714A3E9BFB80BD9E5EF90 /* Pods-ApolloTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApolloTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApolloTests/Pods-ApolloTests.debug.xcconfig"; sourceTree = ""; }; 8623D545D4837963CF2FFF02 /* Pods-AWSAppSyncUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAppSyncUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AWSAppSyncUnitTests/Pods-AWSAppSyncUnitTests.debug.xcconfig"; sourceTree = ""; }; 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestHostApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1109,6 +1111,7 @@ FAFD409121D702EA0063D894 /* Helpers */, FADC6B8822679B00008588FC /* Resources */, 741880AF213878B400523CA8 /* AuthProviderTests.swift */, + 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */, 17664128214F6732003AE269 /* AWSAppSyncAPIKeyAuthTests.swift */, 174F80AE2109229C00775D0D /* AWSAppSyncCognitoAuthTests.swift */, E48168AD226E8325005A1A41 /* AWSAppSyncMultiAuthClientsTests.swift */, @@ -2077,6 +2080,7 @@ files = ( FADC6B8922679B38008588FC /* MutationQueuePerformanceTests.swift in Sources */, FA902D0E21D97E9600C4052F /* AuthProviderTests.swift in Sources */, + 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */, FA902D1021D97EB100C4052F /* AWSAppSyncCognitoAuthTests.swift in Sources */, E414DDB52289BD8A004C37CE /* AWSAppSyncMultiAuthClientsTests.swift in Sources */, FA902D1321D97EC500C4052F /* SubscriptionStressTestHelper.swift in Sources */, diff --git a/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift index c547f009..fbff3e20 100644 --- a/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift +++ b/AWSAppSyncIntegrationTests/AWSAppSyncLambdaAuthTests.swift @@ -5,27 +5,93 @@ // import XCTest +@testable import AWSAppSync +@testable import AWSCore +@testable import AWSAppSyncTestCommon class AWSAppSyncLambdaAuthTests: XCTestCase { - override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. - } + var appSyncClient: AWSAppSyncClient? + + /// Use this as our timeout value for any operation that hits the network. Note that this may need to be higher + /// than you think, to account for CI systems running in shared environments + private static let networkOperationTimeout = 180.0 - override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + private static let mutationQueue = DispatchQueue(label: "com.amazonaws.appsync.AWSAppSyncLambdaAuthTests.mutationQueue") + private static let subscriptionAndFetchQueue = DispatchQueue(label: "com.amazonaws.appsync.AWSAppSyncLambdaAuthTests.subscriptionAndFetchQueue") + + + override func setUp() { + super.setUp() + let authType = AppSyncClientTestHelper.AuthenticationType.lambda + do { + appSyncClient = try AWSAppSyncLambdaAuthTests.makeAppSyncClient(authType: authType) + } catch { + XCTFail(error.localizedDescription) + } } + + func testMutation() { + let postCreated = expectation(description: "Post created successfully.") + let addPost = DefaultTestPostData.defaultCreatePostWithoutFileUsingParametersMutation - func testExample() throws { - // This is an example of a functional test case. - // Use XCTAssert and related functions to verify your tests produce the correct results. + appSyncClient?.perform(mutation: addPost, queue: AWSAppSyncLambdaAuthTests.mutationQueue, resultHandler: { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.createPostWithoutFileUsingParameters?.id) + XCTAssertEqual( + result!.data!.createPostWithoutFileUsingParameters?.author, + DefaultTestPostData.author + ) + print("Created post \(result?.data?.createPostWithoutFileUsingParameters?.id ?? "(ID unexpectedly nil)")") + postCreated.fulfill() + }) + + wait(for: [postCreated], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) } - func testPerformanceExample() throws { - // This is an example of a performance test case. - self.measure { - // Put the code you want to measure the time of here. + func testQuery() { + let postCreated = expectation(description: "Post created successfully.") + let addPost = DefaultTestPostData.defaultCreatePostWithoutFileUsingParametersMutation + + appSyncClient?.perform(mutation: addPost, queue: AWSAppSyncLambdaAuthTests.mutationQueue, resultHandler: { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.createPostWithoutFileUsingParameters?.id) + XCTAssertEqual( + result!.data!.createPostWithoutFileUsingParameters?.author, + DefaultTestPostData.author + ) + postCreated.fulfill() + }) + + wait(for: [postCreated], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) + + let query = ListPostsQuery() + + let listPostsCompleted = expectation(description: "Query done successfully.") + + appSyncClient?.fetch(query: query, cachePolicy: .fetchIgnoringCacheData, queue: AWSAppSyncLambdaAuthTests.subscriptionAndFetchQueue) { result, error in + XCTAssertNil(error) + XCTAssertNotNil(result?.data?.listPosts) + XCTAssertGreaterThan(result!.data!.listPosts!.count, 0, "Expected service to return at least 1 post.") + listPostsCompleted.fulfill() } + + wait(for: [listPostsCompleted], timeout: AWSAppSyncLambdaAuthTests.networkOperationTimeout) } + + + // MARK: - Utilities + static func makeAppSyncClient(authType: AppSyncClientTestHelper.AuthenticationType, + cacheConfiguration: AWSAppSyncCacheConfiguration? = nil) throws -> DeinitNotifiableAppSyncClient { + + let testBundle = Bundle(for: AWSAppSyncLambdaAuthTests.self) + let helper = try AppSyncClientTestHelper( + with: authType, + cacheConfiguration: cacheConfiguration, + testBundle: testBundle + ) + return helper.appSyncClient + } } + diff --git a/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift b/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift index 5f59cd22..9947a096 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestConfiguration.swift @@ -13,6 +13,9 @@ struct AppSyncClientTestConfiguration { static let apiKey = "AppSyncAPIKey" static let apiKeyEndpointURL = "AppSyncEndpointAPIKey" static let apiKeyEndpointRegion = "AppSyncEndpointAPIKeyRegion" + + static let lambdaEndpointURL = "AppSyncEndpointAPIKeyLambda" + static let lambdaEndpointRegion = "AppSyncEndpointAPIKeyLambdaRegion" static let cognitoPoolId = "CognitoIdentityPoolId" static let cognitoPoolRegion = "CognitoIdentityPoolRegion" @@ -37,12 +40,17 @@ struct AppSyncClientTestConfiguration { bucketName: "FOR_UNIT_TESTING", bucketRegion: .USEast1, clientDatabasePrefix: "", - apiKeyForCognitoPoolEndpoint: "FOR_UNIT_TESTING") + apiKeyForCognitoPoolEndpoint: "FOR_UNIT_TESTING", + lambdaEndpointURL: URL(string: "http://www.amazon.com/for_unit_testing")!, + lambdaEndpointRegion: .USEast1) }() let apiKey: String let apiKeyEndpointURL: URL let apiKeyEndpointRegion: AWSRegionType + + let lambdaEndpointURL: URL + let lambdaEndpointRegion: AWSRegionType let cognitoPoolId: String let cognitoPoolRegion: AWSRegionType @@ -74,7 +82,9 @@ struct AppSyncClientTestConfiguration { bucketName: AppSyncClientTestConfigurationDefaults.bucketName, bucketRegion: AppSyncClientTestConfigurationDefaults.bucketRegion, clientDatabasePrefix: "", - apiKeyForCognitoPoolEndpoint: AppSyncClientTestConfigurationDefaults.apiKeyForCognitoPoolEndpoint) + apiKeyForCognitoPoolEndpoint: AppSyncClientTestConfigurationDefaults.apiKeyForCognitoPoolEndpoint, + lambdaEndpointURL: AppSyncClientTestConfigurationDefaults.lambdaEndpointURL, + lambdaEndpointRegion: AppSyncClientTestConfigurationDefaults.lambdaEndpointRegion) } init?(with bundle: Bundle) { @@ -146,6 +156,18 @@ struct AppSyncClientTestConfiguration { } self.bucketRegion = bucketRegionString.aws_regionTypeValue() self.clientDatabasePrefix = "" + + guard let lambdaEndpointRegionString = jsonObject[JSONKeys.lambdaEndpointRegion] as? String else { + return nil + } + self.lambdaEndpointRegion = lambdaEndpointRegionString.aws_regionTypeValue() + + guard let lambdaEndpointString = jsonObject[JSONKeys.lambdaEndpointURL] as? String, + let lambdaEndpoint = URL(string: lambdaEndpointString) else { + return nil + } + self.lambdaEndpointURL = lambdaEndpoint + } private init(apiKey: String, @@ -158,7 +180,9 @@ struct AppSyncClientTestConfiguration { bucketName: String, bucketRegion: AWSRegionType, clientDatabasePrefix: String?, - apiKeyForCognitoPoolEndpoint: String) { + apiKeyForCognitoPoolEndpoint: String, + lambdaEndpointURL: URL, + lambdaEndpointRegion: AWSRegionType) { self.apiKey = apiKey self.apiKeyEndpointURL = apiKeyEndpointURL self.apiKeyEndpointRegion = apiKeyEndpointRegion @@ -170,6 +194,8 @@ struct AppSyncClientTestConfiguration { self.bucketRegion = bucketRegion self.clientDatabasePrefix = clientDatabasePrefix ?? "" self.apiKeyForCognitoPoolEndpoint = apiKeyForCognitoPoolEndpoint + self.lambdaEndpointRegion = lambdaEndpointRegion + self.lambdaEndpointURL = lambdaEndpointURL } } diff --git a/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift b/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift index dce2db35..95495923 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestConfigurationDefaults.swift @@ -49,4 +49,10 @@ struct AppSyncClientTestConfigurationDefaults { // Equivalent to the JSON key "BucketRegion" static let bucketRegion = AWSRegionType.USEast1 + + // Equivalent to the JSON key "AppSyncEndpoint" + static let lambdaEndpointURL = URL(string: "https://localhost")! + + // Equivalent to the JSON key "AppSyncRegion" + static let lambdaEndpointRegion = AWSRegionType.USEast1 } diff --git a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift index c71fbac6..0aacc090 100644 --- a/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift +++ b/AWSAppSyncTestCommon/AppSyncClientTestHelper.swift @@ -235,8 +235,8 @@ public class AppSyncClientTestHelper: NSObject { case .lambda: appSyncConfig = try AWSAppSyncClientConfiguration( - url: testConfiguration.apiKeyEndpointURL, - serviceRegion: testConfiguration.apiKeyEndpointRegion, + url: testConfiguration.lambdaEndpointURL, + serviceRegion: testConfiguration.lambdaEndpointRegion, awsLambdaAuthProvider: MockLambdaAuthProvider(), cacheConfiguration: cacheConfiguration, s3ObjectManager: s3ObjectManager From 34c31967ac9159d452f79cef6903e6ae73fb6e40 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 13:54:08 -0700 Subject: [PATCH 06/12] update integration tests instructions --- .../ConsoleResources/appsync-lambda-authorizer.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js diff --git a/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js b/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js new file mode 100644 index 00000000..ce2f2353 --- /dev/null +++ b/AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorizer.js @@ -0,0 +1,13 @@ +exports.handler = async (event) => { + console.log(`auth event >`, JSON.stringify(event, null, 2)) + const { + authorizationToken, + requestContext: { apiId, accountId }, + } = event + const response = { + isAuthorized: authorizationToken === 'custom-lambda-token', + ttlOverride: 10, + } + console.log(`response >`, JSON.stringify(response, null, 2)) + return response +}; From 0a16096e8f756a33e0eec05191fc7ba8082b4a26 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 13:57:08 -0700 Subject: [PATCH 07/12] update integration tests instructions --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index e8285b38..5e951673 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,16 @@ You can get the backend setup by following the steps below: - `BucketName` - `BucketRegion` - `AppSyncMultiAuthAPIKey` +1. Create another CloudFormation Stack following step 1-6 above with `API Key` as the Auth type (we'll change that later) + 1. Create a Lambda function using the template provided in this project at `AWSAppSyncIntegrationTests/ConsoleResources/appsync-lambda-authorize +r.js` + 1. Once the stack is complete click on the __Outputs__ tab + 1. Copy the appropriate values to the test configuration file `AppSyncIntegrationTests/appsync_test_credentials.json`: + - `AppSyncEndpointAPIKeyLambda` + - `AppSyncEndpointAPIKeyLambdaRegion` + + 1. Go to the [AWS AppSync console](https://console.aws.amazon.com/appsync/home), select the newly created AppSync instance + 1. In the `Settings` section, change the default authentication type to `AWS Lambda` and select the Lambda function created at the previous step > Note: You must either provide all values in the `AppSyncIntegrationTests/appsync_test_credentials.json` or in code. There is no mechanism to handle partial overrides of one source with the other. All values must be specified before running the integration tests. From b02cd285929353b17ac2ffeef513f418b63f08f0 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 14:41:36 -0700 Subject: [PATCH 08/12] clean up http transport logic --- .../AWSAppSyncHTTPNetworkTransport.swift | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift index ca105ac1..fe007854 100644 --- a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift +++ b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift @@ -372,20 +372,20 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { } case .awsLambda(let provider): - if let provider = provider as? AWSLambdaAuthProviderAsync { - provider.getLatestAuthToken { (token, error) in - if let error = error { - completionHandler(.failure(error)) - } else if let token = token { - mutableRequest.setValue(token, forHTTPHeaderField: "authorization") - completionHandler(.success(())) - } else { - fatalError("Invalid data returned in token callback") - } - } - } else { + guard let provider = provider as? AWSLambdaAuthProviderAsync else { mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") completionHandler(.success(())) + break + } + provider.getLatestAuthToken { (token, error) in + if let error = error { + completionHandler(.failure(error)) + } else if let token = token { + mutableRequest.setValue(token, forHTTPHeaderField: "authorization") + completionHandler(.success(())) + } else { + fatalError("Invalid data returned in token callback") + } } } From d3a64750284bd676e7d777230fbf98588d0db1d6 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Tue, 27 Jul 2021 14:55:53 -0700 Subject: [PATCH 09/12] fix lint error --- AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift index fe007854..a80484b5 100644 --- a/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift +++ b/AWSAppSyncClient/AWSAppSyncHTTPNetworkTransport.swift @@ -372,12 +372,12 @@ public class AWSAppSyncHTTPNetworkTransport: AWSNetworkTransport { } case .awsLambda(let provider): - guard let provider = provider as? AWSLambdaAuthProviderAsync else { + guard let asyncProvider = provider as? AWSLambdaAuthProviderAsync else { mutableRequest.setValue(provider.getLatestAuthToken(), forHTTPHeaderField: "authorization") completionHandler(.success(())) break } - provider.getLatestAuthToken { (token, error) in + asyncProvider.getLatestAuthToken { (token, error) in if let error = error { completionHandler(.failure(error)) } else if let token = token { From 703816793b95294cf418a4f47b436c162af85509 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Wed, 28 Jul 2021 10:09:32 -0700 Subject: [PATCH 10/12] LambdaAuth interceptor --- AWSAppSyncClient.xcodeproj/project.pbxproj | 4 + AWSAppSyncClient/AWSAppSyncAuthProvider.swift | 5 +- .../LambdaAuthInterceptor.swift | 85 +++++++++++++++++++ .../LambdaBasedConnectionPool.swift | 3 +- 4 files changed, 94 insertions(+), 3 deletions(-) create mode 100644 AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift diff --git a/AWSAppSyncClient.xcodeproj/project.pbxproj b/AWSAppSyncClient.xcodeproj/project.pbxproj index 91c1a21e..12c5ec13 100644 --- a/AWSAppSyncClient.xcodeproj/project.pbxproj +++ b/AWSAppSyncClient.xcodeproj/project.pbxproj @@ -123,6 +123,7 @@ 70C68E4D132FE62623DB8C07 /* Pods_AWSAppSyncTestHostApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */; }; 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */; }; 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */; }; + 763C857926B1C262005164B2 /* LambdaAuthInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */; }; 8032C5415EF414C038394D69 /* Pods_AWSAppSyncTestCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */; }; 90DE0C49240A304D000E875B /* AWSAppSyncAuthTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE0C48240A304D000E875B /* AWSAppSyncAuthTypeTests.swift */; }; A70604C0C722923A70C937A1 /* Pods_AWSAppSyncTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F5A94352E1ABE35159489D /* Pods_AWSAppSyncTestApp.framework */; }; @@ -514,6 +515,7 @@ 741880AF213878B400523CA8 /* AuthProviderTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AuthProviderTests.swift; sourceTree = ""; }; 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPool.swift; sourceTree = ""; }; 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAppSyncLambdaAuthTests.swift; sourceTree = ""; }; + 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaAuthInterceptor.swift; sourceTree = ""; }; 82E714A3E9BFB80BD9E5EF90 /* Pods-ApolloTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApolloTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApolloTests/Pods-ApolloTests.debug.xcconfig"; sourceTree = ""; }; 8623D545D4837963CF2FFF02 /* Pods-AWSAppSyncUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAppSyncUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AWSAppSyncUnitTests/Pods-AWSAppSyncUnitTests.debug.xcconfig"; sourceTree = ""; }; 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestHostApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -983,6 +985,7 @@ isa = PBXGroup; children = ( 21D5286224169CEE005186BA /* IAMAuthInterceptor.swift */, + 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */, ); path = AuthInterceptor; sourceTree = ""; @@ -1973,6 +1976,7 @@ 17E009BB1FCAB234005031DB /* GraphQLDependencyTracker.swift in Sources */, 17E009C71FCAB234005031DB /* JSONSerializationFormat.swift in Sources */, 17E009CD1FCAB234005031DB /* Collections.swift in Sources */, + 763C857926B1C262005164B2 /* LambdaAuthInterceptor.swift in Sources */, 17E009C51FCAB234005031DB /* GraphQLResultAccumulator.swift in Sources */, 178B31071FCDB34100EA4619 /* AWSAppSyncClientS3ObjectsExtensions.swift in Sources */, 17E009CF1FCAB234005031DB /* ResultOrPromise.swift in Sources */, diff --git a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift index 1773c062..d387ac95 100644 --- a/AWSAppSyncClient/AWSAppSyncAuthProvider.swift +++ b/AWSAppSyncClient/AWSAppSyncAuthProvider.swift @@ -51,7 +51,10 @@ extension AWSLambdaAuthProviderAsync { } // For using AWS Lambda based authorization, this protocol needs to be implemented and passed to configuration object. -public protocol AWSLambdaAuthProvider: AWSOIDCAuthProvider { } +public protocol AWSLambdaAuthProvider { + /// The method should fetch the token and return it to the client for using in header request. + func getLatestAuthToken() -> String +} // MARK: - AWSAPIKeyAuthProvider // For using API Key based authorization, this protocol needs to be implemented and passed to configuration object. diff --git a/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift b/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift new file mode 100644 index 00000000..1c03f8a3 --- /dev/null +++ b/AWSAppSyncClient/Internal/AuthInterceptor/LambdaAuthInterceptor.swift @@ -0,0 +1,85 @@ +// +// Copyright Amazon.com Inc. or its affiliates. +// All Rights Reserved. +// +// SPDX-License-Identifier: Apache-2.0 +// + +import Foundation +import AppSyncRealTimeClient + +/// AWS Lambda Authorizer interceptor +class LambdaAuthInterceptor: AuthInterceptor { + + let authTokenProvider: AWSLambdaAuthProvider + + init(authTokenProvider: AWSLambdaAuthProvider) { + self.authTokenProvider = authTokenProvider + } + + func interceptMessage(_ message: AppSyncMessage, for endpoint: URL) -> AppSyncMessage { + let host = endpoint.host! + let authToken = authTokenProvider.getLatestAuthToken() + guard case .subscribe = message.messageType else { + return message + } + + let authHeader = TokenAuthHeader(token: authToken, host: host) + var payload = message.payload ?? AppSyncMessage.Payload() + payload.authHeader = authHeader + + let signedMessage = AppSyncMessage( + id: message.id, + payload: payload, + type: message.messageType + ) + return signedMessage + } + + func interceptConnection( + _ request: AppSyncConnectionRequest, + for endpoint: URL + ) -> AppSyncConnectionRequest { + let host = endpoint.host! + let authToken = authTokenProvider.getLatestAuthToken() + + let authHeader = TokenAuthHeader(token: authToken, host: host) + let base64Auth = AppSyncJSONHelper.base64AuthenticationBlob(authHeader) + + let payloadData = SubscriptionConstants.emptyPayload.data(using: .utf8) + let payloadBase64 = payloadData?.base64EncodedString() + + guard var urlComponents = URLComponents(url: request.url, resolvingAgainstBaseURL: false) else { + return request + } + let headerQuery = URLQueryItem(name: RealtimeProviderConstants.header, value: base64Auth) + let payloadQuery = URLQueryItem(name: RealtimeProviderConstants.payload, value: payloadBase64) + urlComponents.queryItems = [headerQuery, payloadQuery] + guard let url = urlComponents.url else { + return request + } + let signedRequest = AppSyncConnectionRequest(url: url) + return signedRequest + } +} + +// MARK: - TokenAuthenticationHeader +/// Authentication header for user pool based auth +private class TokenAuthHeader: AuthenticationHeader { + let authorization: String + + init(token: String, host: String) { + self.authorization = token + super.init(host: host) + } + + private enum CodingKeys: String, CodingKey { + case authorization = "Authorization" + } + + override func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(authorization, forKey: .authorization) + try super.encode(to: encoder) + } +} diff --git a/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift index de32ac7c..8239a681 100644 --- a/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift +++ b/AWSAppSyncClient/Internal/SubscriptionFactory/LambdaBasedConnectionPool.swift @@ -22,8 +22,7 @@ class LambdaBasedConnectionPool: SubscriptionConnectionPool { return AppSyncSubscriptionConnection(provider: connectionProvider) } - let authProvider = AppSyncRealTimeClientOIDCAuthProvider(authProvider: tokenProvider) - let authInterceptor = OIDCAuthInterceptor(authProvider) + let authInterceptor = LambdaAuthInterceptor(authTokenProvider: tokenProvider) let connectionProvider = ConnectionProviderFactory.createConnectionProvider(for: url, authInterceptor: authInterceptor, connectionType: connectionType) From ea73e9b7f73fc72fe50b2d1e8397be955ed6fa88 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Wed, 28 Jul 2021 10:26:10 -0700 Subject: [PATCH 11/12] add more unit tests --- .../AWSAppSyncAuthTypeTests.swift | 16 ++++++++++++++ .../AWSAppSyncClientConfigurationTests.swift | 21 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift b/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift index b66837e9..78bee83a 100644 --- a/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift +++ b/AWSAppSyncUnitTests/AWSAppSyncAuthTypeTests.swift @@ -60,6 +60,10 @@ class AWSAppSyncAuthTypeTests: XCTestCase { try performSuccessDecodableTest(inputString: "AMAZON_COGNITO_USER_POOLS", expectedOutput: .amazonCognitoUserPools) } + func test_SuccessfulDecodable_AWSLambda() throws { + try performSuccessDecodableTest(inputString: "AWS_LAMBDA", expectedOutput: .awsLambda) + } + func test_FailureDecodable_BadData() throws { let inputData = try JSONSerialization.data(withJSONObject: "INVALID_DATA", options: .fragmentsAllowed) XCTAssertThrowsError(try JSONDecoder().decode(AWSAppSyncAuthType.self, from: inputData)) @@ -83,6 +87,10 @@ class AWSAppSyncAuthTypeTests: XCTestCase { try performSuccessEncodableTest(inputType: .amazonCognitoUserPools, expectedString: "AMAZON_COGNITO_USER_POOLS") } + func test_SuccessfulEncodable_AWSLambda() throws { + try performSuccessEncodableTest(inputType: .awsLambda, expectedString: "AWS_LAMBDA") + } + // MARK: - Tests: Hashable func test_Hashable_AwsIAM() { @@ -109,6 +117,14 @@ class AWSAppSyncAuthTypeTests: XCTestCase { XCTAssertNotEqual(oidcToken.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) } + func test_Hashable_AWSLambdaToken() { + let lambdaToken = AWSAppSyncAuthType.awsLambda + XCTAssertEqual(lambdaToken.hashValue, AWSAppSyncAuthType.awsLambda.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.awsIAM.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.apiKey.hashValue) + XCTAssertNotEqual(lambdaToken.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) + } + func test_Hashable_AmazonCognitoUserPools() { let amazonCognitoUserPools = AWSAppSyncAuthType.amazonCognitoUserPools XCTAssertEqual(amazonCognitoUserPools.hashValue, AWSAppSyncAuthType.amazonCognitoUserPools.hashValue) diff --git a/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift b/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift index fa24f740..1336388a 100644 --- a/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift +++ b/AWSAppSyncUnitTests/AWSAppSyncClientConfigurationTests.swift @@ -355,6 +355,27 @@ class AWSAppSyncClientConfigurationTests: XCTestCase { XCTAssert(clientInfoError.localizedDescription.starts(with: "Invalid Auth Configuration"), "Expected error to begin with 'Invalid Auth Configuration', but got '\(clientInfoError.localizedDescription)'") } } + + func testInitializer_MultipleProviders_AWSLambda() { + let serviceConfig = MockAWSAppSyncServiceConfig( + endpoint: URL(string: "http://www.amazon.com/for_unit_testing")!, + region: .USEast1, + authType: .awsLambda + ) + + do { + let _ = try AWSAppSyncClientConfiguration(appSyncServiceConfig: serviceConfig, + apiKeyAuthProvider: MockAWSAPIKeyAuthProvider(), + awsLambdaAuthProvider: MockLambdaAuthProvider()) + XCTFail("Expected validation to fail with multiple auth providers") + } catch { + guard let clientInfoError = error as? AWSAppSyncClientConfigurationError else { + XCTFail("Expected validation to throw AWSAppSyncClientInfoError if specifying multiple providers, but got \(type(of: error))") + return + } + XCTAssert(clientInfoError.localizedDescription.starts(with: "Invalid Auth Configuration"), "Expected error to begin with 'Invalid Auth Configuration', but got '\(clientInfoError.localizedDescription)'") + } + } // MARK: - Test other derived properties From a558d041dd06c45a8ea3661343e2be7faee25074 Mon Sep 17 00:00:00 2001 From: Diego Costantino Date: Wed, 28 Jul 2021 10:29:26 -0700 Subject: [PATCH 12/12] add connection pool unit test --- AWSAppSyncClient.xcodeproj/project.pbxproj | 4 + .../LambdaBasedConnectionPoolTests.swift | 76 +++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100644 AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift diff --git a/AWSAppSyncClient.xcodeproj/project.pbxproj b/AWSAppSyncClient.xcodeproj/project.pbxproj index 12c5ec13..cba6da17 100644 --- a/AWSAppSyncClient.xcodeproj/project.pbxproj +++ b/AWSAppSyncClient.xcodeproj/project.pbxproj @@ -124,6 +124,7 @@ 7638897026A9E4D70061AF0B /* LambdaBasedConnectionPool.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */; }; 763C857726B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */; }; 763C857926B1C262005164B2 /* LambdaAuthInterceptor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */; }; + 763C857B26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */; }; 8032C5415EF414C038394D69 /* Pods_AWSAppSyncTestCommon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 74071C397A83DEA980BB2F4C /* Pods_AWSAppSyncTestCommon.framework */; }; 90DE0C49240A304D000E875B /* AWSAppSyncAuthTypeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 90DE0C48240A304D000E875B /* AWSAppSyncAuthTypeTests.swift */; }; A70604C0C722923A70C937A1 /* Pods_AWSAppSyncTestApp.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57F5A94352E1ABE35159489D /* Pods_AWSAppSyncTestApp.framework */; }; @@ -516,6 +517,7 @@ 7638896F26A9E4D70061AF0B /* LambdaBasedConnectionPool.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPool.swift; sourceTree = ""; }; 763C857626B08D74005164B2 /* AWSAppSyncLambdaAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AWSAppSyncLambdaAuthTests.swift; sourceTree = ""; }; 763C857826B1C262005164B2 /* LambdaAuthInterceptor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaAuthInterceptor.swift; sourceTree = ""; }; + 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LambdaBasedConnectionPoolTests.swift; sourceTree = ""; }; 82E714A3E9BFB80BD9E5EF90 /* Pods-ApolloTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ApolloTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-ApolloTests/Pods-ApolloTests.debug.xcconfig"; sourceTree = ""; }; 8623D545D4837963CF2FFF02 /* Pods-AWSAppSyncUnitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AWSAppSyncUnitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AWSAppSyncUnitTests/Pods-AWSAppSyncUnitTests.debug.xcconfig"; sourceTree = ""; }; 8C707001F57B091A8A001CAB /* Pods_AWSAppSyncTestHostApp.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AWSAppSyncTestHostApp.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -977,6 +979,7 @@ 2171808C23FDB28100E520C9 /* UserPoolsBasedConnectionPoolTests.swift */, 2171808D23FDB28100E520C9 /* OIDCBasedConnectionPoolTests.swift */, 2171808E23FDB28100E520C9 /* IAMBasedConnectionPoolTests.swift */, + 763C857A26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift */, ); path = ConnectionPool; sourceTree = ""; @@ -2075,6 +2078,7 @@ 21933B3E24DA629B00F4D741 /* JSONValueSerializationTests.swift in Sources */, E47789592284B3DC008E7D6E /* MockAWSAppSyncServiceConfig.swift in Sources */, FA4F0D9521D6ED9E0099D165 /* AppSyncClientComplexObjectMutationUnitTests.swift in Sources */, + 763C857B26B1CB18005164B2 /* LambdaBasedConnectionPoolTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift new file mode 100644 index 00000000..083898f3 --- /dev/null +++ b/AWSAppSyncUnitTests/Subscription/Connection/ConnectionPool/LambdaBasedConnectionPoolTests.swift @@ -0,0 +1,76 @@ +// +// Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +// Licensed under the Amazon Software License +// http://aws.amazon.com/asl/ +// + +import XCTest +@testable import AWSAppSync +@testable import AWSAppSyncTestCommon + +class LambdaBasedConnectionPoolTests: XCTestCase { + + var connectionPool: LambdaBasedConnectionPool! + + let url = URL(string: "http://appsyncendpoint.com/graphql")! + let url2 = URL(string: "http://appsyncendpoint-2.com/graphql")! + + override func setUp() { + connectionPool = LambdaBasedConnectionPool(MockLambdaAuthProvider()) + } + + /// Test retrieve connection + /// + /// - Given: A connection pool + /// - When: + /// - I call connection(for:connectionType:) + /// - Then: + /// - I should get a non-nil connection + /// + func testRetrieveConnection() { + let connection = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection) + } + + /// Test retrieving multiple connection using the same url + /// + /// - Given: A connection pool + /// - When: + /// - I try to retrieve multiple connection with same url + /// - Then: + /// - I should get non-nil connections for each request. And the internal count of provider should be 1. + /// + func testRetreiveMultipleConnectionSameUrl() { + let connection1 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection1) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider1 = connectionPool.endPointToProvider[url.absoluteString] + + let connection2 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection2) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider2 = connectionPool.endPointToProvider[url.absoluteString] + XCTAssertTrue(provider1 === provider2, "Internal connection provider should be same") + } + + /// Test retrieving multiple connection using the different url + /// + /// - Given: A connection pool + /// - When: + /// - I try to retrieve multiple connection with 2 different urls + /// - Then: + /// - I should get non-nil connections for each request. And the internal count of provider should be 2. + /// + func testRetreiveMultipleConnectionDifferentUrl() { + let connection1 = connectionPool.connection(for: url, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection1) + XCTAssertEqual(connectionPool.endPointToProvider.count, 1, "Only one connection provider should be created") + let provider1 = connectionPool.endPointToProvider[url.absoluteString] + + let connection2 = connectionPool.connection(for: url2, connectionType: .appSyncRealtime) + XCTAssertNotNil(connection2) + XCTAssertEqual(connectionPool.endPointToProvider.count, 2, "Second connection provider should be created") + let provider2 = connectionPool.endPointToProvider[url2.absoluteString] + XCTAssertFalse(provider1 === provider2, "Internal connection provider should not be same") + } +}