Skip to content

Commit 077e34e

Browse files
calvincestarigh-action-runner
authored andcommitted
feature: Enhanced Client Awareness (#638)
1 parent e4dde28 commit 077e34e

12 files changed

+370
-70
lines changed

Tests/ApolloTests/AutomaticPersistedQueriesTests.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,6 @@ class AutomaticPersistedQueriesTests: XCTestCase {
145145
expect(file: file, line: line, version).to(equal(1))
146146

147147
expect(file: file, line: line, sha256Hash).to(equal(O.operationIdentifier))
148-
149-
} else {
150-
expect(file: file, line: line, ext).to(beNil())
151148
}
152149
}
153150

@@ -238,9 +235,6 @@ class AutomaticPersistedQueriesTests: XCTestCase {
238235
expect(file: file, line: line, version).to(equal(1))
239236

240237
expect(file: file, line: line, sha256Hash).to(equal(MockHeroNameQuery.operationIdentifier))
241-
242-
} else {
243-
expect(file: file, line: line, ext).to(beNil())
244238
}
245239
}
246240

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import XCTest
2+
import Nimble
3+
@testable import Apollo
4+
import ApolloAPI
5+
import ApolloInternalTestHelpers
6+
7+
class RequestClientMetadataTests : XCTestCase {
8+
9+
private class Hero: MockSelectionSet {
10+
typealias Schema = MockSchemaMetadata
11+
12+
override class var __selections: [Selection] {[
13+
.field("__typename", String.self),
14+
.field("name", String.self)
15+
]}
16+
17+
var name: String { __data["name"] }
18+
}
19+
20+
// MARK: JSONRequest
21+
22+
func test__jsonRequest__usingDefaultInitializer_shouldAddClientHeadersAndExtension() throws {
23+
let jsonRequest = JSONRequest(
24+
operation: MockQuery<Hero>(),
25+
graphQLEndpoint: TestURL.mockServer.url,
26+
clientName: "test-client",
27+
clientVersion: "test-client-version"
28+
)
29+
30+
let urlRequest = try jsonRequest.toURLRequest()
31+
32+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
33+
fail("Missing HTTP header fields!")
34+
return
35+
}
36+
37+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
38+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
39+
40+
guard
41+
let httpBody = urlRequest.httpBody,
42+
let jsonBody = try? JSONSerialization.jsonObject(with: httpBody) as? JSONObject,
43+
let extensions = jsonBody["extensions"] as? JSONObject,
44+
let clientLibrary = extensions["clientLibrary"] as? JSONObject
45+
else {
46+
fail("Could not deserialize client library extension.")
47+
return
48+
}
49+
50+
expect(clientLibrary["name"]).to(equal(Constants.ApolloClientName))
51+
expect(clientLibrary["version"]).to(equal(Constants.ApolloClientVersion))
52+
}
53+
54+
func test__jsonRequest__usingInitializerEnablingClientExtension_shouldAddClientHeadersAndExtension() throws {
55+
let jsonRequest = JSONRequest(
56+
operation: MockQuery<Hero>(),
57+
graphQLEndpoint: TestURL.mockServer.url,
58+
clientName: "test-client",
59+
clientVersion: "test-client-version",
60+
sendEnhancedClientAwareness: true
61+
)
62+
63+
let urlRequest = try jsonRequest.toURLRequest()
64+
65+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
66+
fail("Missing HTTP header fields!")
67+
return
68+
}
69+
70+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
71+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
72+
73+
guard
74+
let httpBody = urlRequest.httpBody,
75+
let jsonBody = try? JSONSerialization.jsonObject(with: httpBody) as? JSONObject,
76+
let extensions = jsonBody["extensions"] as? JSONObject,
77+
let clientLibrary = extensions["clientLibrary"] as? JSONObject
78+
else {
79+
fail("Could not deserialize client library extension.")
80+
return
81+
}
82+
83+
expect(clientLibrary["name"]).to(equal(Constants.ApolloClientName))
84+
expect(clientLibrary["version"]).to(equal(Constants.ApolloClientVersion))
85+
}
86+
87+
func test__jsonRequest__usingInitializerDisablingClientExtension_shouldAddClientHeaders_doesNotAddClientExtension() throws {
88+
let jsonRequest = JSONRequest(
89+
operation: MockQuery<Hero>(),
90+
graphQLEndpoint: TestURL.mockServer.url,
91+
clientName: "test-client",
92+
clientVersion: "test-client-version",
93+
sendEnhancedClientAwareness: false
94+
)
95+
96+
let urlRequest = try jsonRequest.toURLRequest()
97+
98+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
99+
fail("Missing HTTP header fields!")
100+
return
101+
}
102+
103+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
104+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
105+
106+
guard
107+
let httpBody = urlRequest.httpBody,
108+
let jsonBody = try? JSONSerialization.jsonObject(with: httpBody) as? JSONObject
109+
else {
110+
fail("Could not deserialize client library extension.")
111+
return
112+
}
113+
114+
expect(jsonBody["extensions"]).to(beNil())
115+
}
116+
117+
// MARK: UploadRequest
118+
119+
func test__uploadRequest__usingDefaultInitializer_shouldAddClientHeadersAndExtension() throws {
120+
let uploadRequest = UploadRequest(
121+
graphQLEndpoint: TestURL.mockServer.url,
122+
operation: MockQuery<Hero>(),
123+
clientName: "test-client",
124+
clientVersion: "test-client-version",
125+
files: [GraphQLFile(fieldName: "x", originalName: "y", data: "z".data(using: .utf8)!)]
126+
)
127+
128+
let urlRequest = try uploadRequest.toURLRequest()
129+
130+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
131+
fail("Missing HTTP header fields!")
132+
return
133+
}
134+
135+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
136+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
137+
138+
guard
139+
let httpBody = urlRequest.httpBody,
140+
let multipartBody = String(data: httpBody, encoding: .utf8)
141+
else {
142+
fail("Could not deserialize client library extension.")
143+
return
144+
}
145+
146+
expect(multipartBody).to(contain("{\"extensions\":{\"clientLibrary\":{\"name\":\"apollo-ios\",\"version\":\"\(Constants.ApolloClientVersion)\"}},\"operationName\":\"MockOperationName\",\"query\":\"Mock Operation Definition\",\"variables\":{\"x\":null}}"))
147+
}
148+
149+
func test__uploadRequest__usingInitializerEnablingClientExtension_shouldAddClientHeadersAndExtension() throws {
150+
let uploadRequest = UploadRequest(
151+
graphQLEndpoint: TestURL.mockServer.url,
152+
operation: MockQuery<Hero>(),
153+
clientName: "test-client",
154+
clientVersion: "test-client-version",
155+
files: [GraphQLFile(fieldName: "x", originalName: "y", data: "z".data(using: .utf8)!)],
156+
sendEnhancedClientAwareness: true
157+
)
158+
159+
let urlRequest = try uploadRequest.toURLRequest()
160+
161+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
162+
fail("Missing HTTP header fields!")
163+
return
164+
}
165+
166+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
167+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
168+
169+
guard
170+
let httpBody = urlRequest.httpBody,
171+
let multipartBody = String(data: httpBody, encoding: .utf8)
172+
else {
173+
fail("Could not deserialize client library extension.")
174+
return
175+
}
176+
177+
expect(multipartBody).to(contain("{\"extensions\":{\"clientLibrary\":{\"name\":\"apollo-ios\",\"version\":\"\(Constants.ApolloClientVersion)\"}},\"operationName\":\"MockOperationName\",\"query\":\"Mock Operation Definition\",\"variables\":{\"x\":null}}"))
178+
}
179+
180+
func test__uploadRequest__usingInitializerDisablingClientExtension_shouldAddClientHeaders_doesNotAddClientExtension() throws {
181+
let uploadRequest = UploadRequest(
182+
graphQLEndpoint: TestURL.mockServer.url,
183+
operation: MockQuery<Hero>(),
184+
clientName: "test-client",
185+
clientVersion: "test-client-version",
186+
files: [GraphQLFile(fieldName: "x", originalName: "y", data: "z".data(using: .utf8)!)],
187+
sendEnhancedClientAwareness: false
188+
)
189+
190+
let urlRequest = try uploadRequest.toURLRequest()
191+
192+
guard let httpHeaderFields = urlRequest.allHTTPHeaderFields else {
193+
fail("Missing HTTP header fields!")
194+
return
195+
}
196+
197+
expect(httpHeaderFields["apollographql-client-version"]).to(equal("test-client-version"))
198+
expect(httpHeaderFields["apollographql-client-name"]).to(equal("test-client"))
199+
200+
guard
201+
let httpBody = urlRequest.httpBody,
202+
let multipartBody = String(data: httpBody, encoding: .utf8)
203+
else {
204+
fail("Could not deserialize client library extension.")
205+
return
206+
}
207+
208+
expect(multipartBody).notTo(contain("\"extensions\":{\"clientLibrary\":{\"name\":\"apollo-ios\",\"version\":\"\(Constants.ApolloClientVersion)\"}}"))
209+
}
210+
}

Tests/ApolloTests/UploadRequestTests.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ class UploadRequestTests: XCTestCase {
5656
--TEST.BOUNDARY
5757
Content-Disposition: form-data; name="operations"
5858
59-
{"operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) { singleUpload(file: $file) { __typename id path filename mimetype } }","variables":{"file":null}}
59+
{"extensions":{"clientLibrary":{"name":"apollo-ios","version":"\(Constants.ApolloClientVersion)"}},"operationName":"UploadOneFile","query":"mutation UploadOneFile($file: Upload!) { singleUpload(file: $file) { __typename id path filename mimetype } }","variables":{"file":null}}
6060
--TEST.BOUNDARY
6161
Content-Disposition: form-data; name="map"
6262
@@ -104,7 +104,7 @@ Alpha file content.
104104
--TEST.BOUNDARY
105105
Content-Disposition: form-data; name="operations"
106106
107-
{"operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) { multipleUpload(files: $files) { __typename id path filename mimetype } }","variables":{"files":[null,null]}}
107+
{"extensions":{"clientLibrary":{"name":"apollo-ios","version":"\(Constants.ApolloClientVersion)"}},"operationName":"UploadMultipleFilesToTheSameParameter","query":"mutation UploadMultipleFilesToTheSameParameter($files: [Upload!]!) { multipleUpload(files: $files) { __typename id path filename mimetype } }","variables":{"files":[null,null]}}
108108
--TEST.BOUNDARY
109109
Content-Disposition: form-data; name="map"
110110
@@ -166,7 +166,7 @@ Bravo file content.
166166
--TEST.BOUNDARY
167167
Content-Disposition: form-data; name="operations"
168168
169-
{"operationName":"UploadMultipleFilesToDifferentParameters","query":"mutation UploadMultipleFilesToDifferentParameters($singleFile: Upload!, $multipleFiles: [Upload!]!) { multipleParameterUpload(singleFile: $singleFile, multipleFiles: $multipleFiles) { __typename id path filename mimetype } }","variables":{"multipleFiles":["b.txt","c.txt"],"secondField":null,"singleFile":"a.txt","uploads":null}}
169+
{"extensions":{"clientLibrary":{"name":"apollo-ios","version":"\(Constants.ApolloClientVersion)"}},"operationName":"UploadMultipleFilesToDifferentParameters","query":"mutation UploadMultipleFilesToDifferentParameters($singleFile: Upload!, $multipleFiles: [Upload!]!) { multipleParameterUpload(singleFile: $singleFile, multipleFiles: $multipleFiles) { __typename id path filename mimetype } }","variables":{"multipleFiles":["b.txt","c.txt"],"secondField":null,"singleFile":"a.txt","uploads":null}}
170170
--TEST.BOUNDARY
171171
Content-Disposition: form-data; name="map"
172172

Tests/CodegenCLITests/VersionCheckerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class VerifyCLIVersionUpdateTest: XCTestCase {
1111
/// This version number uses the project configurations `CURRENT_PROJECT_VERSION`.
1212
func test__cliVersion__matchesApolloProjectVersion() {
1313
// given
14-
let codegenLibVersion = Apollo.Constants.ApolloVersion
14+
let codegenLibVersion = Apollo.Constants.ApolloClientVersion
1515

1616
// when
1717
let cliVersion = CodegenCLI.Constants.CLIVersion

apollo-ios/Sources/Apollo/ApolloClient.swift

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class ApolloClient {
3434

3535
public let store: ApolloStore
3636

37+
private let sendEnhancedClientAwareness: Bool
38+
3739
public enum ApolloClientError: Error, LocalizedError, Hashable {
3840
case noUploadTransport
3941

@@ -49,22 +51,40 @@ public class ApolloClient {
4951
///
5052
/// - Parameters:
5153
/// - networkTransport: A network transport used to send operations to a server.
52-
/// - store: A store used as a local cache. Note that if the `NetworkTransport` or any of its dependencies takes a store, you should make sure the same store is passed here so that it can be cleared properly.
53-
public init(networkTransport: any NetworkTransport, store: ApolloStore) {
54+
/// - store: A store used as a local cache. Note that if the `NetworkTransport` or any of its dependencies takes
55+
/// a store, you should make sure the same store is passed here so that it can be cleared properly.
56+
/// - sendEnhancedClientAwareness: Specifies whether client library metadata is sent in each request `extensions`
57+
/// key. Client library metadata is the Apollo iOS library name and version. Defaults to `true`.
58+
public init(
59+
networkTransport: any NetworkTransport,
60+
store: ApolloStore,
61+
sendEnhancedClientAwareness: Bool = true
62+
) {
5463
self.networkTransport = networkTransport
5564
self.store = store
65+
self.sendEnhancedClientAwareness = sendEnhancedClientAwareness
5666
}
5767

5868
/// Creates a client with a `RequestChainNetworkTransport` connecting to the specified URL.
5969
///
6070
/// - Parameter url: The URL of a GraphQL server to connect to.
61-
public convenience init(url: URL) {
71+
public convenience init(
72+
url: URL,
73+
sendEnhancedClientAwareness: Bool = true
74+
) {
6275
let store = ApolloStore(cache: InMemoryNormalizedCache())
6376
let provider = DefaultInterceptorProvider(store: store)
64-
let transport = RequestChainNetworkTransport(interceptorProvider: provider,
65-
endpointURL: url)
66-
67-
self.init(networkTransport: transport, store: store)
77+
let transport = RequestChainNetworkTransport(
78+
interceptorProvider: provider,
79+
endpointURL: url,
80+
sendEnhancedClientAwareness: sendEnhancedClientAwareness
81+
)
82+
83+
self.init(
84+
networkTransport: transport,
85+
store: store,
86+
sendEnhancedClientAwareness: sendEnhancedClientAwareness
87+
)
6888
}
6989
}
7090

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import Foundation
22

33
public enum Constants {
4-
public static let ApolloVersion: String = "1.20.0"
4+
public static let ApolloClientName = "apollo-ios"
5+
public static let ApolloClientVersion: String = "1.20.0"
6+
7+
@available(*, deprecated, renamed: "ApolloClientVersion")
8+
public static let ApolloVersion: String = ApolloClientVersion
59
}

0 commit comments

Comments
 (0)