From 6e7af189609213b2570fa54c7c51e79ca590e0c5 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 17 Feb 2022 23:09:14 -0800 Subject: [PATCH 01/21] Implement graphql-transport-ws protocol support --- .../DefaultImplementation/WebSocket.swift | 34 +++++++++++++++---- .../ApolloWebSocket/OperationMessage.swift | 4 ++- .../ApolloWebSocket/WebSocketTransport.swift | 10 +++++- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index c4c1bb3b6d..e4334173b5 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -68,14 +68,25 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock public let code: Int } - private struct Constants { + public enum WSProtocol: CustomStringConvertible { + case graphql_ws + case graphql_transport_ws + + public var description: String { + switch self { + case .graphql_ws: return "graphql-ws" + case .graphql_transport_ws: return "graphql-transport-ws" + } + } + } + + struct Constants { static let headerWSUpgradeName = "Upgrade" static let headerWSUpgradeValue = "websocket" static let headerWSHostName = "Host" static let headerWSConnectionName = "Connection" static let headerWSConnectionValue = "Upgrade" static let headerWSProtocolName = "Sec-WebSocket-Protocol" - static let headerWSProtocolValue = "graphql-ws" static let headerWSVersionName = "Sec-WebSocket-Version" static let headerWSVersionValue = "13" static let headerWSExtensionName = "Sec-WebSocket-Extensions" @@ -197,20 +208,29 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } - self.request.setValue(Constants.headerWSProtocolValue, - forHTTPHeaderField: Constants.headerWSProtocolName) + if self.request.value(forHTTPHeaderField: Constants.headerWSProtocolName) == nil { + self.request.setValue(WSProtocol.graphql_ws.description, + forHTTPHeaderField: Constants.headerWSProtocolName) + } writeQueue.maxConcurrentOperationCount = 1 } - public convenience init(url: URL) { + public convenience init(url: URL, webSocketProtocol: WSProtocol = .graphql_ws) { var request = URLRequest(url: url) request.timeoutInterval = 5 + request.setValue(webSocketProtocol.description, + forHTTPHeaderField: Constants.headerWSProtocolName) + self.init(request: request) } // Used for specifically setting the QOS for the write queue. - public convenience init(url: URL, writeQueueQOS: QualityOfService) { - self.init(url: url) + public convenience init( + url: URL, + writeQueueQOS: QualityOfService, + webSocketProtocol: WSProtocol = .graphql_ws + ) { + self.init(url: url, webSocketProtocol: webSocketProtocol) writeQueue.qualityOfService = writeQueueQOS } diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index d0719262b8..ae11cff405 100644 --- a/Sources/ApolloWebSocket/OperationMessage.swift +++ b/Sources/ApolloWebSocket/OperationMessage.swift @@ -7,6 +7,7 @@ final class OperationMessage { enum Types : String { case connectionInit = "connection_init" // Client -> Server case connectionTerminate = "connection_terminate" // Client -> Server + case subscribe = "subscribe" // Client -> Server case start = "start" // Client -> Server case stop = "stop" // Client -> Server @@ -17,6 +18,7 @@ final class OperationMessage { case data = "data" // Server -> Client case error = "error" // Server -> Client case complete = "complete" // Server -> Client + case next = "next" // Server -> Client } let serializationFormat = JSONSerializationFormat.self @@ -34,7 +36,7 @@ final class OperationMessage { init(payload: GraphQLMap? = nil, id: String? = nil, - type: Types = .start) { + type: Types) { var message: GraphQLMap = [:] if let payload = payload { message["payload"] = payload diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index aed9d25bec..8951a3a644 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -145,6 +145,7 @@ public class WebSocketTransport { switch messageType { case .data, + .next, .error: if let id = parseHandler.id, let responseHandler = subscribers[id] { if let payload = parseHandler.payload { @@ -185,6 +186,7 @@ public class WebSocketTransport { case .connectionInit, .connectionTerminate, + .subscribe, .start, .stop, .connectionError: @@ -270,7 +272,13 @@ public class WebSocketTransport { sendQueryDocument: true, autoPersistQuery: false) let identifier = operationMessageIdCreator.requestId() - guard let message = OperationMessage(payload: body, id: identifier).rawMessage else { + + var type: OperationMessage.Types = .start + if case WebSocket.WSProtocol.graphql_transport_ws.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) { + type = .subscribe + } + + guard let message = OperationMessage(payload: body, id: identifier, type: type).rawMessage else { return nil } From d60ae4ad480137d34cd85e16a0c30190a7fe34cb Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 18 Feb 2022 10:02:51 -0800 Subject: [PATCH 02/21] Add graphql-transport-ws integration test based on Apollo Server docs-examples --- Apollo.xcodeproj/project.pbxproj | 170 ++++++++++++++++++ .../xcshareddata/swiftpm/Package.resolved | 27 +++ .../Apollo-Target-SubscriptionAPI.xcconfig | 3 + Sources/SubscriptionAPI/API.swift | 51 ++++++ Sources/SubscriptionAPI/Info.plist | 24 +++ Sources/SubscriptionAPI/SubscriptionAPI.h | 11 ++ .../graphql/operation_ids.json | 6 + .../SubscriptionAPI/graphql/schema.graphqls | 7 + .../graphql/subscription.graphql | 4 + .../SubscriptionTests.swift | 58 ++++++ .../TestHelpers/TestServerURLs.swift | 2 + 11 files changed, 363 insertions(+) create mode 100644 Configuration/Apollo/Apollo-Target-SubscriptionAPI.xcconfig create mode 100644 Sources/SubscriptionAPI/API.swift create mode 100644 Sources/SubscriptionAPI/Info.plist create mode 100644 Sources/SubscriptionAPI/SubscriptionAPI.h create mode 100644 Sources/SubscriptionAPI/graphql/operation_ids.json create mode 100644 Sources/SubscriptionAPI/graphql/schema.graphqls create mode 100644 Sources/SubscriptionAPI/graphql/subscription.graphql create mode 100644 Tests/ApolloServerIntegrationTests/SubscriptionTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index e8cb61b051..1ee46182ea 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -251,9 +251,15 @@ DED46051261CEAD20086EF63 /* StarWarsAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FCE2CFA1E6C213D00E34457 /* StarWarsAPI.framework */; }; E616B6D126C3335600DB049E /* ExecutionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E616B6D026C3335600DB049E /* ExecutionTests.swift */; }; E61DD76526D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61DD76426D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift */; }; + E63C03DF27BDDC3D00D675C6 /* SubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */; }; + E63C03E227BDE00400D675C6 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; E657CDBA26FD01D4005834D6 /* ApolloSchemaInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */; }; E6630B8C26F0639B002D9E41 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB926EC05290094434A /* MockNetworkSession.swift */; }; E6630B8E26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */; }; + E6A19C6227BEDAE00099C6E3 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E6A19C6127BEDAE00099C6E3 /* Nimble */; }; + E6A19C6727BF0E1C0099C6E3 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A19C6527BF0E1C0099C6E3 /* API.swift */; }; + E6A901D727BDAFA100931C9E /* SubscriptionAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; + E6A901DC27BDB01200931C9E /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; E6C4267B26F16CB400904AD2 /* introspection_response.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C4267A26F16CB400904AD2 /* introspection_response.json */; }; E6D79AB826E9D59C0094434A /* URLDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */; }; E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; }; @@ -479,6 +485,20 @@ remoteGlobalIDString = 9FCE2CF91E6C213D00E34457; remoteInfo = StarWarsAPI; }; + E63C03E027BDDFEF00D675C6 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; + proxyType = 1; + remoteGlobalIDString = E6A901D327BDAFA100931C9E; + remoteInfo = SubscriptionAPI; + }; + E6A901DE27BDB01200931C9E /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9FC7503B1D2A532C00458D91 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 9FC750431D2A532C00458D91; + remoteInfo = Apollo; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -821,9 +841,18 @@ DED45FB3261CDEC60086EF63 /* Apollo-CodegenTestPlan.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = "Apollo-CodegenTestPlan.xctestplan"; sourceTree = ""; }; E616B6D026C3335600DB049E /* ExecutionTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ExecutionTests.swift; sourceTree = ""; }; E61DD76426D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SQLiteDotSwiftDatabaseBehaviorTests.swift; sourceTree = ""; }; + E63C03D327BDB55900D675C6 /* subscription.graphql */ = {isa = PBXFileReference; lastKnownFileType = text; path = subscription.graphql; sourceTree = ""; }; + E63C03D627BDBA8900D675C6 /* operation_ids.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = operation_ids.json; sourceTree = ""; }; + E63C03DB27BDD99100D675C6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTests.swift; sourceTree = ""; }; E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloSchemaInternalTests.swift; sourceTree = ""; }; + E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-SubscriptionAPI.xcconfig"; sourceTree = ""; }; E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemaRegistryApolloSchemaDownloaderTests.swift; sourceTree = ""; }; + E6A19C6527BF0E1C0099C6E3 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; + E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SubscriptionAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SubscriptionAPI.h; sourceTree = ""; }; E6C4267A26F16CB400904AD2 /* introspection_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = introspection_response.json; sourceTree = ""; }; + E6CE3DBA27BDB26E00B43E0A /* schema.graphqls */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphqls; sourceTree = ""; }; E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLDownloaderTests.swift; sourceTree = ""; }; E6D79AB926EC05290094434A /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = ""; }; E86D8E03214B32DA0028EFE1 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = ""; }; @@ -941,10 +970,12 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E63C03E227BDE00400D675C6 /* SubscriptionAPI.framework in Frameworks */, DECD498F262F840700924527 /* ApolloCodegenTestSupport.framework in Frameworks */, DECD4736262F668500924527 /* UploadAPI.framework in Frameworks */, DECD46FB262F659500924527 /* ApolloCodegenLib.framework in Frameworks */, DED46051261CEAD20086EF63 /* StarWarsAPI.framework in Frameworks */, + E6A19C6227BEDAE00099C6E3 /* Nimble in Frameworks */, DED46035261CEA660086EF63 /* ApolloTestSupport.framework in Frameworks */, DED45FE7261CE8C50086EF63 /* ApolloWebSocket.framework in Frameworks */, DED45FD0261CE88C0086EF63 /* ApolloSQLite.framework in Frameworks */, @@ -960,6 +991,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901D127BDAFA100931C9E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + E6A901DC27BDB01200931C9E /* Apollo.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ @@ -984,6 +1023,7 @@ DECD492F262F820500924527 /* Apollo-Target-CodegenTestSupport.xcconfig */, 90690D2522433CAF00FC2E54 /* Apollo-Target-TestSupport.xcconfig */, 9B2DFBC824E1FA7E00ED3AE6 /* Apollo-Target-UploadAPI.xcconfig */, + E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */, 9B7BDAD923FDECB400ACD198 /* ApolloSQLite-Project-Debug.xcconfig */, 9B7BDADC23FDECB400ACD198 /* ApolloSQLite-Project-Release.xcconfig */, 9B7BDAD823FDECB300ACD198 /* ApolloSQLite-Target-Framework.xcconfig */, @@ -1434,6 +1474,7 @@ 9BDF200723FDC37600153E2B /* GitHubAPI */, 9BCF0CE923FC9F060031D2A2 /* StarWarsAPI */, 9B2DFBC424E1FA3E00ED3AE6 /* UploadAPI */, + E6A901D527BDAFA100931C9E /* SubscriptionAPI */, DECD490C262F81BF00924527 /* ApolloCodegenTestSupport */, 9B7BDAF923FDEE8A00ACD198 /* Frameworks */, 90690D04224333DA00FC2E54 /* Configuration */, @@ -1462,6 +1503,7 @@ DE6B15AC26152BE10068D642 /* ApolloServerIntegrationTests.xctest */, DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */, DE058621266978A100265760 /* ApolloAPI.framework */, + E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */, ); name = Products; sourceTree = ""; @@ -1669,6 +1711,7 @@ DED45F49261CDBFC0086EF63 /* UploadTests.swift */, DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */, E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */, + E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */, ); path = ApolloServerIntegrationTests; sourceTree = ""; @@ -1773,6 +1816,18 @@ path = DefaultImplementation; sourceTree = ""; }; + E6A901D527BDAFA100931C9E /* SubscriptionAPI */ = { + isa = PBXGroup; + children = ( + E6CE3DB927BDB26E00B43E0A /* graphql */, + E6A19C6527BF0E1C0099C6E3 /* API.swift */, + E63C03DB27BDD99100D675C6 /* Info.plist */, + E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */, + ); + name = SubscriptionAPI; + path = Sources/SubscriptionAPI; + sourceTree = SOURCE_ROOT; + }; E6BE04ED26F11B3500CF858D /* Resources */ = { isa = PBXGroup; children = ( @@ -1781,6 +1836,16 @@ path = Resources; sourceTree = ""; }; + E6CE3DB927BDB26E00B43E0A /* graphql */ = { + isa = PBXGroup; + children = ( + E63C03D627BDBA8900D675C6 /* operation_ids.json */, + E6CE3DBA27BDB26E00B43E0A /* schema.graphqls */, + E63C03D327BDB55900D675C6 /* subscription.graphql */, + ); + path = graphql; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ @@ -1867,6 +1932,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901CF27BDAFA100931C9E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E6A901D727BDAFA100931C9E /* SubscriptionAPI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -2149,6 +2222,7 @@ buildRules = ( ); dependencies = ( + E63C03E127BDDFEF00D675C6 /* PBXTargetDependency */, DECD498E262F840100924527 /* PBXTargetDependency */, DECD4735262F668200924527 /* PBXTargetDependency */, DECD46FA262F659100924527 /* PBXTargetDependency */, @@ -2160,6 +2234,7 @@ ); name = ApolloServerIntegrationTests; packageProductDependencies = ( + E6A19C6127BEDAE00099C6E3 /* Nimble */, ); productName = ApolloServerIntegrationTests; productReference = DE6B15AC26152BE10068D642 /* ApolloServerIntegrationTests.xctest */; @@ -2184,6 +2259,25 @@ productReference = DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */; productType = "com.apple.product-type.framework"; }; + E6A901D327BDAFA100931C9E /* SubscriptionAPI */ = { + isa = PBXNativeTarget; + buildConfigurationList = E6A901D827BDAFA100931C9E /* Build configuration list for PBXNativeTarget "SubscriptionAPI" */; + buildPhases = ( + E6A901CF27BDAFA100931C9E /* Headers */, + E6A901D027BDAFA100931C9E /* Sources */, + E6A901D127BDAFA100931C9E /* Frameworks */, + E6A901D227BDAFA100931C9E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + E6A901DF27BDB01200931C9E /* PBXTargetDependency */, + ); + name = SubscriptionAPI; + productName = SubscriptionAPI; + productReference = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; + productType = "com.apple.product-type.framework"; + }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -2248,6 +2342,10 @@ DECD490A262F81BF00924527 = { CreatedOnToolsVersion = 12.4; }; + E6A901D327BDAFA100931C9E = { + CreatedOnToolsVersion = 13.2.1; + LastSwiftMigration = 1320; + }; }; }; buildConfigurationList = 9FC7503E1D2A532C00458D91 /* Build configuration list for PBXProject "Apollo" */; @@ -2261,6 +2359,7 @@ mainGroup = 9FC7503A1D2A532C00458D91; packageReferences = ( 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */, + E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */, ); productRefGroup = 9FC750451D2A532C00458D91 /* Products */; projectDirPath = ""; @@ -2278,6 +2377,7 @@ 9FCE2CF91E6C213D00E34457 /* StarWarsAPI */, 9FACA9B71F42E67200AE2DBD /* GitHubAPI */, 9B2DFBB524E1FA0D00ED3AE6 /* UploadAPI */, + E6A901D327BDAFA100931C9E /* SubscriptionAPI */, 9B7B6F46233C26D100F32205 /* ApolloCodegenLib */, 9BAEEBFB234BB8FD00808306 /* ApolloCodegenTests */, DECD490A262F81BF00924527 /* ApolloCodegenTestSupport */, @@ -2392,6 +2492,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901D227BDAFA100931C9E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2738,6 +2845,7 @@ DED45D852616759C0086EF63 /* TestConfigs.swift in Sources */, DED45D9626167F020086EF63 /* StarWarsServerCachingRoundtripTests.swift in Sources */, DECD46D0262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift in Sources */, + E63C03DF27BDDC3D00D675C6 /* SubscriptionTests.swift in Sources */, DE6B15AF26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift in Sources */, DED46000261CE9080086EF63 /* HTTPBinAPI.swift in Sources */, DED45F4A261CDBFC0086EF63 /* UploadTests.swift in Sources */, @@ -2757,6 +2865,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901D027BDAFA100931C9E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + E6A19C6727BF0E1C0099C6E3 /* API.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ @@ -2923,6 +3039,16 @@ target = 9FCE2CF91E6C213D00E34457 /* StarWarsAPI */; targetProxy = DED4606A261CEDD10086EF63 /* PBXContainerItemProxy */; }; + E63C03E127BDDFEF00D675C6 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = E6A901D327BDAFA100931C9E /* SubscriptionAPI */; + targetProxy = E63C03E027BDDFEF00D675C6 /* PBXContainerItemProxy */; + }; + E6A901DF27BDB01200931C9E /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 9FC750431D2A532C00458D91 /* Apollo */; + targetProxy = E6A901DE27BDB01200931C9E /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin XCBuildConfiguration section */ @@ -3262,6 +3388,27 @@ }; name = PerformanceTesting; }; + E6A901D927BDAFA100931C9E /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */; + buildSettings = { + }; + name = Debug; + }; + E6A901DA27BDAFA100931C9E /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */; + buildSettings = { + }; + name = Release; + }; + E6A901DB27BDAFA100931C9E /* PerformanceTesting */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */; + buildSettings = { + }; + name = PerformanceTesting; + }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -3425,6 +3572,16 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; + E6A901D827BDAFA100931C9E /* Build configuration list for PBXNativeTarget "SubscriptionAPI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + E6A901D927BDAFA100931C9E /* Debug */, + E6A901DA27BDAFA100931C9E /* Release */, + E6A901DB27BDAFA100931C9E /* PerformanceTesting */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ @@ -3436,6 +3593,14 @@ minimumVersion = 0.13.1; }; }; + E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Quick/Nimble"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 9.2.1; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -3454,6 +3619,11 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; + E6A19C6127BEDAE00099C6E3 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; /* End XCSwiftPackageProductDependency section */ }; rootObject = 9FC7503B1D2A532C00458D91 /* Project object */; diff --git a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index ebd96a8eac..eddb11890a 100644 --- a/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Apollo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,6 +1,33 @@ { "object": { "pins": [ + { + "package": "CwlCatchException", + "repositoryURL": "https://github.com/mattgallagher/CwlCatchException.git", + "state": { + "branch": null, + "revision": "35f9e770f54ce62dd8526470f14c6e137cef3eea", + "version": "2.1.1" + } + }, + { + "package": "CwlPreconditionTesting", + "repositoryURL": "https://github.com/mattgallagher/CwlPreconditionTesting.git", + "state": { + "branch": null, + "revision": "c21f7bab5ca8eee0a9998bbd17ca1d0eb45d4688", + "version": "2.1.0" + } + }, + { + "package": "Nimble", + "repositoryURL": "https://github.com/Quick/Nimble", + "state": { + "branch": null, + "revision": "c93f16c25af5770f0d3e6af27c9634640946b068", + "version": "9.2.1" + } + }, { "package": "SQLite.swift", "repositoryURL": "https://github.com/stephencelis/SQLite.swift.git", diff --git a/Configuration/Apollo/Apollo-Target-SubscriptionAPI.xcconfig b/Configuration/Apollo/Apollo-Target-SubscriptionAPI.xcconfig new file mode 100644 index 0000000000..8da079e221 --- /dev/null +++ b/Configuration/Apollo/Apollo-Target-SubscriptionAPI.xcconfig @@ -0,0 +1,3 @@ +#include "../Shared/Workspace-Universal-Framework.xcconfig" + +INFOPLIST_FILE = Sources/SubscriptionAPI/Info.plist diff --git a/Sources/SubscriptionAPI/API.swift b/Sources/SubscriptionAPI/API.swift new file mode 100644 index 0000000000..f1d28c984d --- /dev/null +++ b/Sources/SubscriptionAPI/API.swift @@ -0,0 +1,51 @@ +// @generated +// This file was automatically generated and should not be edited. + +import Apollo +import Foundation + +public final class IncrementingSubscription: GraphQLSubscription { + /// The raw GraphQL definition of this operation. + public let operationDefinition: String = + """ + subscription Incrementing { + numberIncremented + } + """ + + public let operationName: String = "Incrementing" + + public let operationIdentifier: String? = "fe12b5f0dfc7fefa513cc8aecef043b45daf2d776fd000d3a7703f9798ecf233" + + public init() { + } + + public struct Data: GraphQLSelectionSet { + public static let possibleTypes: [String] = ["Subscription"] + + public static var selections: [GraphQLSelection] { + return [ + GraphQLField("numberIncremented", type: .scalar(Int.self)), + ] + } + + public private(set) var resultMap: ResultMap + + public init(unsafeResultMap: ResultMap) { + self.resultMap = unsafeResultMap + } + + public init(numberIncremented: Int? = nil) { + self.init(unsafeResultMap: ["__typename": "Subscription", "numberIncremented": numberIncremented]) + } + + public var numberIncremented: Int? { + get { + return resultMap["numberIncremented"] as? Int + } + set { + resultMap.updateValue(newValue, forKey: "numberIncremented") + } + } + } +} diff --git a/Sources/SubscriptionAPI/Info.plist b/Sources/SubscriptionAPI/Info.plist new file mode 100644 index 0000000000..09738dfd75 --- /dev/null +++ b/Sources/SubscriptionAPI/Info.plist @@ -0,0 +1,24 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + FMWK + CFBundleShortVersionString + $(CURRENT_PROJECT_VERSION) + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + NSPrincipalClass + + + diff --git a/Sources/SubscriptionAPI/SubscriptionAPI.h b/Sources/SubscriptionAPI/SubscriptionAPI.h new file mode 100644 index 0000000000..beb356750a --- /dev/null +++ b/Sources/SubscriptionAPI/SubscriptionAPI.h @@ -0,0 +1,11 @@ +#import + +//! Project version number for SubscriptionAPI. +FOUNDATION_EXPORT double SubscriptionAPIVersionNumber; + +//! Project version string for SubscriptionAPI. +FOUNDATION_EXPORT const unsigned char SubscriptionAPIVersionString[]; + +// In this header, you should import all the public headers of your framework using statements like #import + + diff --git a/Sources/SubscriptionAPI/graphql/operation_ids.json b/Sources/SubscriptionAPI/graphql/operation_ids.json new file mode 100644 index 0000000000..9ab21dcf46 --- /dev/null +++ b/Sources/SubscriptionAPI/graphql/operation_ids.json @@ -0,0 +1,6 @@ +{ + "fe12b5f0dfc7fefa513cc8aecef043b45daf2d776fd000d3a7703f9798ecf233": { + "name": "Incrementing", + "source": "subscription Incrementing {\n numberIncremented\n}" + } +} diff --git a/Sources/SubscriptionAPI/graphql/schema.graphqls b/Sources/SubscriptionAPI/graphql/schema.graphqls new file mode 100644 index 0000000000..b8a42c27ac --- /dev/null +++ b/Sources/SubscriptionAPI/graphql/schema.graphqls @@ -0,0 +1,7 @@ +type Query { + currentNumber: Int +} + +type Subscription { + numberIncremented: Int +} diff --git a/Sources/SubscriptionAPI/graphql/subscription.graphql b/Sources/SubscriptionAPI/graphql/subscription.graphql new file mode 100644 index 0000000000..55b482bc83 --- /dev/null +++ b/Sources/SubscriptionAPI/graphql/subscription.graphql @@ -0,0 +1,4 @@ +subscription Incrementing { + numberIncremented +} + diff --git a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift new file mode 100644 index 0000000000..1e800c38fe --- /dev/null +++ b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift @@ -0,0 +1,58 @@ +import XCTest +import Apollo +import SubscriptionAPI +import ApolloWebSocket +import SQLite +import Nimble + +class SubscriptionTests: XCTestCase { + enum Connection: Equatable { + case disconnected + case connected + } + + var connectionState: Connection = .disconnected + var resultNumber: Int? = nil + + func test_subscribe_givenSubscription_shouldReceiveSuccessResult_andCancelSubscription() { + // given + let store = ApolloStore() + let webSocketTransport = WebSocketTransport( + websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphql_transport_ws), + store: store + ) + webSocketTransport.delegate = self + let client = ApolloClient(networkTransport: webSocketTransport, store: store) + + expect(self.connectionState).toEventually(equal(Connection.connected), timeout: .seconds(1)) + + // when + let subject = client.subscribe(subscription: IncrementingSubscription()) { result in + switch result { + case let .failure(error): + XCTFail("Expected .success, got \(error.localizedDescription)") + + case let .success(graphqlResult): + expect(graphqlResult.errors).to(beNil()) + self.resultNumber = graphqlResult.data?.numberIncremented + } + } + + // then + expect(self.resultNumber).toEventuallyNot(beNil(), timeout: .seconds(2)) + + subject.cancel() + webSocketTransport.closeConnection() + expect(self.connectionState).toEventually(equal(.disconnected), timeout: .seconds(2)) + } +} + +extension SubscriptionTests: WebSocketTransportDelegate { + func webSocketTransportDidConnect(_ webSocketTransport: WebSocketTransport) { + connectionState = .connected + } + + func webSocketTransport(_ webSocketTransport: WebSocketTransport, didDisconnectWithError error:Error?) { + connectionState = .disconnected + } +} diff --git a/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift b/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift index 4b76b04eff..900aa1e7e0 100644 --- a/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift +++ b/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift @@ -6,6 +6,8 @@ public enum TestServerURL: String { case starWarsServer = "http://localhost:8080/graphql" case starWarsWebSocket = "ws://localhost:8080/websocket" case uploadServer = "http://localhost:4000" + case subscriptionServer = "http://localhost:4000/graphql" + case subscriptionWebSocket = "ws://localhost:4000/graphql" public var url: URL { return URL(string: self.rawValue)! From 685d359497733d88f9faacc9441dd2be7ac53f31 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 18 Feb 2022 13:21:11 -0800 Subject: [PATCH 03/21] Add CI step for Apollo Server graphql-transport-ws tests - Clone and install Apollo Server docs-examples - Move SimpleUploadServer to port 4001 and lock nvm to v12.22.10 - Stop using node v12 as default (CI installed is v16) --- .circleci/config.yml | 15 ++++++++++++++- SimpleUploadServer/.nvmrc | 1 + SimpleUploadServer/index.js | 4 +++- .../TestHelpers/TestServerURLs.swift | 2 +- .../install-apollo-server-docs-example-server.sh | 9 +++++++++ scripts/{install-node.sh => install-node-v12.sh} | 4 +--- 6 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 SimpleUploadServer/.nvmrc create mode 100755 scripts/install-apollo-server-docs-example-server.sh rename scripts/{install-node.sh => install-node-v12.sh} (68%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1841d762aa..08019e88f4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -31,9 +31,11 @@ commands: steps: - restore_cache: key: starwars-server + - restore_cache: + key: apollo-server-graphql-transport-ws - common_test_setup - run: - command: ./scripts/install-node.sh + command: ./scripts/install-node-v12.sh name: Install Node - run: command: ./scripts/install-or-update-starwars-server.sh @@ -49,12 +51,23 @@ commands: - run: command: sudo chmod -R +rwx SimpleUploadServer name: Adjust permissions for simple upload server folder + - run: + command: ./scripts/install-apollo-server-docs-example-server.sh + name: Install Apollo Server (graphql-transport-ws configuration) + - run: + command: cd ../docs-examples/apollo-server/v3/subscriptions-graphql-ws && npm start + name: Start Apollo Server (graphql-transport-ws configuration) + background: true integration_test_cleanup: steps: - save_cache: key: starwars-server paths: - ../starwars-server + - save_cache: + key: apollo-server-graphql-transport-ws + paths: + - ../docs-examples/apollo-server/v3/subscriptions-graphql-ws common_test_setup: description: Commands to run for setup of every set of tests steps: diff --git a/SimpleUploadServer/.nvmrc b/SimpleUploadServer/.nvmrc new file mode 100644 index 0000000000..7814f7d060 --- /dev/null +++ b/SimpleUploadServer/.nvmrc @@ -0,0 +1 @@ +v12.22.10 \ No newline at end of file diff --git a/SimpleUploadServer/index.js b/SimpleUploadServer/index.js index 5f6fafce67..fed3f0b44c 100644 --- a/SimpleUploadServer/index.js +++ b/SimpleUploadServer/index.js @@ -64,6 +64,8 @@ const server = new ApolloServer({ } }); -server.listen().then(({ url }) => { +server.listen({ + port: 4001 +}).then(({ url }) => { console.info(`Upload server started at ${url}`); }); diff --git a/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift b/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift index 900aa1e7e0..95c504bd1c 100644 --- a/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift +++ b/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift @@ -5,7 +5,7 @@ public enum TestServerURL: String { case mockServer = "http://localhost/dummy_url" case starWarsServer = "http://localhost:8080/graphql" case starWarsWebSocket = "ws://localhost:8080/websocket" - case uploadServer = "http://localhost:4000" + case uploadServer = "http://localhost:4001" case subscriptionServer = "http://localhost:4000/graphql" case subscriptionWebSocket = "ws://localhost:4000/graphql" diff --git a/scripts/install-apollo-server-docs-example-server.sh b/scripts/install-apollo-server-docs-example-server.sh new file mode 100755 index 0000000000..db81cfc94c --- /dev/null +++ b/scripts/install-apollo-server-docs-example-server.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +cd $(dirname "$0")/../.. + +git clone https://github.com/apollographql/docs-examples.git + +cd docs-examples/apollo-server/v3/subscriptions-graphql-ws + +npm install diff --git a/scripts/install-node.sh b/scripts/install-node-v12.sh similarity index 68% rename from scripts/install-node.sh rename to scripts/install-node-v12.sh index 713ec98a9f..5549677e18 100755 --- a/scripts/install-node.sh +++ b/scripts/install-node-v12.sh @@ -4,6 +4,4 @@ touch $BASH_ENV curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV -echo nvm install 12 >> $BASH_ENV -echo nvm alias default 12 >> $BASH_ENV -echo nvm use default >> $BASH_ENV \ No newline at end of file +echo nvm install v12.22.10 >> $BASH_ENV From c36b77903ade2b00ae67cf9f3b2552020e5363d4 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 18 Feb 2022 13:27:55 -0800 Subject: [PATCH 04/21] After installing node v12 switch to use v16 --- scripts/install-node-v12.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/install-node-v12.sh b/scripts/install-node-v12.sh index 5549677e18..b301cb17c2 100755 --- a/scripts/install-node-v12.sh +++ b/scripts/install-node-v12.sh @@ -5,3 +5,4 @@ curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.35.3/install.sh | bash echo 'export NVM_DIR="$HOME/.nvm"' >> $BASH_ENV echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV echo nvm install v12.22.10 >> $BASH_ENV +echo nvm use v16.13.1 >> $BASH_ENV \ No newline at end of file From b1fc644323a5ca7a7dc4057560e4c4b29d61ef8b Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 18 Feb 2022 13:41:29 -0800 Subject: [PATCH 05/21] Instruct nvm to use version in .nvmrc --- .circleci/config.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 08019e88f4..984e441bc0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,7 +45,7 @@ commands: name: Start StarWars Server background: true - run: - command: cd SimpleUploadServer && npm install && npm start + command: cd SimpleUploadServer && nvm use && npm install && npm start name: Start Upload Server background: true - run: From 6768fdeabcfb78fd1d3b9393c4a208923f0f9f3e Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 18 Feb 2022 15:30:14 -0800 Subject: [PATCH 06/21] Update documentation and tutorial --- .../DefaultImplementation/WebSocket.swift | 3 +++ docs/source/subscriptions.md | 20 +++++++++++++++---- .../source/tutorial/tutorial-subscriptions.md | 11 +++++++--- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index e4334173b5..522d2a3780 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -68,8 +68,11 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock public let code: Int } + /// The WebSocket sub-protocols supported. public enum WSProtocol: CustomStringConvertible { + /// Protocol implemented by the graphql-ws library. case graphql_ws + /// Protocol implemented by the subscriptions-transport-ws libary - considered legacy. case graphql_transport_ws public var description: String { diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index fe21f39ce3..353426c56e 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -19,6 +19,20 @@ There are two different classes which conform to the [`NetworkTransport` protoco Typically, you'll want to use `SplitNetworkTransport`, since this allows you to retain the single `NetworkTransport` setup and avoids any potential issues of using multiple client objects. +## GraphQL over WebSocket Protocols + +There are two WebSocket sub-protocols that apollo-ios supports: +1. [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) which is implemented in the [graphql-ws](https://github.com/enisdenjo/graphql-ws) package. This is a modern, actively maintained package and is our recommendation to use in your server. +2. [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) which is implemented in the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) package. This is considered a legacy implementation and is not recommended for new server builds. + +It is important to note that the two libraries and protocols are not cross-compatible. You will need to know which sub-protocol is implemented in the server you're connecting to. + +There are a number of ways to specify which sub-protocol apollo-ios should use: +1. Manually specify the protocol in the `Sec-WebSocket-Protocol` header of a `URLRequest`. This must be done before initializing a WebSocket using `WebSocket(request:)` as apollo-ios will look for the header key and if found it will use that protocol otherwise it will default to using the `graphql-transport-ws`. See `WebSocket.WSProtocol` for the header values. +2. Use one of the convenience initializers `WebSocket(url:webSocketProtocol:)` or `WebSocket(url:writeQueueQOS:webSocketProtocol:)` where you can specify the protocol on initialization. + +Using `graphql-transport-ws` as the default may seem odd since it's considered a legacy protocol but this was done to not force a breaking change to pre-existing apps using apollo-ios. + ## Sample subscription-supporting initializer Here is an example of setting up a singleton similar to the [Example Advanced Client Setup](initialization/#advanced-client-creation), but which uses a `SplitNetworkTransport` to support both subscriptions and queries: @@ -36,8 +50,7 @@ class Apollo { /// A web socket transport to use for subscriptions private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let request = URLRequest(url: url) - let webSocketClient = WebSocket(request: request) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_ws) return WebSocketTransport(websocket: webSocketClient) }() @@ -161,8 +174,7 @@ class Apollo { // initializes the connection as an authorized channel. private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let request = URLRequest(url: url) - let webSocketClient = WebSocket(request: request) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_ws) let authPayload = ["authToken": magicToken] return WebSocketTransport(websocket: webSocketClient, connectingPayload: authPayload) }() diff --git a/docs/source/tutorial/tutorial-subscriptions.md b/docs/source/tutorial/tutorial-subscriptions.md index 6d5eb70fa9..e3e55aa039 100644 --- a/docs/source/tutorial/tutorial-subscriptions.md +++ b/docs/source/tutorial/tutorial-subscriptions.md @@ -73,14 +73,19 @@ Next, in the lazy declaration of the `apollo` variable, immediately after `trans ```swift:title=Network.swift // 1 -let webSocket = WebSocket(url: URL(string: "wss://apollo-fullstack-tutorial.herokuapp.com/graphql")!) +let webSocket = WebSocket( + url: URL(string: "wss://apollo-fullstack-tutorial.herokuapp.com/graphql")!, + webSocketProtocol: .graphql_ws +) // 2 let webSocketTransport = WebSocketTransport(websocket: webSocket) // 3 -let splitTransport = SplitNetworkTransport(uploadingNetworkTransport: transport, - webSocketNetworkTransport: webSocketTransport) +let splitTransport = SplitNetworkTransport( + uploadingNetworkTransport: transport, + webSocketNetworkTransport: webSocketTransport +) // 4 return ApolloClient(networkTransport: splitTransport, store: store) From e8d1123e6f14cc03728845774dfaf25377f59d6b Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 21 Feb 2022 12:51:10 -0800 Subject: [PATCH 07/21] Change WSProtocol cases to closer match library names --- .../DefaultImplementation/WebSocket.swift | 19 ++++++++++--------- .../ApolloWebSocket/WebSocketTransport.swift | 2 +- .../SubscriptionTests.swift | 2 +- 3 files changed, 12 insertions(+), 11 deletions(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index 522d2a3780..d3a163c762 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -70,15 +70,16 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock /// The WebSocket sub-protocols supported. public enum WSProtocol: CustomStringConvertible { - /// Protocol implemented by the graphql-ws library. - case graphql_ws - /// Protocol implemented by the subscriptions-transport-ws libary - considered legacy. - case graphql_transport_ws + /// Protocol implemented in the https://github.com/apollographql/subscriptions-transport-ws + /// library. That library is not actively maintained and considered legacy. + case subscriptionWsProtocol + /// Protocol implemented by the https://github.com/enisdenjo/graphql-ws library. + case graphqlWsProtocol public var description: String { switch self { - case .graphql_ws: return "graphql-ws" - case .graphql_transport_ws: return "graphql-transport-ws" + case .subscriptionWsProtocol: return "graphql-ws" + case .graphqlWsProtocol: return "graphql-transport-ws" } } } @@ -212,13 +213,13 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock } if self.request.value(forHTTPHeaderField: Constants.headerWSProtocolName) == nil { - self.request.setValue(WSProtocol.graphql_ws.description, + self.request.setValue(WSProtocol.subscriptionWsProtocol.description, forHTTPHeaderField: Constants.headerWSProtocolName) } writeQueue.maxConcurrentOperationCount = 1 } - public convenience init(url: URL, webSocketProtocol: WSProtocol = .graphql_ws) { + public convenience init(url: URL, webSocketProtocol: WSProtocol = .subscriptionWsProtocol) { var request = URLRequest(url: url) request.timeoutInterval = 5 request.setValue(webSocketProtocol.description, @@ -231,7 +232,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock public convenience init( url: URL, writeQueueQOS: QualityOfService, - webSocketProtocol: WSProtocol = .graphql_ws + webSocketProtocol: WSProtocol = .subscriptionWsProtocol ) { self.init(url: url, webSocketProtocol: webSocketProtocol) writeQueue.qualityOfService = writeQueueQOS diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 8951a3a644..4e5abca39a 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -274,7 +274,7 @@ public class WebSocketTransport { let identifier = operationMessageIdCreator.requestId() var type: OperationMessage.Types = .start - if case WebSocket.WSProtocol.graphql_transport_ws.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) { + if case WebSocket.WSProtocol.graphqlWsProtocol.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) { type = .subscribe } diff --git a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift index 1e800c38fe..aab6673c7e 100644 --- a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift @@ -18,7 +18,7 @@ class SubscriptionTests: XCTestCase { // given let store = ApolloStore() let webSocketTransport = WebSocketTransport( - websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphql_transport_ws), + websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphqlWsProtocol), store: store ) webSocketTransport.delegate = self From 7f6c4b6f4c7cc301b260695467d9bce349887b75 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 21 Feb 2022 14:20:08 -0800 Subject: [PATCH 08/21] Remove initializer defaults and require web socket protocol on designated initializer. --- .../DefaultImplementation/WebSocket.swift | 36 ++++++++++++------- .../StarWarsSubscriptionTests.swift | 3 +- .../StarWarsWebSocketTests.swift | 5 ++- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index d3a163c762..278462bdb6 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -198,8 +198,12 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock return canWork } - /// Used for setting protocols. - public init(request: URLRequest) { + /// Designated initializer. + /// + /// - Parameters: + /// - request: A URL request object that provides request-specific information such as the URL. + /// - webSocketProtocol: Protocol to use for communication over the web socket. + public init(request: URLRequest, webSocketProtocol: WSProtocol) { self.request = request self.stream = FoundationStream() if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil { @@ -212,27 +216,35 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } - if self.request.value(forHTTPHeaderField: Constants.headerWSProtocolName) == nil { - self.request.setValue(WSProtocol.subscriptionWsProtocol.description, - forHTTPHeaderField: Constants.headerWSProtocolName) - } + self.request.setValue(webSocketProtocol.description, + forHTTPHeaderField: Constants.headerWSProtocolName) + writeQueue.maxConcurrentOperationCount = 1 } - public convenience init(url: URL, webSocketProtocol: WSProtocol = .subscriptionWsProtocol) { + /// Convenience initializer to specify the URL and web socket protocol. + /// + /// - Parameters: + /// - url: The destination URL to connect to. + /// - webSocketProtocol: Protocol to use for communication over the web socket. + public convenience init(url: URL, webSocketProtocol: WSProtocol) { var request = URLRequest(url: url) request.timeoutInterval = 5 - request.setValue(webSocketProtocol.description, - forHTTPHeaderField: Constants.headerWSProtocolName) - self.init(request: request) + self.init(request: request, webSocketProtocol: webSocketProtocol) } - // Used for specifically setting the QOS for the write queue. + /// Convenience initializer to specify the URL and web socket protocol with a specific quality of + /// service on the write queue. + /// + /// - Parameters: + /// - url: The destination URL to connect to. + /// - writeQueueQOS: Specifies the quality of service for the write queue. + /// - webSocketProtocol: Protocol to use for communication over the web socket. public convenience init( url: URL, writeQueueQOS: QualityOfService, - webSocketProtocol: WSProtocol = .subscriptionWsProtocol + webSocketProtocol: WSProtocol ) { self.init(url: url, webSocketProtocol: webSocketProtocol) writeQueue.qualityOfService = writeQueueQOS diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 8da86d0f78..7579b5e028 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -22,7 +22,8 @@ class StarWarsSubscriptionTests: XCTestCase { webSocketTransport = WebSocketTransport( websocket: WebSocket( - request: URLRequest(url: TestServerURL.starWarsWebSocket.url) + request: URLRequest(url: TestServerURL.starWarsWebSocket.url), + webSocketProtocol: .subscriptionWsProtocol ), store: ApolloStore() ) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index c72de86616..60012d4b7e 100755 --- a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift @@ -22,9 +22,8 @@ class StarWarsWebSocketTests: XCTestCase, CacheDependentTesting { let store = ApolloStore(cache: cache) let networkTransport = WebSocketTransport( - websocket: WebSocket( - request: URLRequest(url: TestServerURL.starWarsWebSocket.url) - ), + websocket: WebSocket(request: URLRequest(url: TestServerURL.starWarsWebSocket.url), + webSocketProtocol: .subscriptionWsProtocol), store: store ) From ad9fbb5f8486a9a618d1022700571cc03e7fc655 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 21 Feb 2022 14:31:37 -0800 Subject: [PATCH 09/21] Update Subscriptions documentation --- docs/source/subscriptions.md | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index 353426c56e..190da41261 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -21,17 +21,11 @@ Typically, you'll want to use `SplitNetworkTransport`, since this allows you to ## GraphQL over WebSocket Protocols -There are two WebSocket sub-protocols that apollo-ios supports: -1. [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) which is implemented in the [graphql-ws](https://github.com/enisdenjo/graphql-ws) package. This is a modern, actively maintained package and is our recommendation to use in your server. -2. [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) which is implemented in the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) package. This is considered a legacy implementation and is not recommended for new server builds. +There are two GraphQL over WebSocket protocols that apollo-ios supports: +1. [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) which is implemented in the [graphql-ws](https://github.com/enisdenjo/graphql-ws) package. This is a modern, actively maintained library and is our recommendation to use in your server. +2. [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) which is implemented in the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) package. This library is not actively maintained and is considered legacy. It is not recommended for new server builds. -It is important to note that the two libraries and protocols are not cross-compatible. You will need to know which sub-protocol is implemented in the server you're connecting to. - -There are a number of ways to specify which sub-protocol apollo-ios should use: -1. Manually specify the protocol in the `Sec-WebSocket-Protocol` header of a `URLRequest`. This must be done before initializing a WebSocket using `WebSocket(request:)` as apollo-ios will look for the header key and if found it will use that protocol otherwise it will default to using the `graphql-transport-ws`. See `WebSocket.WSProtocol` for the header values. -2. Use one of the convenience initializers `WebSocket(url:webSocketProtocol:)` or `WebSocket(url:writeQueueQOS:webSocketProtocol:)` where you can specify the protocol on initialization. - -Using `graphql-transport-ws` as the default may seem odd since it's considered a legacy protocol but this was done to not force a breaking change to pre-existing apps using apollo-ios. +It is important to note that the two libraries and protocols are not cross-compatible. You will need to know which is implemented in the server you're connecting to. All `WebSocket` initializers allow you to specify the GraphQL over WebSocket protocol to be used. ## Sample subscription-supporting initializer From 37336c5ae5edf961520b97e9fd5e2b0fe2fe3ff3 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 21 Feb 2022 23:05:10 -0800 Subject: [PATCH 10/21] Add WSProtocol option for AWS AppSync --- Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index 278462bdb6..b99bc81b4e 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -75,10 +75,12 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock case subscriptionWsProtocol /// Protocol implemented by the https://github.com/enisdenjo/graphql-ws library. case graphqlWsProtocol + /// Protocol implemented by AWS AppSync + case appSyncWsProtocol public var description: String { switch self { - case .subscriptionWsProtocol: return "graphql-ws" + case .subscriptionWsProtocol, .appSyncWsProtocol: return "graphql-ws" case .graphqlWsProtocol: return "graphql-transport-ws" } } From 348dcf0078185ebfd94a2e6b73718379ae5d455d Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Mon, 21 Feb 2022 23:05:57 -0800 Subject: [PATCH 11/21] Add ping/pong message support required by graphql-ws --- Sources/ApolloWebSocket/OperationMessage.swift | 3 +++ Sources/ApolloWebSocket/WebSocketTransport.swift | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index ae11cff405..f8651e547b 100644 --- a/Sources/ApolloWebSocket/OperationMessage.swift +++ b/Sources/ApolloWebSocket/OperationMessage.swift @@ -19,6 +19,9 @@ final class OperationMessage { case error = "error" // Server -> Client case complete = "complete" // Server -> Client case next = "next" // Server -> Client + + case ping = "ping" // Bidirectional + case pong = "pong" // Bidirectional } let serializationFormat = JSONSerializationFormat.self diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 4e5abca39a..26a685e365 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -181,9 +181,16 @@ public class WebSocketTransport { writeQueue() case .connectionKeepAlive, - .startAck: + .startAck, + .pong: writeQueue() + case .ping: + if let str = OperationMessage(type: .pong).rawMessage { + write(str) + writeQueue() + } + case .connectionInit, .connectionTerminate, .subscribe, From 3391f33f1cfd79239e88061157133bcde9c871df Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Tue, 22 Feb 2022 16:44:56 -0800 Subject: [PATCH 12/21] Update documentation and tutorial --- docs/source/subscriptions.md | 15 ++++++++------- docs/source/tutorial/tutorial-subscriptions.md | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index 190da41261..554b5c6d6d 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -19,13 +19,14 @@ There are two different classes which conform to the [`NetworkTransport` protoco Typically, you'll want to use `SplitNetworkTransport`, since this allows you to retain the single `NetworkTransport` setup and avoids any potential issues of using multiple client objects. -## GraphQL over WebSocket Protocols +## GraphQL over WebSocket -There are two GraphQL over WebSocket protocols that apollo-ios supports: -1. [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) which is implemented in the [graphql-ws](https://github.com/enisdenjo/graphql-ws) package. This is a modern, actively maintained library and is our recommendation to use in your server. -2. [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) which is implemented in the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) package. This library is not actively maintained and is considered legacy. It is not recommended for new server builds. +There are three GraphQL over WebSocket implementations supported by apollo-ios: +1. The [graphql-ws](https://github.com/enisdenjo/graphql-ws) library which implements the [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol. This is a modern, actively maintained library and is our recommendation to use in your server. +2. The [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) library which implements the [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) protocol. This library is not actively maintained and is considered legacy. It is not recommended for new server builds. +3. AWS AppSync which implements the [graphql-ws](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#appsynclong-real-time-websocket-client-implementation-guide-for-graphql-subscriptions) protocol with a few additions. -It is important to note that the two libraries and protocols are not cross-compatible. You will need to know which is implemented in the server you're connecting to. All `WebSocket` initializers allow you to specify the GraphQL over WebSocket protocol to be used. +It is important to note that the libraries are not cross-compatible and you will need to know which is implemented in the server you're connecting to. All `WebSocket` initializers allow you to specify which GraphQL over WebSocket protocol should be used. ## Sample subscription-supporting initializer @@ -44,7 +45,7 @@ class Apollo { /// A web socket transport to use for subscriptions private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_ws) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphqlWsProtocol) return WebSocketTransport(websocket: webSocketClient) }() @@ -168,7 +169,7 @@ class Apollo { // initializes the connection as an authorized channel. private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_ws) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphqlWsProtocol) let authPayload = ["authToken": magicToken] return WebSocketTransport(websocket: webSocketClient, connectingPayload: authPayload) }() diff --git a/docs/source/tutorial/tutorial-subscriptions.md b/docs/source/tutorial/tutorial-subscriptions.md index e3e55aa039..66913e2c37 100644 --- a/docs/source/tutorial/tutorial-subscriptions.md +++ b/docs/source/tutorial/tutorial-subscriptions.md @@ -75,7 +75,7 @@ Next, in the lazy declaration of the `apollo` variable, immediately after `trans // 1 let webSocket = WebSocket( url: URL(string: "wss://apollo-fullstack-tutorial.herokuapp.com/graphql")!, - webSocketProtocol: .graphql_ws + webSocketProtocol: .subscriptionWsProtocol ) // 2 From 5b3aafbf04b1d402ae4fbc8d32ed7ba774edf687 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 00:31:22 -0800 Subject: [PATCH 13/21] Add tests for subscriptionWsProtocol --- Apollo.xcodeproj/project.pbxproj | 32 +++ .../MockWebSocketDelegate.swift | 18 ++ .../ApolloWebSocket/OperationMessage.swift | 6 + .../OperationMessageMatchers.swift | 27 ++ .../SubscriptionWsProtocolTests.swift | 255 ++++++++++++++++++ .../WebSocket/WebSocketTransportTests.swift | 16 -- 6 files changed, 338 insertions(+), 16 deletions(-) create mode 100644 Sources/ApolloTestSupport/MockWebSocketDelegate.swift create mode 100644 Tests/ApolloTests/OperationMessageMatchers.swift create mode 100644 Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 1ee46182ea..4b20622119 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -254,12 +254,18 @@ E63C03DF27BDDC3D00D675C6 /* SubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */; }; E63C03E227BDE00400D675C6 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; E657CDBA26FD01D4005834D6 /* ApolloSchemaInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */; }; + E658545B27C5C1EE00339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658545A27C5C1EE00339378 /* Nimble */; }; + E658545C27C5CA1C00339378 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; + E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658545D27C6028100339378 /* MockWebSocketDelegate.swift */; }; + E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; + E658546827C62A8700339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658546727C62A8700339378 /* Nimble */; }; E6630B8C26F0639B002D9E41 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB926EC05290094434A /* MockNetworkSession.swift */; }; E6630B8E26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */; }; E6A19C6227BEDAE00099C6E3 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E6A19C6127BEDAE00099C6E3 /* Nimble */; }; E6A19C6727BF0E1C0099C6E3 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A19C6527BF0E1C0099C6E3 /* API.swift */; }; E6A901D727BDAFA100931C9E /* SubscriptionAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; E6A901DC27BDB01200931C9E /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; + E6B9BDDB27C5693300CF911D /* SubscriptionWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */; }; E6C4267B26F16CB400904AD2 /* introspection_response.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C4267A26F16CB400904AD2 /* introspection_response.json */; }; E6D79AB826E9D59C0094434A /* URLDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */; }; E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; }; @@ -846,11 +852,14 @@ E63C03DB27BDD99100D675C6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTests.swift; sourceTree = ""; }; E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloSchemaInternalTests.swift; sourceTree = ""; }; + E658545D27C6028100339378 /* MockWebSocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocketDelegate.swift; sourceTree = ""; }; + E658546527C6277600339378 /* OperationMessageMatchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageMatchers.swift; sourceTree = ""; }; E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-SubscriptionAPI.xcconfig"; sourceTree = ""; }; E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemaRegistryApolloSchemaDownloaderTests.swift; sourceTree = ""; }; E6A19C6527BF0E1C0099C6E3 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SubscriptionAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SubscriptionAPI.h; sourceTree = ""; }; + E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionWsProtocolTests.swift; sourceTree = ""; }; E6C4267A26F16CB400904AD2 /* introspection_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = introspection_response.json; sourceTree = ""; }; E6CE3DBA27BDB26E00B43E0A /* schema.graphqls */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphqls; sourceTree = ""; }; E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLDownloaderTests.swift; sourceTree = ""; }; @@ -927,6 +936,7 @@ buildActionMask = 2147483647; files = ( 9F65B1211EC106F30090B25F /* Apollo.framework in Frameworks */, + E658546827C62A8700339378 /* Nimble in Frameworks */, DED45EE4261BA1FB0086EF63 /* ApolloSQLite.framework in Frameworks */, DED45EE5261BA1FB0086EF63 /* ApolloWebSocket.framework in Frameworks */, ); @@ -944,6 +954,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + E658545C27C5CA1C00339378 /* SubscriptionAPI.framework in Frameworks */, + E658545B27C5C1EE00339378 /* Nimble in Frameworks */, 9B2DFBCD24E201A800ED3AE6 /* UploadAPI.framework in Frameworks */, 9FC7504F1D2A532D00458D91 /* Apollo.framework in Frameworks */, 9F8A958D1EC0FFAB00304A2D /* ApolloTestSupport.framework in Frameworks */, @@ -1046,6 +1058,7 @@ 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */, C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */, 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, + E658546527C6277600339378 /* OperationMessageMatchers.swift */, ); name = TestHelpers; sourceTree = ""; @@ -1342,6 +1355,7 @@ 9FBE0D3F25407B64002ED0B1 /* AsyncResultObserver.swift */, 9F68F9F025415827004F26D0 /* XCTestCase+Helpers.swift */, 9B2061162591B3550020D1E0 /* Resources */, + E658545D27C6028100339378 /* MockWebSocketDelegate.swift */, ); name = ApolloTestSupport; path = Sources/ApolloTestSupport; @@ -1800,6 +1814,7 @@ D90F1AF92479DEE5007A1534 /* WebSocketTransportTests.swift */, DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */, 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */, + E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */, ); path = WebSocket; sourceTree = ""; @@ -2106,6 +2121,7 @@ ); name = ApolloTestSupport; packageProductDependencies = ( + E658546727C62A8700339378 /* Nimble */, ); productName = ApolloTestSupport; productReference = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; @@ -2169,6 +2185,9 @@ 9FCE2D081E6C254000E34457 /* PBXTargetDependency */, ); name = ApolloTests; + packageProductDependencies = ( + E658545A27C5C1EE00339378 /* Nimble */, + ); productName = ApolloTests; productReference = 9FC7504E1D2A532D00458D91 /* ApolloTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -2674,6 +2693,8 @@ DED4600D261CE9260086EF63 /* TestFileHelper.swift in Sources */, 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */, 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */, + E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */, + E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */, 9F68F9F125415827004F26D0 /* XCTestCase+Helpers.swift in Sources */, 9BCF0CE523FC9CA50031D2A2 /* MockNetworkTransport.swift in Sources */, DE05862D2669800000265760 /* Matchable.swift in Sources */, @@ -2794,6 +2815,7 @@ E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, + E6B9BDDB27C5693300CF911D /* SubscriptionWsProtocolTests.swift in Sources */, DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */, 9F21730E2567E6F000566121 /* DataLoaderTests.swift in Sources */, DED45DEC261B96B70086EF63 /* CacheDependentInterceptorTests.swift in Sources */, @@ -3619,6 +3641,16 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; + E658545A27C5C1EE00339378 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; + E658546727C62A8700339378 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; E6A19C6127BEDAE00099C6E3 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; diff --git a/Sources/ApolloTestSupport/MockWebSocketDelegate.swift b/Sources/ApolloTestSupport/MockWebSocketDelegate.swift new file mode 100644 index 0000000000..3b5701ff65 --- /dev/null +++ b/Sources/ApolloTestSupport/MockWebSocketDelegate.swift @@ -0,0 +1,18 @@ +import Foundation +@testable import ApolloWebSocket + +public class MockWebSocketDelegate: WebSocketClientDelegate { + public var didReceiveMessage: ((String) -> Void)? + + public init() {} + + public func websocketDidConnect(socket: WebSocketClient) {} + + public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {} + + public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + didReceiveMessage?(text) + } + + public func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} +} diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index f8651e547b..96cb4da2de 100644 --- a/Sources/ApolloWebSocket/OperationMessage.swift +++ b/Sources/ApolloWebSocket/OperationMessage.swift @@ -104,6 +104,12 @@ final class OperationMessage { } } +extension OperationMessage: CustomDebugStringConvertible { + var debugDescription: String { + rawMessage! + } +} + struct ParseHandler { let type: String? let id: String? diff --git a/Tests/ApolloTests/OperationMessageMatchers.swift b/Tests/ApolloTests/OperationMessageMatchers.swift new file mode 100644 index 0000000000..43b1898ed7 --- /dev/null +++ b/Tests/ApolloTests/OperationMessageMatchers.swift @@ -0,0 +1,27 @@ +import Foundation +import Nimble +import Apollo +@testable import ApolloWebSocket + +public func equalMessage(payload: GraphQLMap? = nil, id: String? = nil, type: OperationMessage.Types) -> Predicate { + return Predicate.define { actualExpression in + guard let actualValue = try actualExpression.evaluate() else { + return PredicateResult( + status: .fail, + message: .fail("Message cannot be nil - type is a required parameter.") + ) + } + + let expected = OperationMessage(payload: payload, id: id, type: type) + guard actualValue == expected.rawMessage! else { + return PredicateResult( + status: .fail, + message: .expectedActualValueTo("equal \(expected)")) + } + + return PredicateResult( + status: .matches, + message: .expectedTo("be equal") + ) + } +} diff --git a/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift new file mode 100644 index 0000000000..dc1b8ea9b1 --- /dev/null +++ b/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift @@ -0,0 +1,255 @@ +import XCTest +@testable import ApolloWebSocket +import ApolloTestSupport +import Nimble +import Apollo +import SubscriptionAPI + +class SubscriptionWsProtocolTests: XCTestCase { + private var store: ApolloStore! + private var mockWebSocket: MockWebSocket! + private var websocketTransport: WebSocketTransport! { + didSet { + if let websocketTransport = websocketTransport { // caters for tearDown setting nil value + websocketTransport.websocket.delegate = mockWebSocketDelegate + } + } + } + private var mockWebSocketDelegate: MockWebSocketDelegate! + private var client: ApolloClient! + + override func setUp() { + super.setUp() + + store = ApolloStore() + } + + override func tearDown() { + client = nil + websocketTransport = nil + mockWebSocket = nil + mockWebSocketDelegate = nil + store = nil + + super.tearDown() + } + + // MARK: Helpers + + private func buildWebSocket() { + var request = URLRequest(url: TestURL.mockServer.url) + request.setValue("graphql-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + + mockWebSocketDelegate = MockWebSocketDelegate() + mockWebSocket = MockWebSocket(request: request) + websocketTransport = WebSocketTransport(websocket: mockWebSocket, store: store) + } + + private func buildClient() { + client = ApolloClient(networkTransport: websocketTransport, store: store) + } + + private func connectWebSocket() { + websocketTransport.socketConnectionState.mutate { $0 = .connected } + } + + private func ackConnection() { + let ackMessage = OperationMessage(type: .connectionAck).rawMessage! + websocketTransport.websocketDidReceiveMessage(socket: mockWebSocket, text: ackMessage) + } + + // MARK: Initializer Tests + + func test__designatedInitializer__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + request: URLRequest(url: TestURL.mockServer.url), + webSocketProtocol: .subscriptionWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-ws")) + } + + func test__convenienceInitializers__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + url: TestURL.mockServer.url, + webSocketProtocol: .subscriptionWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-ws")) + + expect( + WebSocket( + url: TestURL.mockServer.url, + writeQueueQOS: .default, + webSocketProtocol: .subscriptionWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-ws")) + } + + // MARK: Client -> Server Tests + + func test__messaging__givenDefaultConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + // given + buildWebSocket() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: [:], type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenNilConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + buildWebSocket() + + // given + websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + buildWebSocket() + + // given + websocketTransport = WebSocketTransport( + websocket: mockWebSocket, + connectingPayload: ["sample": "data"] + ) + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenSubscriptionSubscribe_shouldSendStartMessage() { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let operation = IncrementingSubscription() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .start)) + done() + } + + // when + self.client.subscribe(subscription: operation) { _ in } + } + } + + func test__messaging__givenSubscriptionCancel_shouldSendStopMessage() { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + let expected = OperationMessage(id: "1", type: .stop).rawMessage! + if message == expected { + done() + } + } + + // when + subject.cancel() + } + } + + func test__messaging__whenWebSocketClosed_shouldSendConnectionTerminate() throws { + // given + buildWebSocket() + + connectWebSocket() + ackConnection() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .connectionTerminate)) + done() + } + + // when + self.websocketTransport.closeConnection() + } + } + + // MARK: Server -> Client Tests + + func test__messaging__whenReceivesData_shouldParseMessage() throws { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let operation = IncrementingSubscription() + + waitUntil { done in + // when + self.client.subscribe(subscription: operation) { result in + switch result { + case let .failure(error): + fail("Expected .success, got error: \(error.localizedDescription)") + + case let .success(graphqlResult): + expect(graphqlResult.data?.numberIncremented).to(equal(42)) + done() + } + } + + let message = OperationMessage( + payload: ["data": ["numberIncremented": 42]], + id: "1", + type: .data + ).rawMessage! + self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + } + } +} + +private extension GraphQLOperation { + var requestBody: GraphQLMap { + ApolloRequestBodyCreator().requestBody( + for: self, + sendOperationIdentifiers: false, + sendQueryDocument: true, + autoPersistQuery: false + ) + } +} diff --git a/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift b/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift index 86c65bd6bd..e062c45400 100644 --- a/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift +++ b/Tests/ApolloTests/WebSocket/WebSocketTransportTests.swift @@ -74,19 +74,3 @@ class WebSocketTransportTests: XCTestCase { } } } - -private final class MockWebSocketDelegate: WebSocketClientDelegate { - - var didReceiveMessage: ((String) -> Void)? - - func websocketDidConnect(socket: WebSocketClient) {} - - func websocketDidDisconnect(socket: WebSocketClient, error: Error?) {} - - func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { - didReceiveMessage?(text) - } - - func websocketDidReceiveData(socket: WebSocketClient, data: Data) {} - -} From 6edea9bc12f346ae84daea5e3da95e8eac585991 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 00:54:43 -0800 Subject: [PATCH 14/21] Add tests for graphqlWSProtocol --- Apollo.xcodeproj/project.pbxproj | 4 + .../WebSocket/GraphqlWsProtocolTests.swift | 274 ++++++++++++++++++ .../SubscriptionWsProtocolTests.swift | 4 +- 3 files changed, 279 insertions(+), 3 deletions(-) create mode 100644 Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 4b20622119..05f9012f79 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -259,6 +259,7 @@ E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658545D27C6028100339378 /* MockWebSocketDelegate.swift */; }; E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; E658546827C62A8700339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658546727C62A8700339378 /* Nimble */; }; + E658546C27C77B8B00339378 /* GraphqlWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */; }; E6630B8C26F0639B002D9E41 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB926EC05290094434A /* MockNetworkSession.swift */; }; E6630B8E26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */; }; E6A19C6227BEDAE00099C6E3 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E6A19C6127BEDAE00099C6E3 /* Nimble */; }; @@ -854,6 +855,7 @@ E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloSchemaInternalTests.swift; sourceTree = ""; }; E658545D27C6028100339378 /* MockWebSocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocketDelegate.swift; sourceTree = ""; }; E658546527C6277600339378 /* OperationMessageMatchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageMatchers.swift; sourceTree = ""; }; + E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphqlWsProtocolTests.swift; sourceTree = ""; }; E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-SubscriptionAPI.xcconfig"; sourceTree = ""; }; E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemaRegistryApolloSchemaDownloaderTests.swift; sourceTree = ""; }; E6A19C6527BF0E1C0099C6E3 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; @@ -1815,6 +1817,7 @@ DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */, 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */, E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */, + E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */, ); path = WebSocket; sourceTree = ""; @@ -2800,6 +2803,7 @@ 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, DED45DEE261B96B70086EF63 /* FetchQueryTests.swift in Sources */, C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */, + E658546C27C77B8B00339378 /* GraphqlWsProtocolTests.swift in Sources */, DED45DED261B96B70086EF63 /* StoreConcurrencyTests.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, 9FF90A6F1DDDEB420034C3B6 /* GraphQLMapEncodingTests.swift in Sources */, diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift new file mode 100644 index 0000000000..061099ee36 --- /dev/null +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -0,0 +1,274 @@ +import XCTest +@testable import ApolloWebSocket +import ApolloTestSupport +import Nimble +import Apollo +import SubscriptionAPI + +class GraphqlWsProtocolTests: XCTestCase { + private var store: ApolloStore! + private var mockWebSocket: MockWebSocket! + private var websocketTransport: WebSocketTransport! { + didSet { + if let websocketTransport = websocketTransport { // caters for tearDown setting nil value + websocketTransport.websocket.delegate = mockWebSocketDelegate + } + } + } + private var mockWebSocketDelegate: MockWebSocketDelegate! + private var client: ApolloClient! + + override func setUp() { + super.setUp() + + store = ApolloStore() + } + + override func tearDown() { + client = nil + websocketTransport = nil + mockWebSocket = nil + mockWebSocketDelegate = nil + store = nil + + super.tearDown() + } + + // MARK: Helpers + + private func buildWebSocket() { + var request = URLRequest(url: TestURL.mockServer.url) + request.setValue("graphql-transport-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + + mockWebSocketDelegate = MockWebSocketDelegate() + mockWebSocket = MockWebSocket(request: request) + websocketTransport = WebSocketTransport(websocket: mockWebSocket, store: store) + } + + private func buildClient() { + client = ApolloClient(networkTransport: websocketTransport, store: store) + } + + private func connectWebSocket() { + websocketTransport.socketConnectionState.mutate { $0 = .connected } + } + + private func ackConnection() { + let ackMessage = OperationMessage(type: .connectionAck).rawMessage! + websocketTransport.websocketDidReceiveMessage(socket: mockWebSocket, text: ackMessage) + } + + // MARK: Initializer Tests + + func test__designatedInitializer__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + request: URLRequest(url: TestURL.mockServer.url), + webSocketProtocol: .graphqlWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-transport-ws")) + } + + func test__convenienceInitializers__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + url: TestURL.mockServer.url, + webSocketProtocol: .graphqlWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-transport-ws")) + + expect( + WebSocket( + url: TestURL.mockServer.url, + writeQueueQOS: .default, + webSocketProtocol: .graphqlWsProtocol + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal("graphql-transport-ws")) + } + + // MARK: Protocol Tests + + func test__messaging__givenDefaultConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + // given + buildWebSocket() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: [:], type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenNilConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + buildWebSocket() + + // given + websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { + buildWebSocket() + + // given + websocketTransport = WebSocketTransport( + websocket: mockWebSocket, + connectingPayload: ["sample": "data"] + ) + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) + done() + } + + // when + self.websocketTransport.websocketDidConnect(socket: self.mockWebSocket) + } + } + + func test__messaging__givenSubscriptionSubscribe_shouldSendStartMessage() { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let operation = IncrementingSubscription() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .subscribe)) + done() + } + + // when + self.client.subscribe(subscription: operation) { _ in } + } + } + + func test__messaging__givenSubscriptionCancel_shouldSendStopMessage() { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + let expected = OperationMessage(id: "1", type: .stop).rawMessage! + if message == expected { + done() + } + } + + // when + subject.cancel() + } + } + + func test__messaging__whenWebSocketClosed_shouldSendConnectionTerminate() throws { + // given + buildWebSocket() + + connectWebSocket() + ackConnection() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .connectionTerminate)) + done() + } + + // when + self.websocketTransport.closeConnection() + } + } + + func test__messaging__whenReceivesNext_shouldParseMessage() throws { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + let operation = IncrementingSubscription() + + waitUntil { done in + // when + self.client.subscribe(subscription: operation) { result in + switch result { + case let .failure(error): + fail("Expected .success, got error: \(error.localizedDescription)") + + case let .success(graphqlResult): + expect(graphqlResult.data?.numberIncremented).to(equal(42)) + done() + } + } + + let message = OperationMessage( + payload: ["data": ["numberIncremented": 42]], + id: "1", + type: .next + ).rawMessage! + self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + } + } + + func test__messaging__whenReceivesPing_shouldSendPong() throws { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .pong)) + done() + } + + // when + let message = OperationMessage(payload: ["sample": "data"], type: .ping).rawMessage! + self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + } + } +} + +private extension GraphQLOperation { + var requestBody: GraphQLMap { + ApolloRequestBodyCreator().requestBody( + for: self, + sendOperationIdentifiers: false, + sendQueryDocument: true, + autoPersistQuery: false + ) + } +} diff --git a/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift index dc1b8ea9b1..f77a8a2df1 100644 --- a/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift @@ -86,7 +86,7 @@ class SubscriptionWsProtocolTests: XCTestCase { ).to(equal("graphql-ws")) } - // MARK: Client -> Server Tests + // MARK: Protocol Tests func test__messaging__givenDefaultConnectingPayload_whenWebSocketConnected_shouldSendConnectionInit() throws { // given @@ -208,8 +208,6 @@ class SubscriptionWsProtocolTests: XCTestCase { } } - // MARK: Server -> Client Tests - func test__messaging__whenReceivesData_shouldParseMessage() throws { // given buildWebSocket() From 4178c667471d2b958e85a26f9bed71a00b57d3b0 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 12:22:50 -0800 Subject: [PATCH 15/21] Revert to naming aligned with the protocols and not the implementation libraries --- Apollo.xcodeproj/project.pbxproj | 16 +++---- .../DefaultImplementation/WebSocket.swift | 19 ++++---- .../ApolloWebSocket/WebSocketTransport.swift | 2 +- .../StarWarsSubscriptionTests.swift | 2 +- .../StarWarsWebSocketTests.swift | 2 +- .../SubscriptionTests.swift | 2 +- ... => GraphqlTransportWsProtocolTests.swift} | 43 ++++++++++++++----- .../WebSocket/GraphqlWsProtocolTests.swift | 41 +++++------------- docs/source/subscriptions.md | 15 +++---- .../source/tutorial/tutorial-subscriptions.md | 2 +- 10 files changed, 71 insertions(+), 73 deletions(-) rename Tests/ApolloTests/WebSocket/{SubscriptionWsProtocolTests.swift => GraphqlTransportWsProtocolTests.swift} (85%) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 05f9012f79..bda1ad20e0 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -259,14 +259,14 @@ E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658545D27C6028100339378 /* MockWebSocketDelegate.swift */; }; E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; E658546827C62A8700339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658546727C62A8700339378 /* Nimble */; }; - E658546C27C77B8B00339378 /* GraphqlWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */; }; + E658546C27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */; }; E6630B8C26F0639B002D9E41 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB926EC05290094434A /* MockNetworkSession.swift */; }; E6630B8E26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */; }; E6A19C6227BEDAE00099C6E3 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E6A19C6127BEDAE00099C6E3 /* Nimble */; }; E6A19C6727BF0E1C0099C6E3 /* API.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6A19C6527BF0E1C0099C6E3 /* API.swift */; }; E6A901D727BDAFA100931C9E /* SubscriptionAPI.h in Headers */ = {isa = PBXBuildFile; fileRef = E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */; settings = {ATTRIBUTES = (Public, ); }; }; E6A901DC27BDB01200931C9E /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; - E6B9BDDB27C5693300CF911D /* SubscriptionWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */; }; + E6B9BDDB27C5693300CF911D /* GraphqlWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6B9BDDA27C5693300CF911D /* GraphqlWsProtocolTests.swift */; }; E6C4267B26F16CB400904AD2 /* introspection_response.json in Resources */ = {isa = PBXBuildFile; fileRef = E6C4267A26F16CB400904AD2 /* introspection_response.json */; }; E6D79AB826E9D59C0094434A /* URLDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */; }; E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E86D8E03214B32DA0028EFE1 /* JSONTests.swift */; }; @@ -855,13 +855,13 @@ E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloSchemaInternalTests.swift; sourceTree = ""; }; E658545D27C6028100339378 /* MockWebSocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocketDelegate.swift; sourceTree = ""; }; E658546527C6277600339378 /* OperationMessageMatchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageMatchers.swift; sourceTree = ""; }; - E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphqlWsProtocolTests.swift; sourceTree = ""; }; + E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphqlTransportWsProtocolTests.swift; sourceTree = ""; }; E661C2D427BDAC500078BEBD /* Apollo-Target-SubscriptionAPI.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Apollo-Target-SubscriptionAPI.xcconfig"; sourceTree = ""; }; E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SchemaRegistryApolloSchemaDownloaderTests.swift; sourceTree = ""; }; E6A19C6527BF0E1C0099C6E3 /* API.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = API.swift; sourceTree = ""; }; E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SubscriptionAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; E6A901D627BDAFA100931C9E /* SubscriptionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SubscriptionAPI.h; sourceTree = ""; }; - E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionWsProtocolTests.swift; sourceTree = ""; }; + E6B9BDDA27C5693300CF911D /* GraphqlWsProtocolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphqlWsProtocolTests.swift; sourceTree = ""; }; E6C4267A26F16CB400904AD2 /* introspection_response.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = introspection_response.json; sourceTree = ""; }; E6CE3DBA27BDB26E00B43E0A /* schema.graphqls */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = schema.graphqls; sourceTree = ""; }; E6D79AB626E97D0D0094434A /* URLDownloaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLDownloaderTests.swift; sourceTree = ""; }; @@ -1816,8 +1816,8 @@ D90F1AF92479DEE5007A1534 /* WebSocketTransportTests.swift */, DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */, 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */, - E6B9BDDA27C5693300CF911D /* SubscriptionWsProtocolTests.swift */, - E658546B27C77B8B00339378 /* GraphqlWsProtocolTests.swift */, + E6B9BDDA27C5693300CF911D /* GraphqlWsProtocolTests.swift */, + E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */, ); path = WebSocket; sourceTree = ""; @@ -2803,7 +2803,7 @@ 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, DED45DEE261B96B70086EF63 /* FetchQueryTests.swift in Sources */, C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */, - E658546C27C77B8B00339378 /* GraphqlWsProtocolTests.swift in Sources */, + E658546C27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift in Sources */, DED45DED261B96B70086EF63 /* StoreConcurrencyTests.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, 9FF90A6F1DDDEB420034C3B6 /* GraphQLMapEncodingTests.swift in Sources */, @@ -2819,7 +2819,7 @@ E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, - E6B9BDDB27C5693300CF911D /* SubscriptionWsProtocolTests.swift in Sources */, + E6B9BDDB27C5693300CF911D /* GraphqlWsProtocolTests.swift in Sources */, DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */, 9F21730E2567E6F000566121 /* DataLoaderTests.swift in Sources */, DED45DEC261B96B70086EF63 /* CacheDependentInterceptorTests.swift in Sources */, diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index b99bc81b4e..e8416285fe 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -68,20 +68,19 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock public let code: Int } - /// The WebSocket sub-protocols supported. + /// The GraphQL over WebSocket protocols supported by apollo-ios. public enum WSProtocol: CustomStringConvertible { - /// Protocol implemented in the https://github.com/apollographql/subscriptions-transport-ws - /// library. That library is not actively maintained and considered legacy. - case subscriptionWsProtocol - /// Protocol implemented by the https://github.com/enisdenjo/graphql-ws library. - case graphqlWsProtocol - /// Protocol implemented by AWS AppSync - case appSyncWsProtocol + /// WebSocket protocol `graphql-ws`. This is implemented by the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) + /// and AWS AppSync libraries. + case graphql_ws + /// WebSocket protocol `graphql-transport-ws`. This is implemented by the [graphql-ws](https://github.com/enisdenjo/graphql-ws) + /// library. + case graphql_transport_ws public var description: String { switch self { - case .subscriptionWsProtocol, .appSyncWsProtocol: return "graphql-ws" - case .graphqlWsProtocol: return "graphql-transport-ws" + case .graphql_ws: return "graphql-ws" + case .graphql_transport_ws: return "graphql-transport-ws" } } } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 26a685e365..20932d84c3 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -281,7 +281,7 @@ public class WebSocketTransport { let identifier = operationMessageIdCreator.requestId() var type: OperationMessage.Types = .start - if case WebSocket.WSProtocol.graphqlWsProtocol.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) { + if case WebSocket.WSProtocol.graphql_transport_ws.description = websocket.request.value(forHTTPHeaderField: WebSocket.Constants.headerWSProtocolName) { type = .subscribe } diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 7579b5e028..0794e70aa0 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -23,7 +23,7 @@ class StarWarsSubscriptionTests: XCTestCase { webSocketTransport = WebSocketTransport( websocket: WebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url), - webSocketProtocol: .subscriptionWsProtocol + webSocketProtocol: .graphql_ws ), store: ApolloStore() ) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index 60012d4b7e..0519a30609 100755 --- a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift @@ -23,7 +23,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheDependentTesting { let networkTransport = WebSocketTransport( websocket: WebSocket(request: URLRequest(url: TestServerURL.starWarsWebSocket.url), - webSocketProtocol: .subscriptionWsProtocol), + webSocketProtocol: .graphql_ws), store: store ) diff --git a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift index aab6673c7e..1e800c38fe 100644 --- a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift @@ -18,7 +18,7 @@ class SubscriptionTests: XCTestCase { // given let store = ApolloStore() let webSocketTransport = WebSocketTransport( - websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphqlWsProtocol), + websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphql_transport_ws), store: store ) webSocketTransport.delegate = self diff --git a/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift similarity index 85% rename from Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift rename to Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index f77a8a2df1..d360744397 100644 --- a/Tests/ApolloTests/WebSocket/SubscriptionWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -5,7 +5,7 @@ import Nimble import Apollo import SubscriptionAPI -class SubscriptionWsProtocolTests: XCTestCase { +class GraphqlTransportWsProtocolTests: XCTestCase { private var store: ApolloStore! private var mockWebSocket: MockWebSocket! private var websocketTransport: WebSocketTransport! { @@ -38,7 +38,7 @@ class SubscriptionWsProtocolTests: XCTestCase { private func buildWebSocket() { var request = URLRequest(url: TestURL.mockServer.url) - request.setValue("graphql-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + request.setValue("graphql-transport-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") mockWebSocketDelegate = MockWebSocketDelegate() mockWebSocket = MockWebSocket(request: request) @@ -64,26 +64,26 @@ class SubscriptionWsProtocolTests: XCTestCase { expect( WebSocket( request: URLRequest(url: TestURL.mockServer.url), - webSocketProtocol: .subscriptionWsProtocol + webSocketProtocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal("graphql-transport-ws")) } func test__convenienceInitializers__shouldSetRequestProtocolHeader() { expect( WebSocket( url: TestURL.mockServer.url, - webSocketProtocol: .subscriptionWsProtocol + webSocketProtocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal("graphql-transport-ws")) expect( WebSocket( url: TestURL.mockServer.url, writeQueueQOS: .default, - webSocketProtocol: .subscriptionWsProtocol + webSocketProtocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal("graphql-transport-ws")) } // MARK: Protocol Tests @@ -156,7 +156,7 @@ class SubscriptionWsProtocolTests: XCTestCase { waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then - expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .start)) + expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .subscribe)) done() } @@ -208,7 +208,7 @@ class SubscriptionWsProtocolTests: XCTestCase { } } - func test__messaging__whenReceivesData_shouldParseMessage() throws { + func test__messaging__whenReceivesNext_shouldParseMessage() throws { // given buildWebSocket() buildClient() @@ -234,11 +234,32 @@ class SubscriptionWsProtocolTests: XCTestCase { let message = OperationMessage( payload: ["data": ["numberIncremented": 42]], id: "1", - type: .data + type: .next ).rawMessage! self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) } } + + func test__messaging__whenReceivesPing_shouldSendPong() throws { + // given + buildWebSocket() + buildClient() + + connectWebSocket() + ackConnection() + + waitUntil { done in + self.mockWebSocketDelegate.didReceiveMessage = { message in + // then + expect(message).to(equalMessage(type: .pong)) + done() + } + + // when + let message = OperationMessage(payload: ["sample": "data"], type: .ping).rawMessage! + self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + } + } } private extension GraphQLOperation { diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index 061099ee36..05cb4202b7 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -38,7 +38,7 @@ class GraphqlWsProtocolTests: XCTestCase { private func buildWebSocket() { var request = URLRequest(url: TestURL.mockServer.url) - request.setValue("graphql-transport-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + request.setValue("graphql-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") mockWebSocketDelegate = MockWebSocketDelegate() mockWebSocket = MockWebSocket(request: request) @@ -64,26 +64,26 @@ class GraphqlWsProtocolTests: XCTestCase { expect( WebSocket( request: URLRequest(url: TestURL.mockServer.url), - webSocketProtocol: .graphqlWsProtocol + webSocketProtocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal("graphql-ws")) } func test__convenienceInitializers__shouldSetRequestProtocolHeader() { expect( WebSocket( url: TestURL.mockServer.url, - webSocketProtocol: .graphqlWsProtocol + webSocketProtocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal("graphql-ws")) expect( WebSocket( url: TestURL.mockServer.url, writeQueueQOS: .default, - webSocketProtocol: .graphqlWsProtocol + webSocketProtocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal("graphql-ws")) } // MARK: Protocol Tests @@ -156,7 +156,7 @@ class GraphqlWsProtocolTests: XCTestCase { waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then - expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .subscribe)) + expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .start)) done() } @@ -208,7 +208,7 @@ class GraphqlWsProtocolTests: XCTestCase { } } - func test__messaging__whenReceivesNext_shouldParseMessage() throws { + func test__messaging__whenReceivesData_shouldParseMessage() throws { // given buildWebSocket() buildClient() @@ -234,32 +234,11 @@ class GraphqlWsProtocolTests: XCTestCase { let message = OperationMessage( payload: ["data": ["numberIncremented": 42]], id: "1", - type: .next + type: .data ).rawMessage! self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) } } - - func test__messaging__whenReceivesPing_shouldSendPong() throws { - // given - buildWebSocket() - buildClient() - - connectWebSocket() - ackConnection() - - waitUntil { done in - self.mockWebSocketDelegate.didReceiveMessage = { message in - // then - expect(message).to(equalMessage(type: .pong)) - done() - } - - // when - let message = OperationMessage(payload: ["sample": "data"], type: .ping).rawMessage! - self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) - } - } } private extension GraphQLOperation { diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index 554b5c6d6d..f4a767a807 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -19,14 +19,13 @@ There are two different classes which conform to the [`NetworkTransport` protoco Typically, you'll want to use `SplitNetworkTransport`, since this allows you to retain the single `NetworkTransport` setup and avoids any potential issues of using multiple client objects. -## GraphQL over WebSocket +## GraphQL over WebSocket protocols -There are three GraphQL over WebSocket implementations supported by apollo-ios: -1. The [graphql-ws](https://github.com/enisdenjo/graphql-ws) library which implements the [graphql-transport-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol. This is a modern, actively maintained library and is our recommendation to use in your server. -2. The [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) library which implements the [graphql-ws](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) protocol. This library is not actively maintained and is considered legacy. It is not recommended for new server builds. -3. AWS AppSync which implements the [graphql-ws](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#appsynclong-real-time-websocket-client-implementation-guide-for-graphql-subscriptions) protocol with a few additions. +There are two protocols supported by apollo-ios: +1. [`graphql-ws`](https://github.com/apollographql/subscriptions-transport-ws/blob/master/PROTOCOL.md) protocol which is implemented in the [subscriptions-transport-ws](https://github.com/apollographql/subscriptions-transport-ws) and [AWS AppSync](https://docs.aws.amazon.com/appsync/latest/devguide/real-time-websocket-client.html#handshake-details-to-establish-the-websocket-connection) libraries. +2. [`graphql-transport-ws`](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) protocol which is implemented in the [graphql-ws](https://github.com/enisdenjo/graphql-ws) library. -It is important to note that the libraries are not cross-compatible and you will need to know which is implemented in the server you're connecting to. All `WebSocket` initializers allow you to specify which GraphQL over WebSocket protocol should be used. +It is important to note that the protocols are not cross-compatible and you will need to know which is implemented in the service you're connecting to. All `WebSocket` initializers allow you to specify which GraphQL over WebSocket protocol should be used. ## Sample subscription-supporting initializer @@ -45,7 +44,7 @@ class Apollo { /// A web socket transport to use for subscriptions private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphqlWsProtocol) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_transport_ws) return WebSocketTransport(websocket: webSocketClient) }() @@ -169,7 +168,7 @@ class Apollo { // initializes the connection as an authorized channel. private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphqlWsProtocol) + let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_transport_ws) let authPayload = ["authToken": magicToken] return WebSocketTransport(websocket: webSocketClient, connectingPayload: authPayload) }() diff --git a/docs/source/tutorial/tutorial-subscriptions.md b/docs/source/tutorial/tutorial-subscriptions.md index 66913e2c37..e3e55aa039 100644 --- a/docs/source/tutorial/tutorial-subscriptions.md +++ b/docs/source/tutorial/tutorial-subscriptions.md @@ -75,7 +75,7 @@ Next, in the lazy declaration of the `apollo` variable, immediately after `trans // 1 let webSocket = WebSocket( url: URL(string: "wss://apollo-fullstack-tutorial.herokuapp.com/graphql")!, - webSocketProtocol: .subscriptionWsProtocol + webSocketProtocol: .graphql_ws ) // 2 From d63d0789beea27c997173300121644e117aeedb2 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 12:31:56 -0800 Subject: [PATCH 16/21] Use longer async timeout for slower environments like CI --- .../GraphqlTransportWsProtocolTests.swift | 18 ++++++++++-------- .../WebSocket/GraphqlWsProtocolTests.swift | 16 +++++++++------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index d360744397..00501e7be2 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -6,6 +6,8 @@ import Apollo import SubscriptionAPI class GraphqlTransportWsProtocolTests: XCTestCase { + private let asyncTimeout: DispatchTimeInterval = .seconds(3) + private var store: ApolloStore! private var mockWebSocket: MockWebSocket! private var websocketTransport: WebSocketTransport! { @@ -92,7 +94,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { // given buildWebSocket() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: [:], type: .connectionInit)) @@ -110,7 +112,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { // given websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionInit)) @@ -131,7 +133,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectingPayload: ["sample": "data"] ) - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) @@ -153,7 +155,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .subscribe)) @@ -175,7 +177,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then let expected = OperationMessage(id: "1", type: .stop).rawMessage! @@ -196,7 +198,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionTerminate)) @@ -218,7 +220,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in // when self.client.subscribe(subscription: operation) { result in switch result { @@ -248,7 +250,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .pong)) diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index 05cb4202b7..21e704fc8b 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -6,6 +6,8 @@ import Apollo import SubscriptionAPI class GraphqlWsProtocolTests: XCTestCase { + private let asyncTimeout: DispatchTimeInterval = .seconds(3) + private var store: ApolloStore! private var mockWebSocket: MockWebSocket! private var websocketTransport: WebSocketTransport! { @@ -92,7 +94,7 @@ class GraphqlWsProtocolTests: XCTestCase { // given buildWebSocket() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: [:], type: .connectionInit)) @@ -110,7 +112,7 @@ class GraphqlWsProtocolTests: XCTestCase { // given websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionInit)) @@ -131,7 +133,7 @@ class GraphqlWsProtocolTests: XCTestCase { connectingPayload: ["sample": "data"] ) - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) @@ -153,7 +155,7 @@ class GraphqlWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .start)) @@ -175,7 +177,7 @@ class GraphqlWsProtocolTests: XCTestCase { let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then let expected = OperationMessage(id: "1", type: .stop).rawMessage! @@ -196,7 +198,7 @@ class GraphqlWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionTerminate)) @@ -218,7 +220,7 @@ class GraphqlWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil { done in + waitUntil(timeout: asyncTimeout) { done in // when self.client.subscribe(subscription: operation) { result in switch result { From fa3f459aba5745831e52e5161fc49172fce68b19 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 13:17:26 -0800 Subject: [PATCH 17/21] Fix test names --- .../WebSocket/GraphqlTransportWsProtocolTests.swift | 4 ++-- Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index 00501e7be2..c1da1a23fa 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -145,7 +145,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { } } - func test__messaging__givenSubscriptionSubscribe_shouldSendStartMessage() { + func test__messaging__givenSubscriptionSubscribe_shouldSendSubscribe() { // given buildWebSocket() buildClient() @@ -167,7 +167,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { } } - func test__messaging__givenSubscriptionCancel_shouldSendStopMessage() { + func test__messaging__givenSubscriptionCancel_shouldSendStop() { // given buildWebSocket() buildClient() diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index 21e704fc8b..071c4ed761 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -145,7 +145,7 @@ class GraphqlWsProtocolTests: XCTestCase { } } - func test__messaging__givenSubscriptionSubscribe_shouldSendStartMessage() { + func test__messaging__givenSubscriptionSubscribe_shouldSendStart() { // given buildWebSocket() buildClient() @@ -167,7 +167,7 @@ class GraphqlWsProtocolTests: XCTestCase { } } - func test__messaging__givenSubscriptionCancel_shouldSendStopMessage() { + func test__messaging__givenSubscriptionCancel_shouldSendStop() { // given buildWebSocket() buildClient() From 118745d88402f6c33d276ecf657729205280c51b Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 22:22:17 -0800 Subject: [PATCH 18/21] Fix project configuration Move OperationMessageMatchers to correct target and remove Nimble linking from ApolloTestSupport --- Apollo.xcodeproj/project.pbxproj | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index bda1ad20e0..15651e35e8 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -253,12 +253,11 @@ E61DD76526D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E61DD76426D60C1800C41614 /* SQLiteDotSwiftDatabaseBehaviorTests.swift */; }; E63C03DF27BDDC3D00D675C6 /* SubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */; }; E63C03E227BDE00400D675C6 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; + E63C67A327C8AA2A00B1654E /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; E657CDBA26FD01D4005834D6 /* ApolloSchemaInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */; }; E658545B27C5C1EE00339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658545A27C5C1EE00339378 /* Nimble */; }; E658545C27C5CA1C00339378 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658545D27C6028100339378 /* MockWebSocketDelegate.swift */; }; - E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; - E658546827C62A8700339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658546727C62A8700339378 /* Nimble */; }; E658546C27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */; }; E6630B8C26F0639B002D9E41 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6D79AB926EC05290094434A /* MockNetworkSession.swift */; }; E6630B8E26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */; }; @@ -938,7 +937,6 @@ buildActionMask = 2147483647; files = ( 9F65B1211EC106F30090B25F /* Apollo.framework in Frameworks */, - E658546827C62A8700339378 /* Nimble in Frameworks */, DED45EE4261BA1FB0086EF63 /* ApolloSQLite.framework in Frameworks */, DED45EE5261BA1FB0086EF63 /* ApolloWebSocket.framework in Frameworks */, ); @@ -2124,7 +2122,6 @@ ); name = ApolloTestSupport; packageProductDependencies = ( - E658546727C62A8700339378 /* Nimble */, ); productName = ApolloTestSupport; productReference = 9F8A95781EC0FC1200304A2D /* ApolloTestSupport.framework */; @@ -2697,7 +2694,6 @@ 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */, 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */, E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */, - E658546627C6277600339378 /* OperationMessageMatchers.swift in Sources */, 9F68F9F125415827004F26D0 /* XCTestCase+Helpers.swift in Sources */, 9BCF0CE523FC9CA50031D2A2 /* MockNetworkTransport.swift in Sources */, DE05862D2669800000265760 /* Matchable.swift in Sources */, @@ -2828,6 +2824,7 @@ F16D083C21EF6F7300C458B8 /* QueryFromJSONBuildingTests.swift in Sources */, 9BF6C97025194ED7000D5B93 /* MultipartFormDataTests.swift in Sources */, 9FF90A711DDDEB420034C3B6 /* ReadFieldValueTests.swift in Sources */, + E63C67A327C8AA2A00B1654E /* OperationMessageMatchers.swift in Sources */, 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, DED45DE9261B96B70086EF63 /* LoadQueryFromStoreTests.swift in Sources */, @@ -3650,11 +3647,6 @@ package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; productName = Nimble; }; - E658546727C62A8700339378 /* Nimble */ = { - isa = XCSwiftPackageProductDependency; - package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; - productName = Nimble; - }; E6A19C6127BEDAE00099C6E3 /* Nimble */ = { isa = XCSwiftPackageProductDependency; package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; From 3589cadb5f31146028ad3b47848af272a2612296 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Thu, 24 Feb 2022 22:36:06 -0800 Subject: [PATCH 19/21] Rename protocol parameter on WebSocket initializers --- .../DefaultImplementation/WebSocket.swift | 19 +++++++++---------- .../StarWarsSubscriptionTests.swift | 2 +- .../StarWarsWebSocketTests.swift | 2 +- .../SubscriptionTests.swift | 2 +- .../GraphqlTransportWsProtocolTests.swift | 6 +++--- .../WebSocket/GraphqlWsProtocolTests.swift | 6 +++--- docs/source/subscriptions.md | 4 ++-- .../source/tutorial/tutorial-subscriptions.md | 2 +- 8 files changed, 21 insertions(+), 22 deletions(-) diff --git a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index e8416285fe..591697592c 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -203,8 +203,8 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock /// /// - Parameters: /// - request: A URL request object that provides request-specific information such as the URL. - /// - webSocketProtocol: Protocol to use for communication over the web socket. - public init(request: URLRequest, webSocketProtocol: WSProtocol) { + /// - protocol: Protocol to use for communication over the web socket. + public init(request: URLRequest, protocol: WSProtocol) { self.request = request self.stream = FoundationStream() if request.value(forHTTPHeaderField: Constants.headerOriginName) == nil { @@ -217,8 +217,7 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } - self.request.setValue(webSocketProtocol.description, - forHTTPHeaderField: Constants.headerWSProtocolName) + self.request.setValue(`protocol`.description, forHTTPHeaderField: Constants.headerWSProtocolName) writeQueue.maxConcurrentOperationCount = 1 } @@ -227,12 +226,12 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock /// /// - Parameters: /// - url: The destination URL to connect to. - /// - webSocketProtocol: Protocol to use for communication over the web socket. - public convenience init(url: URL, webSocketProtocol: WSProtocol) { + /// - protocol: Protocol to use for communication over the web socket. + public convenience init(url: URL, protocol: WSProtocol) { var request = URLRequest(url: url) request.timeoutInterval = 5 - self.init(request: request, webSocketProtocol: webSocketProtocol) + self.init(request: request, protocol: `protocol`) } /// Convenience initializer to specify the URL and web socket protocol with a specific quality of @@ -241,13 +240,13 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock /// - Parameters: /// - url: The destination URL to connect to. /// - writeQueueQOS: Specifies the quality of service for the write queue. - /// - webSocketProtocol: Protocol to use for communication over the web socket. + /// - protocol: Protocol to use for communication over the web socket. public convenience init( url: URL, writeQueueQOS: QualityOfService, - webSocketProtocol: WSProtocol + protocol: WSProtocol ) { - self.init(url: url, webSocketProtocol: webSocketProtocol) + self.init(url: url, protocol: `protocol`) writeQueue.qualityOfService = writeQueueQOS } diff --git a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 0794e70aa0..a10f6b33a7 100644 --- a/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift @@ -23,7 +23,7 @@ class StarWarsSubscriptionTests: XCTestCase { webSocketTransport = WebSocketTransport( websocket: WebSocket( request: URLRequest(url: TestServerURL.starWarsWebSocket.url), - webSocketProtocol: .graphql_ws + protocol: .graphql_ws ), store: ApolloStore() ) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index 0519a30609..a38c4bb424 100755 --- a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift @@ -23,7 +23,7 @@ class StarWarsWebSocketTests: XCTestCase, CacheDependentTesting { let networkTransport = WebSocketTransport( websocket: WebSocket(request: URLRequest(url: TestServerURL.starWarsWebSocket.url), - webSocketProtocol: .graphql_ws), + protocol: .graphql_ws), store: store ) diff --git a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift index 1e800c38fe..b32fc34b86 100644 --- a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift +++ b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift @@ -18,7 +18,7 @@ class SubscriptionTests: XCTestCase { // given let store = ApolloStore() let webSocketTransport = WebSocketTransport( - websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, webSocketProtocol: .graphql_transport_ws), + websocket: WebSocket(url: TestServerURL.subscriptionWebSocket.url, protocol: .graphql_transport_ws), store: store ) webSocketTransport.delegate = self diff --git a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index c1da1a23fa..5d22c300a0 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -66,7 +66,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { expect( WebSocket( request: URLRequest(url: TestURL.mockServer.url), - webSocketProtocol: .graphql_transport_ws + protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-transport-ws")) } @@ -75,7 +75,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { expect( WebSocket( url: TestURL.mockServer.url, - webSocketProtocol: .graphql_transport_ws + protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-transport-ws")) @@ -83,7 +83,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { WebSocket( url: TestURL.mockServer.url, writeQueueQOS: .default, - webSocketProtocol: .graphql_transport_ws + protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-transport-ws")) } diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index 071c4ed761..f06b5d93e5 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -66,7 +66,7 @@ class GraphqlWsProtocolTests: XCTestCase { expect( WebSocket( request: URLRequest(url: TestURL.mockServer.url), - webSocketProtocol: .graphql_ws + protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-ws")) } @@ -75,7 +75,7 @@ class GraphqlWsProtocolTests: XCTestCase { expect( WebSocket( url: TestURL.mockServer.url, - webSocketProtocol: .graphql_ws + protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-ws")) @@ -83,7 +83,7 @@ class GraphqlWsProtocolTests: XCTestCase { WebSocket( url: TestURL.mockServer.url, writeQueueQOS: .default, - webSocketProtocol: .graphql_ws + protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") ).to(equal("graphql-ws")) } diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index f4a767a807..a7258dfbe9 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -44,7 +44,7 @@ class Apollo { /// A web socket transport to use for subscriptions private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_transport_ws) + let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws) return WebSocketTransport(websocket: webSocketClient) }() @@ -168,7 +168,7 @@ class Apollo { // initializes the connection as an authorized channel. private lazy var webSocketTransport: WebSocketTransport = { let url = URL(string: "ws://localhost:8080/websocket")! - let webSocketClient = WebSocket(url: url, webSocketProtocol: .graphql_transport_ws) + let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws) let authPayload = ["authToken": magicToken] return WebSocketTransport(websocket: webSocketClient, connectingPayload: authPayload) }() diff --git a/docs/source/tutorial/tutorial-subscriptions.md b/docs/source/tutorial/tutorial-subscriptions.md index e3e55aa039..b3a9c79daa 100644 --- a/docs/source/tutorial/tutorial-subscriptions.md +++ b/docs/source/tutorial/tutorial-subscriptions.md @@ -75,7 +75,7 @@ Next, in the lazy declaration of the `apollo` variable, immediately after `trans // 1 let webSocket = WebSocket( url: URL(string: "wss://apollo-fullstack-tutorial.herokuapp.com/graphql")!, - webSocketProtocol: .graphql_ws + protocol: .graphql_ws ) // 2 From 2a92350ef29593e0803e233863de1671ff324d79 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 25 Feb 2022 11:12:39 -0800 Subject: [PATCH 20/21] Revert "Use longer async timeout for slower environments like CI" This reverts commit d63d0789beea27c997173300121644e117aeedb2. --- .../GraphqlTransportWsProtocolTests.swift | 18 ++++++++---------- .../WebSocket/GraphqlWsProtocolTests.swift | 16 +++++++--------- 2 files changed, 15 insertions(+), 19 deletions(-) diff --git a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index 5d22c300a0..979dc89ae0 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -6,8 +6,6 @@ import Apollo import SubscriptionAPI class GraphqlTransportWsProtocolTests: XCTestCase { - private let asyncTimeout: DispatchTimeInterval = .seconds(3) - private var store: ApolloStore! private var mockWebSocket: MockWebSocket! private var websocketTransport: WebSocketTransport! { @@ -94,7 +92,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { // given buildWebSocket() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: [:], type: .connectionInit)) @@ -112,7 +110,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { // given websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionInit)) @@ -133,7 +131,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectingPayload: ["sample": "data"] ) - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) @@ -155,7 +153,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .subscribe)) @@ -177,7 +175,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then let expected = OperationMessage(id: "1", type: .stop).rawMessage! @@ -198,7 +196,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionTerminate)) @@ -220,7 +218,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in // when self.client.subscribe(subscription: operation) { result in switch result { @@ -250,7 +248,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .pong)) diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index f06b5d93e5..50fab1e7df 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -6,8 +6,6 @@ import Apollo import SubscriptionAPI class GraphqlWsProtocolTests: XCTestCase { - private let asyncTimeout: DispatchTimeInterval = .seconds(3) - private var store: ApolloStore! private var mockWebSocket: MockWebSocket! private var websocketTransport: WebSocketTransport! { @@ -94,7 +92,7 @@ class GraphqlWsProtocolTests: XCTestCase { // given buildWebSocket() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: [:], type: .connectionInit)) @@ -112,7 +110,7 @@ class GraphqlWsProtocolTests: XCTestCase { // given websocketTransport = WebSocketTransport(websocket: mockWebSocket, connectingPayload: nil) - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionInit)) @@ -133,7 +131,7 @@ class GraphqlWsProtocolTests: XCTestCase { connectingPayload: ["sample": "data"] ) - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: ["sample": "data"], type: .connectionInit)) @@ -155,7 +153,7 @@ class GraphqlWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(payload: operation.requestBody, id: "1", type: .start)) @@ -177,7 +175,7 @@ class GraphqlWsProtocolTests: XCTestCase { let subject = client.subscribe(subscription: IncrementingSubscription()) { _ in } - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then let expected = OperationMessage(id: "1", type: .stop).rawMessage! @@ -198,7 +196,7 @@ class GraphqlWsProtocolTests: XCTestCase { connectWebSocket() ackConnection() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in self.mockWebSocketDelegate.didReceiveMessage = { message in // then expect(message).to(equalMessage(type: .connectionTerminate)) @@ -220,7 +218,7 @@ class GraphqlWsProtocolTests: XCTestCase { let operation = IncrementingSubscription() - waitUntil(timeout: asyncTimeout) { done in + waitUntil { done in // when self.client.subscribe(subscription: operation) { result in switch result { From 9c92529d81d0a9b594a2a8693d2b9a79f6205954 Mon Sep 17 00:00:00 2001 From: Calvin Cestari Date: Fri, 25 Feb 2022 12:25:50 -0800 Subject: [PATCH 21/21] Fix async timing bug and refactor websocket protocol tests --- Apollo.xcodeproj/project.pbxproj | 4 + .../GraphqlTransportWsProtocolTests.swift | 77 +++--------------- .../WebSocket/GraphqlWsProtocolTests.swift | 74 +++-------------- .../WebSocket/WSProtocolTestsBase.swift | 81 +++++++++++++++++++ 4 files changed, 106 insertions(+), 130 deletions(-) create mode 100644 Tests/ApolloTests/WebSocket/WSProtocolTestsBase.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index 15651e35e8..82cba2fcd1 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -254,6 +254,7 @@ E63C03DF27BDDC3D00D675C6 /* SubscriptionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */; }; E63C03E227BDE00400D675C6 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; E63C67A327C8AA2A00B1654E /* OperationMessageMatchers.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658546527C6277600339378 /* OperationMessageMatchers.swift */; }; + E63F15CD27C96D6D006879ED /* WSProtocolTestsBase.swift in Sources */ = {isa = PBXBuildFile; fileRef = E63F15CC27C96D6D006879ED /* WSProtocolTestsBase.swift */; }; E657CDBA26FD01D4005834D6 /* ApolloSchemaInternalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */; }; E658545B27C5C1EE00339378 /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = E658545A27C5C1EE00339378 /* Nimble */; }; E658545C27C5CA1C00339378 /* SubscriptionAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */; }; @@ -851,6 +852,7 @@ E63C03D627BDBA8900D675C6 /* operation_ids.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = operation_ids.json; sourceTree = ""; }; E63C03DB27BDD99100D675C6 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionTests.swift; sourceTree = ""; }; + E63F15CC27C96D6D006879ED /* WSProtocolTestsBase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WSProtocolTestsBase.swift; sourceTree = ""; }; E657CDB926FD01D4005834D6 /* ApolloSchemaInternalTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ApolloSchemaInternalTests.swift; sourceTree = ""; }; E658545D27C6028100339378 /* MockWebSocketDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockWebSocketDelegate.swift; sourceTree = ""; }; E658546527C6277600339378 /* OperationMessageMatchers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OperationMessageMatchers.swift; sourceTree = ""; }; @@ -1816,6 +1818,7 @@ 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */, E6B9BDDA27C5693300CF911D /* GraphqlWsProtocolTests.swift */, E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */, + E63F15CC27C96D6D006879ED /* WSProtocolTestsBase.swift */, ); path = WebSocket; sourceTree = ""; @@ -2828,6 +2831,7 @@ 9F295E311E27534800A24949 /* NormalizeQueryResults.swift in Sources */, 9FF90A731DDDEB420034C3B6 /* ParseQueryResponseTests.swift in Sources */, DED45DE9261B96B70086EF63 /* LoadQueryFromStoreTests.swift in Sources */, + E63F15CD27C96D6D006879ED /* WSProtocolTestsBase.swift in Sources */, 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */, DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */, 19E9F6AC26D58A9A003AB80E /* OperationMessageIdCreatorTests.swift in Sources */, diff --git a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift index 979dc89ae0..8e1c97e9bc 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -5,57 +5,15 @@ import Nimble import Apollo import SubscriptionAPI -class GraphqlTransportWsProtocolTests: XCTestCase { - private var store: ApolloStore! - private var mockWebSocket: MockWebSocket! - private var websocketTransport: WebSocketTransport! { - didSet { - if let websocketTransport = websocketTransport { // caters for tearDown setting nil value - websocketTransport.websocket.delegate = mockWebSocketDelegate - } - } - } - private var mockWebSocketDelegate: MockWebSocketDelegate! - private var client: ApolloClient! - - override func setUp() { - super.setUp() - - store = ApolloStore() - } - - override func tearDown() { - client = nil - websocketTransport = nil - mockWebSocket = nil - mockWebSocketDelegate = nil - store = nil +class GraphqlTransportWsProtocolTests: WSProtocolTestsBase { - super.tearDown() - } + let `protocol` = "graphql-transport-ws" - // MARK: Helpers - - private func buildWebSocket() { + override var urlRequest: URLRequest { var request = URLRequest(url: TestURL.mockServer.url) - request.setValue("graphql-transport-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + request.setValue(`protocol`, forHTTPHeaderField: "Sec-WebSocket-Protocol") - mockWebSocketDelegate = MockWebSocketDelegate() - mockWebSocket = MockWebSocket(request: request) - websocketTransport = WebSocketTransport(websocket: mockWebSocket, store: store) - } - - private func buildClient() { - client = ApolloClient(networkTransport: websocketTransport, store: store) - } - - private func connectWebSocket() { - websocketTransport.socketConnectionState.mutate { $0 = .connected } - } - - private func ackConnection() { - let ackMessage = OperationMessage(type: .connectionAck).rawMessage! - websocketTransport.websocketDidReceiveMessage(socket: mockWebSocket, text: ackMessage) + return request } // MARK: Initializer Tests @@ -66,7 +24,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { request: URLRequest(url: TestURL.mockServer.url), protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal(`protocol`)) } func test__convenienceInitializers__shouldSetRequestProtocolHeader() { @@ -75,7 +33,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { url: TestURL.mockServer.url, protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal(`protocol`)) expect( WebSocket( @@ -83,7 +41,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { writeQueueQOS: .default, protocol: .graphql_transport_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-transport-ws")) + ).to(equal(`protocol`)) } // MARK: Protocol Tests @@ -231,12 +189,11 @@ class GraphqlTransportWsProtocolTests: XCTestCase { } } - let message = OperationMessage( + self.sendAsync(message: OperationMessage( payload: ["data": ["numberIncremented": 42]], id: "1", type: .next - ).rawMessage! - self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + )) } } @@ -256,19 +213,7 @@ class GraphqlTransportWsProtocolTests: XCTestCase { } // when - let message = OperationMessage(payload: ["sample": "data"], type: .ping).rawMessage! - self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + self.sendAsync(message: OperationMessage(payload: ["sample": "data"], type: .ping)) } } } - -private extension GraphQLOperation { - var requestBody: GraphQLMap { - ApolloRequestBodyCreator().requestBody( - for: self, - sendOperationIdentifiers: false, - sendQueryDocument: true, - autoPersistQuery: false - ) - } -} diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift index 50fab1e7df..f71ee63b79 100644 --- a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -5,57 +5,15 @@ import Nimble import Apollo import SubscriptionAPI -class GraphqlWsProtocolTests: XCTestCase { - private var store: ApolloStore! - private var mockWebSocket: MockWebSocket! - private var websocketTransport: WebSocketTransport! { - didSet { - if let websocketTransport = websocketTransport { // caters for tearDown setting nil value - websocketTransport.websocket.delegate = mockWebSocketDelegate - } - } - } - private var mockWebSocketDelegate: MockWebSocketDelegate! - private var client: ApolloClient! - - override func setUp() { - super.setUp() - - store = ApolloStore() - } - - override func tearDown() { - client = nil - websocketTransport = nil - mockWebSocket = nil - mockWebSocketDelegate = nil - store = nil +class GraphqlWsProtocolTests: WSProtocolTestsBase { - super.tearDown() - } + let `protocol` = "graphql-ws" - // MARK: Helpers - - private func buildWebSocket() { + override var urlRequest: URLRequest { var request = URLRequest(url: TestURL.mockServer.url) - request.setValue("graphql-ws", forHTTPHeaderField: "Sec-WebSocket-Protocol") + request.setValue(`protocol`, forHTTPHeaderField: "Sec-WebSocket-Protocol") - mockWebSocketDelegate = MockWebSocketDelegate() - mockWebSocket = MockWebSocket(request: request) - websocketTransport = WebSocketTransport(websocket: mockWebSocket, store: store) - } - - private func buildClient() { - client = ApolloClient(networkTransport: websocketTransport, store: store) - } - - private func connectWebSocket() { - websocketTransport.socketConnectionState.mutate { $0 = .connected } - } - - private func ackConnection() { - let ackMessage = OperationMessage(type: .connectionAck).rawMessage! - websocketTransport.websocketDidReceiveMessage(socket: mockWebSocket, text: ackMessage) + return request } // MARK: Initializer Tests @@ -66,7 +24,7 @@ class GraphqlWsProtocolTests: XCTestCase { request: URLRequest(url: TestURL.mockServer.url), protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal(`protocol`)) } func test__convenienceInitializers__shouldSetRequestProtocolHeader() { @@ -75,7 +33,7 @@ class GraphqlWsProtocolTests: XCTestCase { url: TestURL.mockServer.url, protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal(`protocol`)) expect( WebSocket( @@ -83,7 +41,7 @@ class GraphqlWsProtocolTests: XCTestCase { writeQueueQOS: .default, protocol: .graphql_ws ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") - ).to(equal("graphql-ws")) + ).to(equal(`protocol`)) } // MARK: Protocol Tests @@ -231,23 +189,11 @@ class GraphqlWsProtocolTests: XCTestCase { } } - let message = OperationMessage( + self.sendAsync(message: OperationMessage( payload: ["data": ["numberIncremented": 42]], id: "1", type: .data - ).rawMessage! - self.websocketTransport.websocketDidReceiveMessage(socket: self.mockWebSocket, text: message) + )) } } } - -private extension GraphQLOperation { - var requestBody: GraphQLMap { - ApolloRequestBodyCreator().requestBody( - for: self, - sendOperationIdentifiers: false, - sendQueryDocument: true, - autoPersistQuery: false - ) - } -} diff --git a/Tests/ApolloTests/WebSocket/WSProtocolTestsBase.swift b/Tests/ApolloTests/WebSocket/WSProtocolTestsBase.swift new file mode 100644 index 0000000000..8495457604 --- /dev/null +++ b/Tests/ApolloTests/WebSocket/WSProtocolTestsBase.swift @@ -0,0 +1,81 @@ +import XCTest +@testable import ApolloWebSocket +import ApolloTestSupport +import Nimble +import Apollo +import SubscriptionAPI + +class WSProtocolTestsBase: XCTestCase { + private var store: ApolloStore! + var mockWebSocket: MockWebSocket! + var websocketTransport: WebSocketTransport! { + didSet { + if let websocketTransport = websocketTransport { // caters for tearDown setting nil value + websocketTransport.websocket.delegate = mockWebSocketDelegate + } + } + } + var mockWebSocketDelegate: MockWebSocketDelegate! + var client: ApolloClient! + + override func setUp() { + super.setUp() + + store = ApolloStore() + } + + override func tearDown() { + client = nil + websocketTransport = nil + mockWebSocket = nil + mockWebSocketDelegate = nil + store = nil + + super.tearDown() + } + + // MARK: Helpers + + var urlRequest: URLRequest { + fatalError("Subclasses must override this property!") + } + + func buildWebSocket() { + mockWebSocketDelegate = MockWebSocketDelegate() + mockWebSocket = MockWebSocket(request: urlRequest) + websocketTransport = WebSocketTransport(websocket: mockWebSocket, store: store) + } + + func buildClient() { + client = ApolloClient(networkTransport: websocketTransport, store: store) + } + + func connectWebSocket() { + websocketTransport.socketConnectionState.mutate { $0 = .connected } + } + + func ackConnection() { + let ackMessage = OperationMessage(type: .connectionAck).rawMessage! + websocketTransport.websocketDidReceiveMessage(socket: mockWebSocket, text: ackMessage) + } + + func sendAsync(message: OperationMessage) { + websocketTransport.processingQueue.async { + self.websocketTransport.websocketDidReceiveMessage( + socket: self.mockWebSocket, + text: message.rawMessage! + ) + } + } +} + +extension GraphQLOperation { + var requestBody: GraphQLMap { + ApolloRequestBodyCreator().requestBody( + for: self, + sendOperationIdentifiers: false, + sendQueryDocument: true, + autoPersistQuery: false + ) + } +}