diff --git a/.circleci/config.yml b/.circleci/config.yml index 1841d762aa..984e441bc0 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 @@ -43,18 +45,29 @@ 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: 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/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index e8cb61b051..82cba2fcd1 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -251,9 +251,22 @@ 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 */; }; + 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 */; }; + E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = E658545D27C6028100339378 /* MockWebSocketDelegate.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 /* 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 */; }; @@ -479,6 +492,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 +848,23 @@ 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 = ""; }; + 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 = ""; }; + 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 /* 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 = ""; }; E6D79AB926EC05290094434A /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = ""; }; E86D8E03214B32DA0028EFE1 /* JSONTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONTests.swift; sourceTree = ""; }; @@ -915,6 +956,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 */, @@ -941,10 +984,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 +1005,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 +1037,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 */, @@ -1006,6 +1060,7 @@ 9BF6C99B25195019000D5B93 /* String+IncludesForTesting.swift */, C3279FC52345233000224790 /* TestCustomRequestBodyCreator.swift */, 9B64F6752354D219002D1BB5 /* URL+QueryDict.swift */, + E658546527C6277600339378 /* OperationMessageMatchers.swift */, ); name = TestHelpers; sourceTree = ""; @@ -1302,6 +1357,7 @@ 9FBE0D3F25407B64002ED0B1 /* AsyncResultObserver.swift */, 9F68F9F025415827004F26D0 /* XCTestCase+Helpers.swift */, 9B2061162591B3550020D1E0 /* Resources */, + E658545D27C6028100339378 /* MockWebSocketDelegate.swift */, ); name = ApolloTestSupport; path = Sources/ApolloTestSupport; @@ -1434,6 +1490,7 @@ 9BDF200723FDC37600153E2B /* GitHubAPI */, 9BCF0CE923FC9F060031D2A2 /* StarWarsAPI */, 9B2DFBC424E1FA3E00ED3AE6 /* UploadAPI */, + E6A901D527BDAFA100931C9E /* SubscriptionAPI */, DECD490C262F81BF00924527 /* ApolloCodegenTestSupport */, 9B7BDAF923FDEE8A00ACD198 /* Frameworks */, 90690D04224333DA00FC2E54 /* Configuration */, @@ -1462,6 +1519,7 @@ DE6B15AC26152BE10068D642 /* ApolloServerIntegrationTests.xctest */, DECD490B262F81BF00924527 /* ApolloCodegenTestSupport.framework */, DE058621266978A100265760 /* ApolloAPI.framework */, + E6A901D427BDAFA100931C9E /* SubscriptionAPI.framework */, ); name = Products; sourceTree = ""; @@ -1669,6 +1727,7 @@ DED45F49261CDBFC0086EF63 /* UploadTests.swift */, DECD46CF262F64D000924527 /* StarWarsApolloSchemaDownloaderTests.swift */, E6630B8D26F071F9002D9E41 /* SchemaRegistryApolloSchemaDownloaderTests.swift */, + E63C03DD27BDDC3400D675C6 /* SubscriptionTests.swift */, ); path = ApolloServerIntegrationTests; sourceTree = ""; @@ -1757,6 +1816,9 @@ D90F1AF92479DEE5007A1534 /* WebSocketTransportTests.swift */, DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */, 19E9F6AA26D58A92003AB80E /* OperationMessageIdCreatorTests.swift */, + E6B9BDDA27C5693300CF911D /* GraphqlWsProtocolTests.swift */, + E658546B27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift */, + E63F15CC27C96D6D006879ED /* WSProtocolTestsBase.swift */, ); path = WebSocket; sourceTree = ""; @@ -1773,6 +1835,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 +1855,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 +1951,14 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901CF27BDAFA100931C9E /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + E6A901D727BDAFA100931C9E /* SubscriptionAPI.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXHeadersBuildPhase section */ /* Begin PBXNativeTarget section */ @@ -2096,6 +2188,9 @@ 9FCE2D081E6C254000E34457 /* PBXTargetDependency */, ); name = ApolloTests; + packageProductDependencies = ( + E658545A27C5C1EE00339378 /* Nimble */, + ); productName = ApolloTests; productReference = 9FC7504E1D2A532D00458D91 /* ApolloTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; @@ -2149,6 +2244,7 @@ buildRules = ( ); dependencies = ( + E63C03E127BDDFEF00D675C6 /* PBXTargetDependency */, DECD498E262F840100924527 /* PBXTargetDependency */, DECD4735262F668200924527 /* PBXTargetDependency */, DECD46FA262F659100924527 /* PBXTargetDependency */, @@ -2160,6 +2256,7 @@ ); name = ApolloServerIntegrationTests; packageProductDependencies = ( + E6A19C6127BEDAE00099C6E3 /* Nimble */, ); productName = ApolloServerIntegrationTests; productReference = DE6B15AC26152BE10068D642 /* ApolloServerIntegrationTests.xctest */; @@ -2184,6 +2281,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 +2364,10 @@ DECD490A262F81BF00924527 = { CreatedOnToolsVersion = 12.4; }; + E6A901D327BDAFA100931C9E = { + CreatedOnToolsVersion = 13.2.1; + LastSwiftMigration = 1320; + }; }; }; buildConfigurationList = 9FC7503E1D2A532C00458D91 /* Build configuration list for PBXProject "Apollo" */; @@ -2261,6 +2381,7 @@ mainGroup = 9FC7503A1D2A532C00458D91; packageReferences = ( 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */, + E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */, ); productRefGroup = 9FC750451D2A532C00458D91 /* Products */; projectDirPath = ""; @@ -2278,6 +2399,7 @@ 9FCE2CF91E6C213D00E34457 /* StarWarsAPI */, 9FACA9B71F42E67200AE2DBD /* GitHubAPI */, 9B2DFBB524E1FA0D00ED3AE6 /* UploadAPI */, + E6A901D327BDAFA100931C9E /* SubscriptionAPI */, 9B7B6F46233C26D100F32205 /* ApolloCodegenLib */, 9BAEEBFB234BB8FD00808306 /* ApolloCodegenTests */, DECD490A262F81BF00924527 /* ApolloCodegenTestSupport */, @@ -2392,6 +2514,13 @@ ); runOnlyForDeploymentPostprocessing = 0; }; + E6A901D227BDAFA100931C9E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ @@ -2567,6 +2696,7 @@ DED4600D261CE9260086EF63 /* TestFileHelper.swift in Sources */, 9BCF0CE023FC9CA50031D2A2 /* TestCacheProvider.swift in Sources */, 9BCF0CE323FC9CA50031D2A2 /* XCTAssertHelpers.swift in Sources */, + E658545E27C6028100339378 /* MockWebSocketDelegate.swift in Sources */, 9F68F9F125415827004F26D0 /* XCTestCase+Helpers.swift in Sources */, 9BCF0CE523FC9CA50031D2A2 /* MockNetworkTransport.swift in Sources */, DE05862D2669800000265760 /* Matchable.swift in Sources */, @@ -2672,6 +2802,7 @@ 9F533AB31E6C4A4200CBE097 /* BatchedLoadTests.swift in Sources */, DED45DEE261B96B70086EF63 /* FetchQueryTests.swift in Sources */, C3279FC72345234D00224790 /* TestCustomRequestBodyCreator.swift in Sources */, + E658546C27C77B8B00339378 /* GraphqlTransportWsProtocolTests.swift in Sources */, DED45DED261B96B70086EF63 /* StoreConcurrencyTests.swift in Sources */, 9B95EDC022CAA0B000702BB2 /* GETTransformerTests.swift in Sources */, 9FF90A6F1DDDEB420034C3B6 /* GraphQLMapEncodingTests.swift in Sources */, @@ -2687,6 +2818,7 @@ E86D8E05214B32FD0028EFE1 /* JSONTests.swift in Sources */, 2EE7FFD0276802E30035DC39 /* CacheKeyConstructionTests.swift in Sources */, 9F8622FA1EC2117C00C38162 /* FragmentConstructionAndConversionTests.swift in Sources */, + E6B9BDDB27C5693300CF911D /* GraphqlWsProtocolTests.swift in Sources */, DED45C2A2615319E0086EF63 /* DefaultInterceptorProviderTests.swift in Sources */, 9F21730E2567E6F000566121 /* DataLoaderTests.swift in Sources */, DED45DEC261B96B70086EF63 /* CacheDependentInterceptorTests.swift in Sources */, @@ -2695,9 +2827,11 @@ 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 */, + E63F15CD27C96D6D006879ED /* WSProtocolTestsBase.swift in Sources */, 9BF6C94325194DE2000D5B93 /* MultipartFormData+Testing.swift in Sources */, DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */, 19E9F6AC26D58A9A003AB80E /* OperationMessageIdCreatorTests.swift in Sources */, @@ -2738,6 +2872,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 +2892,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 +3066,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 +3415,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 +3599,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 +3620,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 +3646,16 @@ package = 9B7BDAF423FDEE2600ACD198 /* XCRemoteSwiftPackageReference "SQLite.swift" */; productName = SQLite; }; + E658545A27C5C1EE00339378 /* Nimble */ = { + isa = XCSwiftPackageProductDependency; + package = E6A19C6027BEDAE00099C6E3 /* XCRemoteSwiftPackageReference "Nimble" */; + productName = Nimble; + }; + 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/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/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/DefaultImplementation/WebSocket.swift b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift index c4c1bb3b6d..591697592c 100644 --- a/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift +++ b/Sources/ApolloWebSocket/DefaultImplementation/WebSocket.swift @@ -68,14 +68,30 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock public let code: Int } - private struct Constants { + /// The GraphQL over WebSocket protocols supported by apollo-ios. + public enum WSProtocol: CustomStringConvertible { + /// 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 .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" @@ -183,8 +199,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. + /// - 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 { @@ -197,20 +217,36 @@ public final class WebSocket: NSObject, WebSocketClient, StreamDelegate, WebSock self.request.setValue(origin, forHTTPHeaderField: Constants.headerOriginName) } - self.request.setValue(Constants.headerWSProtocolValue, - forHTTPHeaderField: Constants.headerWSProtocolName) + self.request.setValue(`protocol`.description, forHTTPHeaderField: Constants.headerWSProtocolName) + writeQueue.maxConcurrentOperationCount = 1 } - public convenience init(url: URL) { + /// Convenience initializer to specify the URL and web socket protocol. + /// + /// - Parameters: + /// - url: The destination URL to connect to. + /// - 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) + + self.init(request: request, protocol: `protocol`) } - // Used for specifically setting the QOS for the write queue. - public convenience init(url: URL, writeQueueQOS: QualityOfService) { - self.init(url: url) + /// 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. + /// - protocol: Protocol to use for communication over the web socket. + public convenience init( + url: URL, + writeQueueQOS: QualityOfService, + protocol: WSProtocol + ) { + self.init(url: url, protocol: `protocol`) writeQueue.qualityOfService = writeQueueQOS } diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index d0719262b8..96cb4da2de 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,10 @@ final class OperationMessage { case data = "data" // Server -> Client 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 @@ -34,7 +39,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 @@ -99,6 +104,12 @@ final class OperationMessage { } } +extension OperationMessage: CustomDebugStringConvertible { + var debugDescription: String { + rawMessage! + } +} + struct ParseHandler { let type: String? let id: String? diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index aed9d25bec..20932d84c3 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 { @@ -180,11 +181,19 @@ 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, .start, .stop, .connectionError: @@ -270,7 +279,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 } 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/StarWarsSubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsSubscriptionTests.swift index 8da86d0f78..a10f6b33a7 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), + protocol: .graphql_ws ), store: ApolloStore() ) diff --git a/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift b/Tests/ApolloServerIntegrationTests/StarWarsWebSocketTests.swift index c72de86616..a38c4bb424 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), + protocol: .graphql_ws), store: store ) diff --git a/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift b/Tests/ApolloServerIntegrationTests/SubscriptionTests.swift new file mode 100644 index 0000000000..b32fc34b86 --- /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, protocol: .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..95c504bd1c 100644 --- a/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift +++ b/Tests/ApolloServerIntegrationTests/TestHelpers/TestServerURLs.swift @@ -5,7 +5,9 @@ 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" public var url: URL { return URL(string: self.rawValue)! 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/GraphqlTransportWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift new file mode 100644 index 0000000000..8e1c97e9bc --- /dev/null +++ b/Tests/ApolloTests/WebSocket/GraphqlTransportWsProtocolTests.swift @@ -0,0 +1,219 @@ +import XCTest +@testable import ApolloWebSocket +import ApolloTestSupport +import Nimble +import Apollo +import SubscriptionAPI + +class GraphqlTransportWsProtocolTests: WSProtocolTestsBase { + + let `protocol` = "graphql-transport-ws" + + override var urlRequest: URLRequest { + var request = URLRequest(url: TestURL.mockServer.url) + request.setValue(`protocol`, forHTTPHeaderField: "Sec-WebSocket-Protocol") + + return request + } + + // MARK: Initializer Tests + + func test__designatedInitializer__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + request: URLRequest(url: TestURL.mockServer.url), + protocol: .graphql_transport_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + } + + func test__convenienceInitializers__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + url: TestURL.mockServer.url, + protocol: .graphql_transport_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + + expect( + WebSocket( + url: TestURL.mockServer.url, + writeQueueQOS: .default, + protocol: .graphql_transport_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + } + + // 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_shouldSendSubscribe() { + // 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_shouldSendStop() { + // 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() + } + } + + self.sendAsync(message: OperationMessage( + payload: ["data": ["numberIncremented": 42]], + id: "1", + type: .next + )) + } + } + + 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 + self.sendAsync(message: OperationMessage(payload: ["sample": "data"], type: .ping)) + } + } +} diff --git a/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift new file mode 100644 index 0000000000..f71ee63b79 --- /dev/null +++ b/Tests/ApolloTests/WebSocket/GraphqlWsProtocolTests.swift @@ -0,0 +1,199 @@ +import XCTest +@testable import ApolloWebSocket +import ApolloTestSupport +import Nimble +import Apollo +import SubscriptionAPI + +class GraphqlWsProtocolTests: WSProtocolTestsBase { + + let `protocol` = "graphql-ws" + + override var urlRequest: URLRequest { + var request = URLRequest(url: TestURL.mockServer.url) + request.setValue(`protocol`, forHTTPHeaderField: "Sec-WebSocket-Protocol") + + return request + } + + // MARK: Initializer Tests + + func test__designatedInitializer__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + request: URLRequest(url: TestURL.mockServer.url), + protocol: .graphql_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + } + + func test__convenienceInitializers__shouldSetRequestProtocolHeader() { + expect( + WebSocket( + url: TestURL.mockServer.url, + protocol: .graphql_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + + expect( + WebSocket( + url: TestURL.mockServer.url, + writeQueueQOS: .default, + protocol: .graphql_ws + ).request.value(forHTTPHeaderField: "Sec-WebSocket-Protocol") + ).to(equal(`protocol`)) + } + + // 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_shouldSendStart() { + // 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_shouldSendStop() { + // 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__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() + } + } + + self.sendAsync(message: OperationMessage( + payload: ["data": ["numberIncremented": 42]], + id: "1", + type: .data + )) + } + } +} 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 + ) + } +} 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) {} - -} diff --git a/docs/source/subscriptions.md b/docs/source/subscriptions.md index fe21f39ce3..a7258dfbe9 100644 --- a/docs/source/subscriptions.md +++ b/docs/source/subscriptions.md @@ -19,6 +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 + +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 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 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 +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 request = URLRequest(url: url) - let webSocketClient = WebSocket(request: request) + let webSocketClient = WebSocket(url: url, protocol: .graphql_transport_ws) return WebSocketTransport(websocket: webSocketClient) }() @@ -161,8 +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 request = URLRequest(url: url) - let webSocketClient = WebSocket(request: request) + 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 6d5eb70fa9..b3a9c79daa 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")!, + protocol: .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) 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..b301cb17c2 100755 --- a/scripts/install-node.sh +++ b/scripts/install-node-v12.sh @@ -4,6 +4,5 @@ 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 +echo nvm use v16.13.1 >> $BASH_ENV \ No newline at end of file