diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme index 6970be959c..c13ed00ec0 100644 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme +++ b/.swiftpm/xcode/xcshareddata/xcschemes/Amplify-Package.xcscheme @@ -741,6 +741,26 @@ ReferencedContainer = "container:"> + + + + + + + + AnyPublisher + func unsubscribe(id: String) async throws +} /** The AppSyncRealTimeClient conforms to the AppSync real-time WebSocket protocol. @@ -205,7 +213,7 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol { - Parameters: - id: unique identifier of the subscription. */ - func unsubscribe(id: String) async throws { + public func unsubscribe(id: String) async throws { defer { log.debug("[AppSyncRealTimeClient] deleted subscription with id: \(id)") subscriptions.removeValue(forKey: id) @@ -287,7 +295,7 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol { .map { response -> AppSyncSubscriptionEvent? in switch response.type { case .connectionError, .error: - return .error(Self.decodeAppSyncRealTimeResponseError(response.payload)) + return response.payload.map { .error($0) } case .data: return response.payload.map { .data($0) } default: @@ -298,38 +306,6 @@ actor AppSyncRealTimeClient: AppSyncRealTimeClientProtocol { .eraseToAnyPublisher() } - private static func decodeAppSyncRealTimeResponseError(_ data: JSONValue?) -> [Error] { - let knownAppSyncRealTimeRequestErorrs = - Self.decodeAppSyncRealTimeRequestError(data) - .filter { !$0.isUnknown } - if knownAppSyncRealTimeRequestErorrs.isEmpty { - let graphQLErrors = Self.decodeGraphQLErrors(data) - return graphQLErrors.isEmpty - ? [APIError.operationError("Failed to decode AppSync error response", "", nil)] - : graphQLErrors - } else { - return knownAppSyncRealTimeRequestErorrs - } - } - - private static func decodeGraphQLErrors(_ data: JSONValue?) -> [GraphQLError] { - do { - return try GraphQLErrorDecoder.decodeAppSyncErrors(data) - } catch { - log.debug("[AppSyncRealTimeClient] Failed to decode errors: \(error)") - return [] - } - } - - private static func decodeAppSyncRealTimeRequestError(_ data: JSONValue?) -> [AppSyncRealTimeRequest.Error] { - guard let errorsJson = data?.errors else { - log.error("[AppSyncRealTimeClient] No 'errors' field found in response json") - return [] - } - let errors = errorsJson.asArray ?? [errorsJson] - return errors.compactMap(AppSyncRealTimeRequest.parseResponseError(error:)) - } - private func bindCancellableToConnection(_ cancellable: AnyCancellable) { cancellable.store(in: &cancellablesBindToConnection) } @@ -438,15 +414,15 @@ extension Publisher where Output == AppSyncRealTimeSubscription.State, Failure = } extension AppSyncRealTimeClient: DefaultLogger { - static var log: Logger { + public static var log: Logger { Amplify.Logging.logger(forCategory: CategoryType.api.displayName, forNamespace: String(describing: self)) } - nonisolated var log: Logger { Self.log } + public nonisolated var log: Logger { Self.log } } extension AppSyncRealTimeClient: Resettable { - func reset() async { + public func reset() async { subject.send(completion: .finished) cancellables = Set() cancellablesBindToConnection = Set() diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequest.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequest.swift index 19599820b4..10fb87013a 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequest.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequest.swift @@ -7,18 +7,23 @@ import Foundation -import Combine import Amplify -public enum AppSyncRealTimeRequest { +enum AppSyncRealTimeRequest { case connectionInit case start(StartRequest) case stop(String) - public struct StartRequest { + struct StartRequest { let id: String let data: String let auth: AppSyncRealTimeRequestAuth? + + init(id: String, data: String, auth: AppSyncRealTimeRequestAuth?) { + self.id = id + self.data = data + self.auth = auth + } } var id: String? { @@ -78,7 +83,7 @@ extension AppSyncRealTimeRequest { case unauthorized case unknown(message: String? = nil, causedBy: Swift.Error? = nil, payload: [String: Any]?) - var isUnknown: Bool { + public var isUnknown: Bool { if case .unknown = self { return true } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequestAuth.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequestAuth.swift index 87e01b1842..fffaf97faf 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequestAuth.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeRequestAuth.swift @@ -8,30 +8,48 @@ import Foundation -public enum AppSyncRealTimeRequestAuth { +enum AppSyncRealTimeRequestAuth { case authToken(AuthToken) case apiKey(ApiKey) case iam(IAM) - public struct AuthToken { + struct AuthToken { let host: String let authToken: String + + init(host: String, authToken: String) { + self.host = host + self.authToken = authToken + } } - public struct ApiKey { + struct ApiKey { let host: String let apiKey: String let amzDate: String + + init(host: String, apiKey: String, amzDate: String) { + self.host = host + self.apiKey = apiKey + self.amzDate = amzDate + } } - public struct IAM { + struct IAM { let host: String let authToken: String let securityToken: String let amzDate: String + + init(host: String, authToken: String, securityToken: String, amzDate: String) { + self.host = host + self.authToken = authToken + self.securityToken = securityToken + self.amzDate = amzDate + } } - public struct URLQuery { + struct URLQuery { let header: AppSyncRealTimeRequestAuth let payload: String @@ -53,7 +71,7 @@ public enum AppSyncRealTimeRequestAuth { urlComponents.queryItems = [ URLQueryItem(name: "header", value: headerJsonData.base64EncodedString()), - URLQueryItem(name: "payload", value: try? payload.base64EncodedString()) + URLQueryItem(name: "payload", value: payload.data(using: .utf8)?.base64EncodedString()) ] return urlComponents.url ?? url diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeResponse.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeResponse.swift index dfec371035..36813b4bbb 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeResponse.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeResponse.swift @@ -8,13 +8,13 @@ import Foundation import Amplify -public struct AppSyncRealTimeResponse { +struct AppSyncRealTimeResponse { - public let id: String? - public let payload: JSONValue? - public let type: EventType + let id: String? + let payload: JSONValue? + let type: EventType - public enum EventType: String, Codable { + enum EventType: String, Codable { case connectionAck = "connection_ack" case startAck = "start_ack" case stopAck = "complete" diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeSubscription.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeSubscription.swift index d7e4c6ef42..2997311b7b 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeSubscription.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncRealTimeSubscription.swift @@ -9,7 +9,7 @@ import Foundation import Combine import Amplify -@_spi(WebSocket) import AWSPluginsCore +@_spi(RetryWithJitter) import InternalAmplifyNetwork /** AppSyncRealTimeSubscription reprensents one realtime subscription to AppSync realtime server. @@ -36,8 +36,8 @@ actor AppSyncRealTimeSubscription { private weak var appSyncRealTimeClient: AppSyncRealTimeClient? - public let id: String - public let query: String + let id: String + let query: String init(id: String, query: String, appSyncRealTimeClient: AppSyncRealTimeClient) { @@ -121,9 +121,9 @@ actor AppSyncRealTimeSubscription { } extension AppSyncRealTimeSubscription: DefaultLogger { - static var log: Logger { + public static var log: Logger { Amplify.Logging.logger(forCategory: CategoryType.api.displayName, forNamespace: String(describing: self)) } - nonisolated var log: Logger { Self.log } + nonisolated public var log: Logger { Self.log } } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncSubscriptionEvent.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncSubscriptionEvent.swift index ec86c53e6a..fbc0d27671 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncSubscriptionEvent.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncSubscriptionEvent.swift @@ -9,10 +9,10 @@ import Foundation import Amplify -public enum AppSyncSubscriptionEvent { +enum AppSyncSubscriptionEvent { case subscribing case subscribed case data(JSONValue) case unsubscribed - case error([Error]) + case error(JSONValue) } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncWebSocketClientProtocol.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncWebSocketClientProtocol.swift index d7d9cadc29..8969aaa1e3 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncWebSocketClientProtocol.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/AppSyncRealTimeClient/AppSyncWebSocketClientProtocol.swift @@ -8,7 +8,7 @@ import Foundation import Combine -@_spi(WebSocket) import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork protocol AppSyncWebSocketClientProtocol: AnyObject { var isConnected: Bool { get async } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/APIKeyAuthInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/APIKeyAuthInterceptor.swift index f52ded490e..7174bfd414 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/APIKeyAuthInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/APIKeyAuthInterceptor.swift @@ -8,7 +8,7 @@ import Foundation import Amplify -@_spi(WebSocket) import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork class APIKeyAuthInterceptor { private let apiKey: String @@ -31,7 +31,10 @@ extension APIKeyAuthInterceptor: WebSocketInterceptor { extension APIKeyAuthInterceptor: AppSyncRequestInterceptor { func interceptRequest(event: AppSyncRealTimeRequest, url: URL) async -> AppSyncRealTimeRequest { - let host = AppSyncRealTimeClientFactory.appSyncApiEndpoint(url).host! + guard let host = AppSyncRealTimeClientFactory.appSyncApiEndpoint(url).host else { + return event + } + guard case .start(let request) = event else { return event } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/AuthTokenInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/AuthTokenInterceptor.swift index b0f19ffd78..c5ed1b50e7 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/AuthTokenInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/AuthTokenInterceptor.swift @@ -7,7 +7,7 @@ import Foundation import Amplify -@_spi(WebSocket) import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork /// General purpose authenticatication subscriptions interceptor for providers whose only /// requirement is to provide an authentication token via the "Authorization" header diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift index c3d33320c2..41d4aa26b6 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Interceptor/SubscriptionInterceptor/IAMAuthInterceptor.swift @@ -6,10 +6,11 @@ // import Foundation -@_spi(WebSocket) import AWSPluginsCore import Amplify import AWSClientRuntime import ClientRuntime +import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork class IAMAuthInterceptor { @@ -114,7 +115,7 @@ extension IAMAuthInterceptor: AppSyncRequestInterceptor { return .start(.init( id: request.id, data: request.data, - auth: authHeader.map { .iam($0) } + auth: authHeader.map { AppSyncRealTimeRequestAuth.iam($0) } )) } } diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift index 44e2cf378d..09dfa9d3c7 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/Operation/AWSGraphQLSubscriptionTaskRunner.swift @@ -147,8 +147,8 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, case .unsubscribed: send(GraphQLSubscriptionEvent.connection(.disconnected)) finish() - case .error(let errors): - fail(toAPIError(errors, type: R.self)) + case .error(let payload): + fail(toAPIError(Self.decodeAppSyncRealTimeResponseError(payload), type: R.self)) } } @@ -178,6 +178,38 @@ public class AWSGraphQLSubscriptionTaskRunner: InternalTaskRunner, } } + internal static func decodeAppSyncRealTimeResponseError(_ data: JSONValue?) -> [Error] { + let knownAppSyncRealTimeRequestErorrs = + decodeAppSyncRealTimeRequestError(data) + .filter { !$0.isUnknown } + if knownAppSyncRealTimeRequestErorrs.isEmpty { + let graphQLErrors = decodeGraphQLErrors(data) + return graphQLErrors.isEmpty + ? [APIError.operationError("Failed to decode AppSync error response", "", nil)] + : graphQLErrors + } else { + return knownAppSyncRealTimeRequestErorrs + } + } + + internal static func decodeGraphQLErrors(_ data: JSONValue?) -> [GraphQLError] { + do { + return try GraphQLErrorDecoder.decodeAppSyncErrors(data) + } catch { + print("Failed to decode errors: \(error)") + return [] + } + } + + internal static func decodeAppSyncRealTimeRequestError(_ data: JSONValue?) -> [AppSyncRealTimeRequest.Error] { + guard let errorsJson = data?.errors else { + print("No 'errors' field found in response json") + return [] + } + let errors = errorsJson.asArray ?? [errorsJson] + return errors.compactMap(AppSyncRealTimeRequest.parseResponseError(error:)) + } + } // Class is still necessary. See https://github.com/aws-amplify/amplify-swift/issues/2252 @@ -329,8 +361,11 @@ final public class AWSGraphQLSubscriptionOperation: GraphQLSubscri dispatchInProcess(data: GraphQLSubscriptionEvent.connection(.disconnected)) dispatch(result: .successfulVoid) finish() - case .error(let errors): - dispatch(result: .failure(toAPIError(errors, type: R.self))) + case .error(let payload): + dispatch(result: .failure(toAPIError( + AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeResponseError(payload), + type: R.self + ))) finish() } } @@ -438,3 +473,4 @@ fileprivate func toAPIError(_ errors: [Error], type: R.Type) -> AP } #endif } + diff --git a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift index 1666312feb..3f8a7a5d5e 100644 --- a/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift +++ b/AmplifyPlugins/API/Sources/AWSAPIPlugin/SubscriptionFactory/AppSyncRealTimeClientFactory.swift @@ -9,7 +9,9 @@ import Foundation import Amplify import Combine -@_spi(WebSocket) import AWSPluginsCore +import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork + protocol AppSyncRealTimeClientFactoryProtocol { func getAppSyncRealTimeClient( @@ -21,13 +23,6 @@ protocol AppSyncRealTimeClientFactoryProtocol { ) async throws -> AppSyncRealTimeClientProtocol } -protocol AppSyncRealTimeClientProtocol { - func connect() async throws - func disconnectWhenIdel() async - func disconnect() async - func subscribe(id: String, query: String) async throws -> AnyPublisher - func unsubscribe(id: String) async throws -} actor AppSyncRealTimeClientFactory: AppSyncRealTimeClientFactoryProtocol { struct MapperCacheKey: Hashable { diff --git a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift index 5e1ed6b31c..3966917b5e 100644 --- a/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift +++ b/AmplifyPlugins/API/Tests/APIHostApp/AWSAPIPluginFunctionalTests/AppSyncRealTimeClientTests.swift @@ -10,7 +10,8 @@ import XCTest import Combine @testable import Amplify @testable import AWSAPIPlugin -@testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork class AppSyncRealTimeClientTests: XCTestCase { let subscriptionRequest = """ @@ -154,10 +155,12 @@ class AppSyncRealTimeClientTests: XCTestCase { maxSubscriptionReachedError.assertForOverFulfill = false let retryTriggerredAndSucceed = expectation(description: "Retry on max subscription reached error and succeed") cancellables.append(try await makeOneSubscription { event in - if case .error(let errors) = event { - XCTAssertTrue(errors.count == 1) - XCTAssertTrue(errors[0] is AppSyncRealTimeRequest.Error) - if case .maxSubscriptionsReached = errors[0] as! AppSyncRealTimeRequest.Error { + if case .error(let payload) = event, + let error = payload.errors { + let errors = error.asArray ?? [error] + let requestErrors = errors.compactMap(AppSyncRealTimeRequest.parseResponseError(error:)) + XCTAssertTrue(requestErrors.count == 1) + if case .maxSubscriptionsReached = requestErrors[0] { maxSubscriptionReachedError.fulfill() cancellables.dropLast(10).forEach { $0?.cancel() } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AppSyncRealTimeClient/AppSyncRealTimeClientTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AppSyncRealTimeClient/AppSyncRealTimeClientTests.swift index 279ca304d3..dc68e851cc 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AppSyncRealTimeClient/AppSyncRealTimeClientTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/AppSyncRealTimeClient/AppSyncRealTimeClientTests.swift @@ -9,8 +9,8 @@ import XCTest import Combine import Amplify -@_spi(WebSocket) import AWSPluginsCore @testable import AWSAPIPlugin +@_spi(WebSocket) import InternalAmplifyNetwork class AppSyncRealTimeClientTests: XCTestCase { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/SubscriptionInterceptor/CognitoAuthInterceptorTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/SubscriptionInterceptor/CognitoAuthInterceptorTests.swift index 4127f018fd..827ed88dac 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/SubscriptionInterceptor/CognitoAuthInterceptorTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Interceptor/SubscriptionInterceptor/CognitoAuthInterceptorTests.swift @@ -9,7 +9,7 @@ import XCTest import Amplify @testable import AWSAPIPlugin -@testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore class CognitoAuthInterceptorTests: XCTestCase { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift index 2ba9f97779..9840a28170 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Mocks/MockSubscription.swift @@ -10,7 +10,8 @@ import Foundation import Amplify import Combine @testable import AWSAPIPlugin -@_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore +@_spi(WebSocket) import InternalAmplifyNetwork struct MockSubscriptionConnectionFactory: AppSyncRealTimeClientFactoryProtocol { diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionOperationCancelTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionOperationCancelTests.swift index 95fc5b8e63..f0b9dcf390 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionOperationCancelTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionOperationCancelTests.swift @@ -10,7 +10,7 @@ import XCTest @testable import Amplify @testable import AWSAPIPlugin @testable import AmplifyTestCommon -@testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore @testable import AWSPluginsTestCommon // swiftlint:disable:next type_name diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerCancelTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerTests.swift similarity index 66% rename from AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerCancelTests.swift rename to AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerTests.swift index a06001515f..f96b8fe5bd 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerCancelTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/AWSGraphQLSubscriptionTaskRunnerTests.swift @@ -14,7 +14,7 @@ import XCTest @testable import AWSPluginsTestCommon // swiftlint:disable:next type_name -class AWSGraphQLSubscriptionTaskRunnerCancelTests: XCTestCase { +class AWSGraphQLSubscriptionTaskRunnerTests: XCTestCase { var apiPlugin: AWSAPIPlugin! var authService: MockAWSAuthService! var pluginConfig: AWSAPICategoryPluginConfiguration! @@ -183,4 +183,102 @@ class AWSGraphQLSubscriptionTaskRunnerCancelTests: XCTestCase { subscriptionEvents.cancel() await fulfillment(of: [receivedFailure, receivedCompletion], timeout: 5) } + + func testDecodeAppSyncRealTimeResponseError_withEmptyJsonValue_failedToDecode() { + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeResponseError(nil) + XCTAssertEqual(errors.count, 1) + if case .some(.operationError(let description, _, _)) = errors.first as? APIError { + XCTAssertTrue(description.contains("Failed to decode AppSync error response")) + } else { + XCTFail("Should be failed with APIError") + } + } + + func testDecodeAppSYncRealTimeResponseError_withKnownAppSyncRealTimeRequestError_returnKnownErrors() { + let errorJson: JSONValue = [ + "errors": [[ + "message": "test1", + "errorType": "MaxSubscriptionsReachedError" + ]] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeResponseError(errorJson) + XCTAssertEqual(errors.count, 1) + guard case .maxSubscriptionsReached = errors.first as? AppSyncRealTimeRequest.Error else { + XCTFail("Should be AppSyncRealTimeRequestError") + return + } + } + + func testDecodeAppSYncRealTimeResponseError_withUnknownError_returnParsedGraphQLError() { + let errorJson: JSONValue = [ + "errors": [[ + "message": "test1", + "errorType": "Unknown" + ]] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeResponseError(errorJson) + XCTAssertEqual(errors.count, 1) + XCTAssertTrue(errors.first is GraphQLError) + } + + func testDecodeAppSyncRealTimeRequestError_withoutErrorsField_returnEmptyErrors() { + let errorJson: JSONValue = [ + "noErrors": [[ + "message": "test1", + "errorType": "Unknown" + ]] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeRequestError(errorJson) + XCTAssertEqual(errors.count, 0) + } + + func testDecodeAppSyncRealTimeRequestError_withWellFormatErrors_parseErrors() { + let errorJson: JSONValue = [ + "errors": [[ + "message": "test1", + "errorType": "MaxSubscriptionsReachedError" + ], [ + "message": "test2", + "errorType": "LimitExceededError" + ], [ + "message": "test3", + "errorType": "Unauthorized" + ]] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeRequestError(errorJson) + XCTAssertEqual(errors.count, 3) + } + + func testDecodeAppSyncRealTimeRequestError_withSomeWellFormatErrors_parseErrors() { + let errorJson: JSONValue = [ + "errors": [[ + "message": "test1", + "errorType": "MaxSubscriptionsReachedError" + ], [ + "message": "test2", + "errorType": "LimitExceededError" + ], [ + "random": "123" + ]] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeRequestError(errorJson) + XCTAssertEqual(errors.count, 2) + } + + func testDecodeAppSyncRealTimeRequestError_withSingletonErrors_parseErrors() { + let errorJson: JSONValue = [ + "errors": [ + "message": "test1", + "errorType": "MaxSubscriptionsReachedError" + ] + ] + + let errors = AWSGraphQLSubscriptionTaskRunner.decodeAppSyncRealTimeRequestError(errorJson) + XCTAssertEqual(errors.count, 1) + } } diff --git a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/GraphQLSubscribeTaskTests.swift b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/GraphQLSubscribeTaskTests.swift index d632e131c7..9d0257d6c7 100644 --- a/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/GraphQLSubscribeTaskTests.swift +++ b/AmplifyPlugins/API/Tests/AWSAPIPluginTests/Operation/GraphQLSubscribeTaskTests.swift @@ -130,7 +130,11 @@ class GraphQLSubscribeTasksTests: OperationTestBase { await fulfillment(of: [onSubscribeInvoked], timeout: 0.05) try await MockAppSyncRealTimeClient.waitForSubscirbing() - mockAppSyncRealTimeClient?.triggerEvent(.error([AppSyncRealTimeRequest.Error.limitExceeded])) + mockAppSyncRealTimeClient?.triggerEvent(.error([ + "errors": [ + "errorType": "LimitExceededError" + ] + ])) expectedCompletionFailureError = APIError.operationError("", "", AppSyncRealTimeRequest.Error.limitExceeded) await waitForSubscriptionExpectations() } @@ -145,18 +149,21 @@ class GraphQLSubscribeTasksTests: OperationTestBase { try await subscribe() await fulfillment(of: [onSubscribeInvoked], timeout: 0.05) - let unauthorizedError = GraphQLError(message: "", extensions: ["errorType": "Unauthorized"]) try await MockAppSyncRealTimeClient.waitForSubscirbing() - mockAppSyncRealTimeClient?.triggerEvent(.error([unauthorizedError])) + mockAppSyncRealTimeClient?.triggerEvent(.error([ + "errors": [ + "errorType": "Unauthorized" + ] + ])) expectedCompletionFailureError = APIError.operationError( "Subscription item event failed with error: Unauthorized", "", - GraphQLResponseError.error([unauthorizedError]) + AppSyncRealTimeRequest.Error.unauthorized ) await waitForSubscriptionExpectations() } - func testConnectionErrorWithAppSyncConnectionError() async throws { + func testConnectionErrorWithOtherAppSyncConnectionError() async throws { receivedCompletionSuccess.isInverted = true receivedStateValueConnected.isInverted = true receivedStateValueDisconnected.isInverted = true @@ -167,8 +174,16 @@ class GraphQLSubscribeTasksTests: OperationTestBase { await fulfillment(of: [onSubscribeInvoked], timeout: 0.05) try await MockAppSyncRealTimeClient.waitForSubscirbing() - mockAppSyncRealTimeClient?.triggerEvent(.error([URLError(URLError.Code(rawValue: 400))])) - expectedCompletionFailureError = APIError.operationError("", "", URLError(URLError.Code(rawValue: 400))) + mockAppSyncRealTimeClient?.triggerEvent(.error([ + "errors": [ + "message": "other error" + ] + ])) + expectedCompletionFailureError = APIError.operationError( + "Subscription item event failed with error", + "", + GraphQLResponseError.error([GraphQLError(message: "other error")]) + ) await waitForSubscriptionExpectations() } diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/AmplifyNetworkMonitor.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/AmplifyNetworkMonitor.swift similarity index 97% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/AmplifyNetworkMonitor.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/AmplifyNetworkMonitor.swift index 23eb1ec4e2..e1ac3c9c5c 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/AmplifyNetworkMonitor.swift +++ b/AmplifyPlugins/Internal/Sources/Network/WebSocket/AmplifyNetworkMonitor.swift @@ -9,7 +9,7 @@ import Network import Combine -@_spi(WebSocket) +@_spi(NetworkReachability) public final class AmplifyNetworkMonitor { public enum State { diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/README.md b/AmplifyPlugins/Internal/Sources/Network/WebSocket/README.md similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/README.md rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/README.md diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/RetryWithJitter.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/RetryWithJitter.swift similarity index 98% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/RetryWithJitter.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/RetryWithJitter.swift index 9da51cb03f..29436f916f 100644 --- a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/RetryWithJitter.swift +++ b/AmplifyPlugins/Internal/Sources/Network/WebSocket/RetryWithJitter.swift @@ -8,7 +8,7 @@ import Foundation -@_spi(WebSocket) +@_spi(RetryWithJitter) public actor RetryWithJitter { public enum Error: Swift.Error { case maxRetryExceeded([Swift.Error]) diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketClient.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketClient.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketClient.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketEvent.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketEvent.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketEvent.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketEvent.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketInterceptor.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketInterceptor.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketInterceptor.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketInterceptor.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketNetworkMonitorProtocol.swift b/AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketNetworkMonitorProtocol.swift similarity index 100% rename from AmplifyPlugins/Core/AWSPluginsCore/WebSocket/WebSocketNetworkMonitorProtocol.swift rename to AmplifyPlugins/Internal/Sources/Network/WebSocket/WebSocketNetworkMonitorProtocol.swift diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/LocalWebSocketServer.swift b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/LocalWebSocketServer.swift similarity index 93% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/LocalWebSocketServer.swift rename to AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/LocalWebSocketServer.swift index 1dc0fbd948..0b8c47f16a 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/LocalWebSocketServer.swift +++ b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/LocalWebSocketServer.swift @@ -37,7 +37,7 @@ class LocalWebSocketServer { stack.applicationProtocols.insert(ws, at: 0) let port = NWEndpoint.Port(rawValue: portNumber)! guard let listener = try? NWListener(using: params, on: port) else { - throw "unable to start the listener at: localhost:\(port)" + throw LocalWebSocketServerError.error("unable to start the listener at: localhost:\(port)") } listener.newConnectionHandler = { [weak self] conn in @@ -93,7 +93,7 @@ class LocalWebSocketServer { func sendTransientFailureToConnections() { self.connections.forEach { - var metadata = NWProtocolWebSocket.Metadata(opcode: .close) + let metadata = NWProtocolWebSocket.Metadata(opcode: .close) metadata.closeCode = .protocolCode(NWProtocolWebSocket.CloseCode.Defined.internalServerError) $0.send( content: nil, @@ -103,3 +103,7 @@ class LocalWebSocketServer { } } } + +enum LocalWebSocketServerError: Error { + case error(String) +} diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/RetryWithJitterTests.swift b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/RetryWithJitterTests.swift similarity index 96% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/RetryWithJitterTests.swift rename to AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/RetryWithJitterTests.swift index 9ada954056..d1bbe4a22c 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/RetryWithJitterTests.swift +++ b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/RetryWithJitterTests.swift @@ -7,7 +7,8 @@ import XCTest -@testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore +@testable @_spi(RetryWithJitter) import InternalAmplifyNetwork class RetryWithJitterTests: XCTestCase { struct TestError: Error { diff --git a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/WebSocketClientTests.swift b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/WebSocketClientTests.swift similarity index 98% rename from AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/WebSocketClientTests.swift rename to AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/WebSocketClientTests.swift index f3e53669c1..51b7a2e8f9 100644 --- a/AmplifyPlugins/Core/AWSPluginsCoreTests/WebSocket/WebSocketClientTests.swift +++ b/AmplifyPlugins/Internal/Tests/NetworkTests/WebSocket/WebSocketClientTests.swift @@ -8,7 +8,8 @@ import XCTest import Combine -@testable @_spi(WebSocket) import AWSPluginsCore +@testable import AWSPluginsCore +@testable @_spi(WebSocket) @_spi(NetworkReachability) import InternalAmplifyNetwork fileprivate let timeout: TimeInterval = 5 diff --git a/Package.swift b/Package.swift index 25639dce9c..bc4ce0b4a5 100644 --- a/Package.swift +++ b/Package.swift @@ -115,7 +115,8 @@ let apiTargets: [Target] = [ name: "AWSAPIPlugin", dependencies: [ .target(name: "Amplify"), - .target(name: "AWSPluginsCore") + .target(name: "AWSPluginsCore"), + .target(name: "InternalAmplifyNetwork") ], path: "AmplifyPlugins/API/Sources/AWSAPIPlugin", exclude: [ @@ -315,6 +316,24 @@ let internalPinpointTargets: [Target] = [ ) ] +let internalNetworkingTargets: [Target] = [ + .target( + name: "InternalAmplifyNetwork", + dependencies: [ + .target(name: "Amplify") + ], + path: "AmplifyPlugins/Internal/Sources/Network" + ), + .testTarget( + name: "InternalAmplifyNetworkUnitTests", + dependencies: [ + "AmplifyTestCommon", + "InternalAmplifyNetwork" + ], + path: "AmplifyPlugins/Internal/Tests/NetworkTests" + ) +] + let analyticsTargets: [Target] = [ .target( name: "AWSPinpointAnalyticsPlugin", @@ -443,6 +462,7 @@ let targets: [Target] = amplifyTargets + analyticsTargets + pushNotificationsTargets + internalPinpointTargets + + internalNetworkingTargets + predictionsTargets + loggingTargets