Skip to content

Commit cba873a

Browse files
committed
WIP: Network fetch returns data as chunks
1 parent 6a17912 commit cba873a

File tree

54 files changed

+4622
-5383
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+4622
-5383
lines changed

Tests/ApolloInternalTestHelpers/AsyncResultObserver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import XCTest
99
public class AsyncResultObserver<Success, Failure> where Failure: Error {
1010
public typealias ResultHandler = (Result<Success, Failure>) throws -> Void
1111

12-
private class AsyncResultExpectation: XCTestExpectation {
12+
private final class AsyncResultExpectation: XCTestExpectation {
1313
let file: StaticString
1414
let line: UInt
1515
let handler: ResultHandler
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
public extension AsyncSequence {
4+
5+
/// Waits for all values from an async sequence and then returns them as a single array.
6+
func getAllValues() async throws -> [Element] {
7+
var values = [Element]()
8+
for try await value in self {
9+
values.append(value)
10+
}
11+
return values
12+
}
13+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
@testable import Apollo
2+
import ApolloAPI
3+
import Foundation
4+
5+
public final class InterceptorResponseMocker<Operation: GraphQLOperation>: Sendable {
6+
7+
private let internalStream: AsyncThrowingStream<InterceptorResult<Operation>, any Error>
8+
private let continuation: AsyncThrowingStream<InterceptorResult<Operation>, any Error>.Continuation
9+
10+
public init() {
11+
(self.internalStream, self.continuation) =
12+
AsyncThrowingStream<InterceptorResult<Operation>, any Error>.makeStream()
13+
}
14+
15+
public func getStream() -> InterceptorResultStream<Operation> {
16+
InterceptorResultStream(stream: internalStream)
17+
}
18+
19+
public func emit(response: InterceptorResult<Operation>) {
20+
continuation.yield(response)
21+
}
22+
23+
public func emit(error: any Error) {
24+
continuation.finish(throwing: error)
25+
}
26+
27+
public func finish() {
28+
continuation.finish()
29+
}
30+
31+
deinit {
32+
continuation.finish()
33+
}
34+
}
35+
36+
extension InterceptorResult {
37+
public static func mock(
38+
response: HTTPURLResponse = .mock(),
39+
dataChunk: Data = Data(),
40+
parsedResult: ParsedResult? = nil
41+
) -> Self {
42+
self.init(response: response, rawResponseChunk: dataChunk, parsedResult: parsedResult)
43+
}
44+
}

Tests/ApolloInternalTestHelpers/InterceptorTester.swift

Lines changed: 0 additions & 82 deletions
This file was deleted.

Tests/ApolloInternalTestHelpers/MockGraphQLServer.swift

Lines changed: 103 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,20 @@ public class MockGraphQLServer {
4141
}
4242
}
4343

44-
public var customDelay: DispatchTimeInterval?
45-
public typealias RequestHandler<Operation: GraphQLOperation> = (HTTPRequest<Operation>) -> JSONObject
44+
public typealias RequestHandler<Operation: GraphQLOperation> = (any GraphQLRequest<Operation>) ->
45+
JSONObject
4646

47-
private class RequestExpectation<Operation: GraphQLOperation>: XCTestExpectation {
47+
private class RequestExpectation<Operation: GraphQLOperation>: XCTestExpectation, @unchecked Sendable {
4848
let file: StaticString
4949
let line: UInt
5050
let handler: RequestHandler<Operation>
5151

52-
init(description: String, file: StaticString = #filePath, line: UInt = #line, handler: @escaping RequestHandler<Operation>) {
52+
init(
53+
description: String,
54+
file: StaticString = #filePath,
55+
line: UInt = #line,
56+
handler: @escaping RequestHandler<Operation>
57+
) {
5358
self.file = file
5459
self.line = line
5560
self.handler = handler
@@ -60,14 +65,16 @@ public class MockGraphQLServer {
6065

6166
private let queue = DispatchQueue(label: "com.apollographql.MockGraphQLServer")
6267

63-
public init() { }
68+
public init() {}
6469

6570
// Since RequestExpectation is generic over a specific GraphQLOperation, we can't store these in the dictionary
6671
// directly. Moreover, there is no way to specify the type relationship that holds between the key and value.
6772
// To work around this, we store values as Any and use a generic subscript as a type-safe way to access them.
6873
private var requestExpectations: [AnyHashable: Any] = [:]
6974

70-
private subscript<Operation: GraphQLOperation>(_ operationType: Operation.Type) -> RequestExpectation<Operation>? {
75+
private subscript<Operation: GraphQLOperation>(_ operationType: Operation.Type)
76+
-> RequestExpectation<Operation>?
77+
{
7178
get {
7279
requestExpectations[ObjectIdentifier(operationType)] as! RequestExpectation<Operation>?
7380
}
@@ -77,7 +84,9 @@ public class MockGraphQLServer {
7784
}
7885
}
7986

80-
private subscript<Operation: GraphQLOperation>(_ operationType: Operation) -> RequestExpectation<Operation>? {
87+
private subscript<Operation: GraphQLOperation>(_ operationType: Operation) -> RequestExpectation<
88+
Operation
89+
>? {
8190
get {
8291
requestExpectations[operationType] as! RequestExpectation<Operation>?
8392
}
@@ -87,9 +96,19 @@ public class MockGraphQLServer {
8796
}
8897
}
8998

90-
public func expect<Operation: GraphQLOperation>(_ operationType: Operation.Type, file: StaticString = #filePath, line: UInt = #line, requestHandler: @escaping (HTTPRequest<Operation>) -> JSONObject) -> XCTestExpectation {
99+
public func expect<Operation: GraphQLOperation>(
100+
_ operationType: Operation.Type,
101+
file: StaticString = #filePath,
102+
line: UInt = #line,
103+
requestHandler: @escaping RequestHandler<Operation>
104+
) -> XCTestExpectation {
91105
return queue.sync {
92-
let expectation = RequestExpectation<Operation>(description: "Served request for \(String(describing: operationType))", file: file, line: line, handler: requestHandler)
106+
let expectation = RequestExpectation<Operation>(
107+
description: "Served request for \(String(describing: operationType))",
108+
file: file,
109+
line: line,
110+
handler: requestHandler
111+
)
93112
expectation.assertForOverFulfill = true
94113

95114
self[operationType] = expectation
@@ -98,9 +117,19 @@ public class MockGraphQLServer {
98117
}
99118
}
100119

101-
public func expect<Operation: GraphQLOperation>(_ operation: Operation, file: StaticString = #filePath, line: UInt = #line, requestHandler: @escaping (HTTPRequest<Operation>) -> JSONObject) -> XCTestExpectation {
120+
public func expect<Operation: GraphQLOperation>(
121+
_ operation: Operation,
122+
file: StaticString = #filePath,
123+
line: UInt = #line,
124+
requestHandler: @escaping RequestHandler<Operation>
125+
) -> XCTestExpectation {
102126
return queue.sync {
103-
let expectation = RequestExpectation<Operation>(description: "Served request for \(String(describing: operation.self))", file: file, line: line, handler: requestHandler)
127+
let expectation = RequestExpectation<Operation>(
128+
description: "Served request for \(String(describing: operation.self))",
129+
file: file,
130+
line: line,
131+
handler: requestHandler
132+
)
104133
expectation.assertForOverFulfill = true
105134

106135
self[operation] = expectation
@@ -109,21 +138,76 @@ public class MockGraphQLServer {
109138
}
110139
}
111140

112-
func serve<Operation>(request: HTTPRequest<Operation>, completionHandler: @escaping (Result<JSONObject, any Error>) -> Void) where Operation: GraphQLOperation {
141+
func serve<Operation>(
142+
request: any GraphQLRequest<Operation>
143+
) async throws -> JSONObject where Operation: GraphQLOperation {
113144
let operationType = type(of: request.operation)
114145

115146
if let expectation = self[request.operation] ?? self[operationType] {
116147
// Dispatch after a small random delay to spread out concurrent requests and simulate somewhat real-world conditions.
117-
queue.asyncAfter(deadline: .now() + (customDelay ?? .milliseconds(Int.random(in: 10...50)))) {
118-
completionHandler(.success(expectation.handler(request)))
119-
expectation.fulfill()
120-
}
148+
try await Task.sleep(nanoseconds: UInt64.random(in: 10...50) * 1_000_000)
149+
expectation.fulfill()
150+
return expectation.handler(request)
121151

122152
} else {
123-
queue.async {
124-
completionHandler(.failure(ServerError.unexpectedRequest(String(describing: operationType))))
125-
}
153+
throw ServerError.unexpectedRequest(String(describing: operationType))
154+
}
155+
}
156+
}
157+
158+
public struct MockGraphQLServerSession: ApolloURLSession {
159+
160+
nonisolated(unsafe) let server: MockGraphQLServer
161+
162+
init(server: MockGraphQLServer) {
163+
self.server = server
164+
}
165+
166+
public func chunks(for request: some GraphQLRequest) async throws -> (any AsyncChunkSequence, URLResponse) {
167+
let (stream, continuation) = MockAsyncChunkSequence.makeStream()
168+
do {
169+
let body = try await server.serve(request: request)
170+
let data = try JSONSerializationFormat.serialize(value: body)
171+
continuation.yield(data)
172+
continuation.finish()
173+
174+
} catch {
175+
continuation.finish(throwing: error)
126176
}
127177

178+
let httpResponse = HTTPURLResponse(
179+
url: TestURL.mockServer.url,
180+
statusCode: 200,
181+
httpVersion: nil,
182+
headerFields: nil
183+
)!
184+
return (stream, httpResponse)
185+
}
186+
187+
public func invalidateAndCancel() {
188+
}
189+
190+
}
191+
192+
193+
public struct MockAsyncChunkSequence: AsyncChunkSequence {
194+
public typealias UnderlyingStream = AsyncThrowingStream<Data, any Error>
195+
196+
public typealias AsyncIterator = UnderlyingStream.AsyncIterator
197+
198+
public typealias Element = Data
199+
200+
let underlying: UnderlyingStream
201+
202+
public func makeAsyncIterator() -> UnderlyingStream.AsyncIterator {
203+
underlying.makeAsyncIterator()
204+
}
205+
206+
public static func makeStream() -> (
207+
stream: MockAsyncChunkSequence,
208+
continuation: UnderlyingStream.Continuation
209+
) {
210+
let (s, c) = UnderlyingStream.makeStream(of: Data.self)
211+
return (Self.init(underlying: s), c)
128212
}
129213
}

Tests/ApolloInternalTestHelpers/MockHTTPRequest.swift

Lines changed: 0 additions & 15 deletions
This file was deleted.

Tests/ApolloInternalTestHelpers/MockHTTPResponse.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ extension HTTPURLResponse {
77
url: URL = TestURL.mockServer.url,
88
statusCode: Int = 200,
99
httpVersion: String? = nil,
10-
headerFields: [String : String]? = nil
10+
headerFields: [String: String]? = nil
1111
) -> HTTPURLResponse {
1212
return HTTPURLResponse(
1313
url: url,
@@ -16,4 +16,14 @@ extension HTTPURLResponse {
1616
headerFields: headerFields
1717
)!
1818
}
19+
20+
public static func deferResponseMock(
21+
url: URL = TestURL.mockServer.url,
22+
boundary: String = "graphql"
23+
) -> HTTPURLResponse {
24+
.mock(
25+
url: url,
26+
headerFields: ["Content-Type": "multipart/mixed;boundary=\(boundary);deferSpec=20220824"]
27+
)
28+
}
1929
}

0 commit comments

Comments
 (0)