diff --git a/Sources/Apollo/ApolloClient.swift b/Sources/Apollo/ApolloClient.swift index d3a6c71087..4b044e6385 100644 --- a/Sources/Apollo/ApolloClient.swift +++ b/Sources/Apollo/ApolloClient.swift @@ -18,12 +18,8 @@ public enum CachePolicy { /// A handler for operation results. /// /// - Parameters: -/// - result: The result of the performed operation, or `nil` if an error occurred. -/// - error: An error that indicates why the mutation failed, or `nil` if the mutation was succesful. -public typealias GraphQLResultHandler = (_ result: GraphQLResult?, _ error: Error?) -> Void - -@available(*, deprecated, renamed: "GraphQLResultHandler") -public typealias OperationResultHandler = GraphQLResultHandler +/// - result: The result of a performed operation. Will have a `GraphQLResult` with any parsed data and any GraphQL errors on `success`, and an `Error` on `failure`. +public typealias GraphQLResultHandler = (Result, Error>) -> Void /// The `ApolloClient` class provides the core API for Apollo. This API provides methods to fetch and watch queries, and to perform mutations. public class ApolloClient { @@ -103,7 +99,6 @@ public class ApolloClient { /// - queue: A dispatch queue on which the result handler will be called. Defaults to the main queue. /// - resultHandler: An optional closure that is called when query results are available or when an error occurs. /// - Returns: A query watcher object that can be used to control the watching behavior. - public func watch(query: Query, cachePolicy: CachePolicy = .returnCacheDataElseFetch, queue: DispatchQueue = DispatchQueue.main, resultHandler: @escaping GraphQLResultHandler) -> GraphQLQueryWatcher { let watcher = GraphQLQueryWatcher(client: self, query: query, resultHandler: wrapResultHandler(resultHandler, queue: queue)) watcher.fetch(cachePolicy: cachePolicy) @@ -135,34 +130,34 @@ public class ApolloClient { } fileprivate func send(operation: Operation, shouldPublishResultToStore: Bool, context: UnsafeMutableRawPointer?, resultHandler: @escaping GraphQLResultHandler) -> Cancellable { - return networkTransport.send(operation: operation) { (response, error) in - guard let response = response else { - resultHandler(nil, error) - return - } - - // If there is no need to publish the result to the store, we can use a fast path. - if !shouldPublishResultToStore { - do { - let result = try response.parseResultFast() - resultHandler(result, nil) - } catch { - resultHandler(nil, error) - } - return - } - - firstly { - try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) - }.andThen { (result, records) in - if let records = records { - self.store.publish(records: records, context: context).catch { error in - preconditionFailure(String(describing: error)) + return networkTransport.send(operation: operation) { result in + switch result { + case .failure(let error): + resultHandler(.failure(error)) + case .success(let response): + // If there is no need to publish the result to the store, we can use a fast path. + if !shouldPublishResultToStore { + do { + let result = try response.parseResultFast() + resultHandler(.success(result)) + } catch { + resultHandler(.failure(error)) } + return + } + + firstly { + try response.parseResult(cacheKeyForObject: self.cacheKeyForObject) + }.andThen { (result, records) in + if let records = records { + self.store.publish(records: records, context: context).catch { error in + preconditionFailure(String(describing: error)) + } + } + resultHandler(.success(result)) + }.catch { error in + resultHandler(.failure(error)) } - resultHandler(result, nil) - }.catch { error in - resultHandler(nil, error) } } } @@ -170,12 +165,12 @@ public class ApolloClient { private func wrapResultHandler(_ resultHandler: GraphQLResultHandler?, queue handlerQueue: DispatchQueue) -> GraphQLResultHandler { guard let resultHandler = resultHandler else { - return { _, _ in } + return { _ in } } - return { (result, error) in + return { result in handlerQueue.async { - resultHandler(result, error) + resultHandler(result) } } } @@ -210,25 +205,26 @@ private final class FetchQueryOperation: AsynchronousOperat return } - client.store.load(query: query) { (result, error) in - if error == nil { - self.resultHandler(result, nil) - - if self.cachePolicy != .returnCacheDataAndFetch { - self.state = .finished - return - } - } - + client.store.load(query: query) { result in if self.isCancelled { self.state = .finished return } - if self.cachePolicy == .returnCacheDataDontFetch { - self.resultHandler(nil, nil) - self.state = .finished - return + switch result { + case .success: + self.resultHandler(result) + + if self.cachePolicy != .returnCacheDataAndFetch { + self.state = .finished + return + } + case .failure: + if self.cachePolicy == .returnCacheDataDontFetch { + self.resultHandler(result) + self.state = .finished + return + } } self.fetchFromNetwork() @@ -236,8 +232,8 @@ private final class FetchQueryOperation: AsynchronousOperat } func fetchFromNetwork() { - networkTask = client.send(operation: query, shouldPublishResultToStore: true, context: context) { (result, error) in - self.resultHandler(result, error) + networkTask = client.send(operation: query, shouldPublishResultToStore: true, context: context) { result in + self.resultHandler(result) self.state = .finished return } diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index db7794d27e..eaa7c177e1 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -131,9 +131,9 @@ public final class ApolloStore { public func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { load(query: query).andThen { result in - resultHandler(result, nil) + resultHandler(.success(result)) }.catch { error in - resultHandler(nil, error) + resultHandler(.failure(error)) } } diff --git a/Sources/Apollo/GraphQLQueryWatcher.swift b/Sources/Apollo/GraphQLQueryWatcher.swift index 75183bc643..bf4e9fa91c 100644 --- a/Sources/Apollo/GraphQLQueryWatcher.swift +++ b/Sources/Apollo/GraphQLQueryWatcher.swift @@ -26,11 +26,17 @@ public final class GraphQLQueryWatcher: Cancellable, Apollo } func fetch(cachePolicy: CachePolicy) { - fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context) { [weak self] (result, error) in + fetching = client?.fetch(query: query, cachePolicy: cachePolicy, context: &context) { [weak self] result in guard let `self` = self else { return } - - self.dependentKeys = result?.dependentKeys - self.resultHandler(result, error) + + switch result { + case .success(let graphQLResult): + self.dependentKeys = graphQLResult.dependentKeys + case .failure: + break + } + + self.resultHandler(result) } } diff --git a/Sources/Apollo/HTTPNetworkTransport.swift b/Sources/Apollo/HTTPNetworkTransport.swift index d654f9fb95..790724d38d 100644 --- a/Sources/Apollo/HTTPNetworkTransport.swift +++ b/Sources/Apollo/HTTPNetworkTransport.swift @@ -68,7 +68,7 @@ public protocol HTTPNetworkTransportRetryDelegate: HTTPNetworkTransportDelegate // MARK: - /// A network transport that uses HTTP POST requests to send GraphQL operations to a server, and that uses `URLSession` as the networking implementation. -public class HTTPNetworkTransport: NetworkTransport { +public class HTTPNetworkTransport { let url: URL let session: URLSession let serializationFormat = JSONSerializationFormat.self @@ -96,18 +96,6 @@ public class HTTPNetworkTransport: NetworkTransport { self.delegate = delegate } - /// Send a GraphQL operation to a server and return a response. - /// - /// - Parameters: - /// - operation: The operation to send. - /// - completionHandler: A closure to call when a request completes. - /// - response: The response received from the server, or `nil` if an error occurred. - /// - error: An error that indicates why a request failed, or `nil` if the request was succesful. - /// - Returns: An object that can be used to cancel an in progress request. - public func send(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { - return send(operation: operation, files: nil, completionHandler: completionHandler) - } - /// Uploads the given files with the given operation. /// /// - Parameters: @@ -115,16 +103,16 @@ public class HTTPNetworkTransport: NetworkTransport { /// - files: An array of `GraphQLFile` objects to send. /// - completionHandler: The completion handler to execute when the request completes or errors /// - Returns: An object that can be used to cancel an in progress request. - public func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { + public func upload(operation: Operation, files: [GraphQLFile], completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { return send(operation: operation, files: files, completionHandler: completionHandler) } - private func send(operation: Operation, files: [GraphQLFile]?, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { + private func send(operation: Operation, files: [GraphQLFile]?, completionHandler: @escaping (_ results: Result, Error>) -> Void) -> Cancellable { let request: URLRequest do { request = try self.createRequest(for: operation, files: files) } catch { - completionHandler(nil, error) + completionHandler(.failure(error)) return EmptyCancellable() } @@ -176,7 +164,7 @@ public class HTTPNetworkTransport: NetworkTransport { throw GraphQLHTTPResponseError(body: data, response: httpResponse, kind: .invalidResponse) } let response = GraphQLResponse(operation: operation, body: body) - completionHandler(response, nil) + completionHandler(.success(response)) } catch let parsingError { self?.handleErrorOrRetry(operation: operation, error: parsingError, @@ -195,11 +183,11 @@ public class HTTPNetworkTransport: NetworkTransport { error: Error, for request: URLRequest, response: URLResponse?, - completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) { + completionHandler: @escaping (_ result: Result, Error>) -> Void) { guard let delegate = self.delegate, let retrier = delegate as? HTTPNetworkTransportRetryDelegate else { - completionHandler(nil, error) + completionHandler(.failure(error)) return } @@ -210,7 +198,7 @@ public class HTTPNetworkTransport: NetworkTransport { response: response, retryHandler: { [weak self] shouldRetry in guard shouldRetry else { - completionHandler(nil, error) + completionHandler(.failure(error)) return } @@ -284,3 +272,12 @@ public class HTTPNetworkTransport: NetworkTransport { return request } } + +// MARK: - NetworkTransport conformance + +extension HTTPNetworkTransport: NetworkTransport { + + public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { + return send(operation: operation, files: nil, completionHandler: completionHandler) + } +} diff --git a/Sources/Apollo/NetworkTransport.swift b/Sources/Apollo/NetworkTransport.swift index c0f7ccec43..0a608c9e42 100644 --- a/Sources/Apollo/NetworkTransport.swift +++ b/Sources/Apollo/NetworkTransport.swift @@ -1,12 +1,11 @@ /// A network transport is responsible for sending GraphQL operations to a server. public protocol NetworkTransport { + /// Send a GraphQL operation to a server and return a response. /// /// - Parameters: /// - operation: The operation to send. - /// - completionHandler: A closure to call when a request completes. - /// - response: The response received from the server, or `nil` if an error occurred. - /// - error: An error that indicates why a request failed, or `nil` if the request was succesful. + /// - completionHandler: A closure to call when a request completes. On `success` will contain the response received from the server. On `failure` will contain the error which occurred. /// - Returns: An object that can be used to cancel an in progress request. - func send(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable + func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable } diff --git a/Sources/ApolloWebSocket/SplitNetworkTransport.swift b/Sources/ApolloWebSocket/SplitNetworkTransport.swift index 95b0ca5c35..157c674bff 100644 --- a/Sources/ApolloWebSocket/SplitNetworkTransport.swift +++ b/Sources/ApolloWebSocket/SplitNetworkTransport.swift @@ -2,21 +2,31 @@ import Apollo #endif - -public class SplitNetworkTransport: NetworkTransport { +/// A network transport that sends subscriptions using one `NetworkTransport` and other requests using another `NetworkTransport`. Ideal for sending subscriptions via a web socket but everything else via HTTP. +public class SplitNetworkTransport { private let httpNetworkTransport: NetworkTransport private let webSocketNetworkTransport: NetworkTransport + /// Designated initializer + /// + /// - Parameters: + /// - httpNetworkTransport: A `NetworkTransport` to use for non-subscription requests. Should generally be a `HTTPNetworkTransport` or something similar. + /// - webSocketNetworkTransport: A `NetworkTransport` to use for subscription requests. Should generally be a `WebSocketTransport` or something similar. public init(httpNetworkTransport: NetworkTransport, webSocketNetworkTransport: NetworkTransport) { self.httpNetworkTransport = httpNetworkTransport self.webSocketNetworkTransport = webSocketNetworkTransport } +} + +// MARK: - NetworkTransport conformance + +extension SplitNetworkTransport: NetworkTransport { - public func send(operation: Operation, completionHandler: @escaping (GraphQLResponse?, Error?) -> Void) -> Cancellable { + public func send(operation: Operation, completionHandler: @escaping (Result, Error>) -> Void) -> Cancellable { if operation.operationType == .subscription { - return webSocketNetworkTransport.send(operation: operation, completionHandler: completionHandler) + return webSocketNetworkTransport.send(operation: operation, completionHandler: completionHandler) } else { - return httpNetworkTransport.send(operation: operation, completionHandler: completionHandler) + return httpNetworkTransport.send(operation: operation, completionHandler: completionHandler) } } } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index d7f2b99630..0b81e87b80 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -28,8 +28,8 @@ public extension WebSocketTransportDelegate { } /// A network transport that uses web sockets requests to send GraphQL subscription operations to a server, and that uses the Starscream implementation of web sockets. -public class WebSocketTransport: NetworkTransport, WebSocketDelegate { - public static var provider : ApolloWebSocketClient.Type = ApolloWebSocket.self +public class WebSocketTransport { + public static var provider: ApolloWebSocketClient.Type = ApolloWebSocket.self public weak var delegate: WebSocketTransportDelegate? var reconnect = false @@ -44,7 +44,7 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { private var queue: [Int: String] = [:] private var connectingPayload: GraphQLMap? - private var subscribers = [String: (JSONObject?, Error?) -> Void]() + private var subscribers = [String: (Result) -> Void]() private var subscriptions : [String: String] = [:] private let sendOperationIdentifiers: Bool @@ -62,23 +62,6 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { self.websocket.connect() } - public func send(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { - if let error = self.error { - completionHandler(nil,error) - return EmptyCancellable() - } - - return WebSocketTask(self,operation) { (body, error) in - if let body = body { - let response = GraphQLResponse(operation: operation, body: body) - completionHandler(response,error) - } else { - completionHandler(nil,error) - } - } - - } - public func isConnected() -> Bool { return websocket.isConnected } @@ -89,19 +72,35 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { private func processMessage(socket: WebSocketClient, text: String) { OperationMessage(serialized: text).parse { (type, id, payload, error) in - guard let type = type, let messageType = OperationMessage.Types(rawValue: type) else { - notifyErrorAllHandlers(WebSocketError(payload: payload, error: error, kind: .unprocessedMessage(text))) - return + guard + let type = type, + let messageType = OperationMessage.Types(rawValue: type) else { + self.notifyErrorAllHandlers(WebSocketError(payload: payload, error: error, kind: .unprocessedMessage(text))) + return } - switch(messageType) { - case .data, .error: - if let id = id, let responseHandler = subscribers[id] { - responseHandler(payload,error) + switch messageType { + case .data, + .error: + if + let id = id, + let responseHandler = subscribers[id] { + if let payload = payload { + responseHandler(.success(payload)) + } else if let error = error { + responseHandler(.failure(error)) + } else { + let websocketError = WebSocketError(payload: payload, + error: error, + kind: .neitherErrorNorPayloadReceived) + responseHandler(.failure(websocketError)) + } } else { - notifyErrorAllHandlers(WebSocketError(payload: payload, error: error, kind: .unprocessedMessage(text))) + let websocketError = WebSocketError(payload: payload, + error: error, + kind: .unprocessedMessage(text)) + self.notifyErrorAllHandlers(websocketError) } - case .complete: if let id = id { // remove the callback if NOT a subscription @@ -127,7 +126,7 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { private func notifyErrorAllHandlers(_ error: Error) { for (_, handler) in subscribers { - handler(nil,error) + handler(.failure(error)) } } @@ -147,52 +146,6 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { print("WebSocketTransport::unprocessed event \(data)") } - public func websocketDidConnect(socket: WebSocketClient) { - self.error = nil - initServer() - if reconnected { - self.delegate?.webSocketTransportDidReconnect(self) - // re-send the subscriptions whenever we are re-connected - // for the first connect, any subscriptions are already in queue - for (_,msg) in self.subscriptions { - write(msg) - } - } else { - self.delegate?.webSocketTransportDidConnect(self) - } - - reconnected = true - } - - public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { - // report any error to all subscribers - if let error = error { - self.error = WebSocketError(payload: nil, error: error, kind: .networkError) - for (_, responseHandler) in subscribers { - responseHandler(nil,error) - } - } else { - self.error = nil - } - - self.delegate?.webSocketTransport(self, didDisconnectWithError: self.error) - acked = false // need new connect and ack before sending - - if reconnect { - DispatchQueue.main.asyncAfter(deadline: .now() + reconnectionInterval) { - self.websocket.connect(); - } - } - } - - public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { - processMessage(socket: socket, text: text) - } - - public func websocketDidReceiveData(socket: WebSocketClient, data: Data) { - processMessage(socket: socket, data: data) - } - public func initServer(reconnect: Bool = true) { self.reconnect = reconnect self.acked = false @@ -238,7 +191,7 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { return sequenceNumber } - fileprivate func sendHelper(operation: Operation, resultHandler: @escaping (_ response: JSONObject?, _ error: Error?) -> Void) -> String? { + fileprivate func sendHelper(operation: Operation, resultHandler: @escaping (_ result: Result) -> Void) -> String? { let body = RequestCreator.requestBody(for: operation, sendOperationIdentifiers: self.sendOperationIdentifiers) let sequenceNumber = "\(nextSequenceNumber())" @@ -264,11 +217,11 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { subscriptions.removeValue(forKey: subscriptionId) } - fileprivate final class WebSocketTask : Cancellable { + fileprivate final class WebSocketTask: Cancellable { let sequenceNumber : String? let transport: WebSocketTransport - init(_ ws: WebSocketTransport, _ operation: Operation, _ completionHandler: @escaping (_ response: JSONObject?, _ error: Error?) -> Void) { + init(_ ws: WebSocketTransport, _ operation: Operation, _ completionHandler: @escaping (_ result: Result) -> Void) { sequenceNumber = ws.sendHelper(operation: operation, resultHandler: completionHandler) transport = ws } @@ -359,12 +312,83 @@ public class WebSocketTransport: NetworkTransport, WebSocketDelegate { } +// MARK: - HTTPNetworkTransport conformance + +extension WebSocketTransport: NetworkTransport { + public func send(operation: Operation, completionHandler: @escaping (_ result: Result,Error>) -> Void) -> Cancellable { + if let error = self.error { + completionHandler(.failure(error)) + return EmptyCancellable() + } + + return WebSocketTask(self, operation) { result in + switch result { + case .success(let jsonBody): + let response = GraphQLResponse(operation: operation, body: jsonBody) + completionHandler(.success(response)) + case .failure(let error): + completionHandler(.failure(error)) + } + } + } +} + +// MARK: - WebSocketDelegate implementation + +extension WebSocketTransport: WebSocketDelegate { + + public func websocketDidConnect(socket: WebSocketClient) { + self.error = nil + initServer() + if reconnected { + self.delegate?.webSocketTransportDidReconnect(self) + // re-send the subscriptions whenever we are re-connected + // for the first connect, any subscriptions are already in queue + for (_,msg) in self.subscriptions { + write(msg) + } + } else { + self.delegate?.webSocketTransportDidConnect(self) + } + + reconnected = true + } + + public func websocketDidDisconnect(socket: WebSocketClient, error: Error?) { + // report any error to all subscribers + if let error = error { + self.error = WebSocketError(payload: nil, error: error, kind: .networkError) + self.notifyErrorAllHandlers(error) + } else { + self.error = nil + } + + self.delegate?.webSocketTransport(self, didDisconnectWithError: self.error) + acked = false // need new connect and ack before sending + + if reconnect { + DispatchQueue.main.asyncAfter(deadline: .now() + reconnectionInterval) { + self.websocket.connect() + } + } + } + + public func websocketDidReceiveMessage(socket: WebSocketClient, text: String) { + processMessage(socket: socket, text: text) + } + + public func websocketDidReceiveData(socket: WebSocketClient, data: Data) { + processMessage(socket: socket, data: data) + } +} + public struct WebSocketError: Error, LocalizedError { public enum ErrorKind { case errorResponse case networkError case unprocessedMessage(String) case serializedMessageError + case neitherErrorNorPayloadReceived var description: String { switch self { @@ -376,6 +400,8 @@ public struct WebSocketError: Error, LocalizedError { return "Websocket error: Unprocessed message \(message)" case .serializedMessageError: return "Websocket error: Serialized message not found" + case .neitherErrorNorPayloadReceived: + return "Websocket error: Did not receive an error or a payload." } } } diff --git a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift index 343b69e1f6..ff4c4a037d 100644 --- a/Tests/ApolloCacheDependentTests/FetchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/FetchQueryTests.swift @@ -31,12 +31,15 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { (result, error) in + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { result in defer { expectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") + switch result { + case .success(let queryResult): + XCTAssertEqual(queryResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Error: \(error)") + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -70,14 +73,20 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataAndFetch) { (result, error) in - // ignore first result assuming from cache, and then make sure we get fetched result - if result?.data?.hero?.name != "R2-D2" { - defer { expectation.fulfill() } - - guard let result = result else { XCTFail("No query result"); return } - - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") + client.fetch(query: query, cachePolicy: .returnCacheDataAndFetch) { result in + + switch result { + case .success(let queryResult): + if queryResult.data?.hero?.name == "R2-D2" { + // ignore first result assuming from cache, and wait for second callback with fetched result + return + } else { + XCTAssertEqual(queryResult.data?.hero?.name, "Luke Skywalker") + expectation.fulfill() + } + case .failure(let error): + XCTFail("Error: \(error)") + expectation.fulfill() } } @@ -112,12 +121,15 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataElseFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataElseFetch) { result in defer { expectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - - XCTAssertEqual(result.data?.hero?.name, "R2-D2") + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -150,12 +162,15 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataElseFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataElseFetch) { result in defer { expectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -189,12 +204,15 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { result in defer { expectation.fulfill() } - - guard let result = result else { XCTFail("No query result"); return } - - XCTAssertEqual(result.data?.hero?.name, "R2-D2") + + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -228,10 +246,15 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { result in defer { expectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - XCTAssertEqual(result.data?.hero?.name, "R2-D2") + + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -241,10 +264,24 @@ class FetchQueryTests: XCTestCase { let expectation2 = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { result in defer { expectation2.fulfill() } - XCTAssertNil(result) - XCTAssertNil(error) + switch result { + case .success: + XCTFail("This should have returned an error") + case .failure(let error): + if let resultError = error as? JSONDecodingError { + switch resultError { + case .missingValue: + // Correct error! + break + default: + XCTFail("Unexpected JSON error: \(error)") + } + } else { + XCTFail("Unexpected error: \(error)") + } + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -277,11 +314,26 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { result in defer { expectation.fulfill() } - - XCTAssertNil(error) - XCTAssertNil(result) + switch result { + case .success: + XCTFail("This should have returned an error!") + case .failure(let error): + if + let resultError = error as? GraphQLResultError, + let underlyingError = resultError.underlying as? JSONDecodingError { + switch underlyingError { + case .missingValue: + // Correct error! + break + default: + XCTFail("Unexpected JSON error: \(error)") + } + } else { + XCTFail("Unexpected error: \(error)") + } + } } self.waitForExpectations(timeout: 5, handler: nil) @@ -311,7 +363,7 @@ class FetchQueryTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData, queue: queue) { (result, error) in + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData, queue: queue) { _ in defer { expectation.fulfill() } XCTAssertNotNil(DispatchQueue.getSpecific(key: key)) diff --git a/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift b/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift index 6690056ea9..b9f516376f 100644 --- a/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift +++ b/Tests/ApolloCacheDependentTests/LoadQueryFromStoreTests.swift @@ -17,10 +17,14 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroNameQuery() - load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - XCTAssertEqual(result?.data?.hero?.name, "R2-D2") + load(query: query) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } } @@ -36,10 +40,14 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroNameQuery(episode: .jedi) - load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - XCTAssertEqual(result?.data?.hero?.name, "R2-D2") + load(query: query) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } } @@ -55,14 +63,17 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroNameQuery() - load(query: query) { (result, error) in - XCTAssertNil(result) - - if case let error as GraphQLResultError = error { - XCTAssertEqual(error.path, ["hero", "name"]) - XCTAssertMatch(error.underlying, JSONDecodingError.missingValue) - } else { - XCTFail("Unexpected error: \(String(describing: error))") + load(query: query) { result in + switch result { + case .success: + XCTFail("This should not have succeeded!") + case .failure(let error): + if let graphQLError = error as? GraphQLResultError { + XCTAssertEqual(graphQLError.path, ["hero", "name"]) + XCTAssertMatch(graphQLError.underlying, JSONDecodingError.missingValue) + } else { + XCTFail("Unexpected error: \(error)") + } } } } @@ -79,14 +90,17 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroNameQuery() - load(query: query) { (result, error) in - XCTAssertNil(result) - - if case let error as GraphQLResultError = error { - XCTAssertEqual(error.path, ["hero", "name"]) - XCTAssertMatch(error.underlying, JSONDecodingError.nullValue) - } else { - XCTFail("Unexpected error: \(String(describing: error))") + load(query: query) { result in + switch result { + case .success: + XCTFail("This should not have succeeded!") + case .failure(let error): + if let graphQLError = error as? GraphQLResultError { + XCTAssertEqual(graphQLError.path, ["hero", "name"]) + XCTAssertMatch(graphQLError.underlying, JSONDecodingError.nullValue) + } else { + XCTFail("Unexpected error: \(error)") + } } } } @@ -114,14 +128,26 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroAndFriendsNamesQuery(episode: .jedi) - load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + load(query: query) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data returned with result") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } } @@ -135,7 +161,7 @@ class LoadQueryFromStoreTests: XCTestCase { "friends": [ Reference(key: "1000"), Reference(key: "1002"), - Reference(key: "1003") + Reference(key: "1003"), ] ], "1000": ["__typename": "Human", "name": "Luke Skywalker"], @@ -148,14 +174,26 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroAndFriendsNamesQuery() - load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + load(query: query) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data in result!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } } @@ -175,13 +213,21 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroAndFriendsNamesQuery() - load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - XCTAssertNil(data.hero?.friends) + load(query: query) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data in result!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + XCTAssertNil(data.hero?.friends) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } } @@ -197,14 +243,17 @@ class LoadQueryFromStoreTests: XCTestCase { let query = HeroAndFriendsNamesQuery() - load(query: query) { (result, error) in - XCTAssertNil(result) - - if case let error as GraphQLResultError = error { - XCTAssertEqual(error.path, ["hero", "friends"]) - XCTAssertMatch(error.underlying, JSONDecodingError.missingValue) - } else { - XCTFail("Unexpected error: \(String(describing: error))") + load(query: query) { result in + switch result { + case .success: + XCTFail("This should not have succeeded!") + case .failure(let error): + if let graphQLError = error as? GraphQLResultError { + XCTAssertEqual(graphQLError.path, ["hero", "friends"]) + XCTAssertMatch(graphQLError.underlying, JSONDecodingError.missingValue) + } else { + XCTFail("Unexpected error: \(String(describing: error))") + } } } } @@ -215,8 +264,8 @@ class LoadQueryFromStoreTests: XCTestCase { private func load(query: Query, resultHandler: @escaping GraphQLResultHandler) { let expectation = self.expectation(description: "Loading query from store") - store.load(query: query) { (result, error) in - resultHandler(result, error) + store.load(query: query) { result in + resultHandler(result) expectation.fulfill() } diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift index 56d80d7bbb..61187dca6c 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerCachingRoundtripTests.swift @@ -46,24 +46,36 @@ class StarWarsServerCachingRoundtripTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query) { (result, error) in - if let error = error { XCTFail("Error while fetching query: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No query result"); return } - - if let errors = result.errors { - XCTFail("Errors in query result: \(errors)") - } - - guard result.data != nil else { XCTFail("No query result data"); return } - - client.store.load(query: query) { (result, error) in - defer { expectation.fulfill() } - - if let error = error { XCTFail("Error while loading query from store: \(error.localizedDescription)"); return } - - guard let data = result?.data else { XCTFail("No query result data"); return } - - completionHandler(data) + client.fetch(query: query) { outerResult in + switch outerResult { + case .failure(let error): + XCTFail("Unexpected error with fetch: \(error)") + expectation.fulfill() + return + case .success(let fetchGraphQLResult): + XCTAssertNil(fetchGraphQLResult.errors) + + guard fetchGraphQLResult.data != nil else { + XCTFail("No query result data from fetching!") + expectation.fulfill() + return + } + + client.store.load(query: query) { innerResult in + defer { expectation.fulfill() } + + switch innerResult { + case .success(let loadGraphQLResult): + guard let data = loadGraphQLResult.data else { + XCTFail("No query result data from loading!") + return + } + + completionHandler(data) + case .failure(let error): + XCTFail("Error while loading query from store: \(error.localizedDescription)") + } + } } } diff --git a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift index df847ef0ff..3ce83067a4 100644 --- a/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift +++ b/Tests/ApolloCacheDependentTests/StarWarsServerTests.swift @@ -272,19 +272,21 @@ class StarWarsServerTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query) { (result, error) in + client.fetch(query: query) { result in defer { expectation.fulfill() } - - if let error = error { XCTFail("Error while fetching query: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No query result"); return } - - if let errors = result.errors { - XCTFail("Errors in query result: \(errors)") + + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No query result data") + return + } + + completionHandler(data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") } - - guard let data = result.data else { XCTFail("No query result data"); return } - - completionHandler(data) } waitForExpectations(timeout: 5, handler: nil) @@ -299,19 +301,22 @@ class StarWarsServerTests: XCTestCase { let expectation = self.expectation(description: "Performing mutation") - client.perform(mutation: mutation) { (result, error) in + client.perform(mutation: mutation) { result in defer { expectation.fulfill() } - - if let error = error { XCTFail("Error while performing mutation: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No mutation result"); return } - - if let errors = result.errors { - XCTFail("Errors in mutation result: \(errors)") + + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No mutation result data") + return + } + + completionHandler(data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") } - - guard let data = result.data else { XCTFail("No mutation result data"); return } - - completionHandler(data) } waitForExpectations(timeout: 5, handler: nil) diff --git a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift index 97abe6832c..d3e8c1fbe6 100644 --- a/Tests/ApolloCacheDependentTests/WatchQueryTests.swift +++ b/Tests/ApolloCacheDependentTests/WatchQueryTests.swift @@ -30,27 +30,33 @@ class WatchQueryTests: XCTestCase { var verifyResult: GraphQLResultHandler - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - XCTAssertEqual(result?.data?.hero?.name, "R2-D2") + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpexcted error: \(error)") + } } var expectation = self.expectation(description: "Fetching query") - let watcher = client.watch(query: query) { (result, error) in - verifyResult(result, error) + let watcher = client.watch(query: query) { result in + verifyResult(result) expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - XCTAssertEqual(result?.data?.hero?.name, "Artoo") + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "Artoo") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } expectation = self.expectation(description: "Refetching query") @@ -94,33 +100,50 @@ class WatchQueryTests: XCTestCase { var verifyResult: GraphQLResultHandler - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No data in graphQLResult!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } var expectation = self.expectation(description: "Fetching query") - _ = client.watch(query: query) { (result, error) in - verifyResult(result, error) + _ = client.watch(query: query) { result in + verifyResult(result) expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "Artoo") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No data in GraphQL result!") + return + } + XCTAssertEqual(data.hero?.name, "Artoo") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } expectation = self.expectation(description: "Updated after fetching other query") @@ -165,23 +188,37 @@ class WatchQueryTests: XCTestCase { let fetching = self.expectation(description: "Fetching query") var refetching: XCTestExpectation? - let _ = client.watch(query: query) { (result, error) in + let _ = client.watch(query: query) { result in guard refetching == nil else { return refetching!.fulfill() } - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { return XCTFail() } - - XCTAssertEqual(data.hero?.name, "R2-D2") - - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + defer { + fetching.fulfill() + } - fetching.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data on graphQL result!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } wait(for: [fetching], timeout: 5) @@ -222,27 +259,33 @@ class WatchQueryTests: XCTestCase { var verifyResult: GraphQLResultHandler - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - XCTAssertEqual(result?.data?.hero?.name, "R2-D2") + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "R2-D2") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } var expectation = self.expectation(description: "Fetching query") - _ = client.watch(query: query) { (result, error) in - verifyResult(result, error) + _ = client.watch(query: query) { result in + verifyResult(result) expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - XCTAssertEqual(result?.data?.hero?.name, "Luke Skywalker") + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } expectation = self.expectation(description: "Fetching other query") @@ -278,20 +321,32 @@ class WatchQueryTests: XCTestCase { var verifyResult: GraphQLResultHandler - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data returned with GraphQL result!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } var expectation = self.expectation(description: "Fetching query") - _ = client.watch(query: query) { (result, error) in - verifyResult(result, error) + _ = client.watch(query: query) { result in + verifyResult(result) expectation.fulfill() } @@ -304,14 +359,25 @@ class WatchQueryTests: XCTestCase { } }) - verifyResult = { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "Artoo") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + verifyResult = { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("GraphqlResult had no data!") + return + } + + XCTAssertEqual(data.hero?.name, "Artoo") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, [ + "Luke Skywalker", + "Han Solo", + "Leia Organa", + ]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } expectation = self.expectation(description: "Updated after fetching other query") diff --git a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift index 1db2326cfb..ab6ab2aa18 100644 --- a/Tests/ApolloSQLiteTests/CachePersistenceTests.swift +++ b/Tests/ApolloSQLiteTests/CachePersistenceTests.swift @@ -26,25 +26,35 @@ class CachePersistenceTests: XCTestCase { let networkExpectation = self.expectation(description: "Fetching query from network") let newCacheExpectation = self.expectation(description: "Fetch query from new cache") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { (result, error) in + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in defer { networkExpectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") - - // Do another fetch from cache to ensure that data is cached before creating new cache - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in - SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { (cache) in - let newStore = ApolloStore(cache: cache) - let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) - newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in - defer { newCacheExpectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") - _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run + + switch outerResult { + case .failure(let error): + XCTFail("Unexpected error: \(error)") + return + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + // Do another fetch from cache to ensure that data is cached before creating new cache + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in + SQLiteTestCacheProvider.withCache(fileURL: sqliteFileURL) { cache in + let newStore = ApolloStore(cache: cache) + let newClient = ApolloClient(networkTransport: networkTransport, store: newStore) + newClient.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { newClientResult in + defer { newCacheExpectation.fulfill() } + switch newClientResult { + case .success(let newClientGraphQLResult): + XCTAssertEqual(newClientGraphQLResult.data?.hero?.name, "Luke Skywalker") + case .failure(let error): + XCTFail("Unexpected error with new client: \(error)") + } + _ = newClient // Workaround for a bug - ensure that newClient is retained until this block is run + } } } } } + self.waitForExpectations(timeout: 2, handler: nil) } } @@ -68,18 +78,43 @@ class CachePersistenceTests: XCTestCase { let networkExpectation = self.expectation(description: "Fetching query from network") let emptyCacheExpectation = self.expectation(description: "Fetch query from empty cache") - client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { (result, error) in + client.fetch(query: query, cachePolicy: .fetchIgnoringCacheData) { outerResult in defer { networkExpectation.fulfill() } - guard let result = result else { XCTFail("No query result"); return } - XCTAssertEqual(result.data?.hero?.name, "Luke Skywalker") + + switch outerResult { + case .failure(let error): + XCTFail("Unexpected faillure: \(error)") + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.hero?.name, "Luke Skywalker") + } - do { try client.clearCache().await() } - catch { XCTFail() } + do { + try client.clearCache().await() + } catch { + XCTFail("Error Clearing cache: \(error)") + emptyCacheExpectation.fulfill() + return + } - client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { (result, error) in + client.fetch(query: query, cachePolicy: .returnCacheDataDontFetch) { innerResult in defer { emptyCacheExpectation.fulfill() } - XCTAssertNil(result) - XCTAssertNil(error) + + switch innerResult { + case .success: + XCTFail("This should have returned an error") + case .failure(let error): + if let resultError = error as? JSONDecodingError { + switch resultError { + case .missingValue: + // Correct error! + break + default: + XCTFail("Unexpected JSON error: \(error)") + } + } else { + XCTFail("Unexpected error: \(error)") + } + } } } diff --git a/Tests/ApolloTestSupport/MockNetworkTransport.swift b/Tests/ApolloTestSupport/MockNetworkTransport.swift index c006d6441a..5893615b51 100644 --- a/Tests/ApolloTestSupport/MockNetworkTransport.swift +++ b/Tests/ApolloTestSupport/MockNetworkTransport.swift @@ -8,9 +8,9 @@ public final class MockNetworkTransport: NetworkTransport { self.body = body } - public func send(operation: Operation, completionHandler: @escaping (_ response: GraphQLResponse?, _ error: Error?) -> Void) -> Cancellable { + public func send(operation: Operation, completionHandler: @escaping (_ result: Result, Error>) -> Void) -> Cancellable { DispatchQueue.global(qos: .default).async { - completionHandler(GraphQLResponse(operation: operation, body: self.body), nil) + completionHandler(.success(GraphQLResponse(operation: operation, body: self.body))) } return MockTask() } diff --git a/Tests/ApolloTests/BatchedLoadTests.swift b/Tests/ApolloTests/BatchedLoadTests.swift index f2c02097c5..cfca777706 100644 --- a/Tests/ApolloTests/BatchedLoadTests.swift +++ b/Tests/ApolloTests/BatchedLoadTests.swift @@ -64,15 +64,25 @@ class BatchedLoadTests: XCTestCase { let expectation = self.expectation(description: "Loading query from store") - store.load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) - - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - XCTAssertEqual(data.hero?.friends?.count, 100) + store.load(query: query) { result in + defer { + expectation.fulfill() + } - expectation.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data returned with result!") + return + } + + XCTAssertEqual(data.hero?.name, "R2-D2") + XCTAssertEqual(data.hero?.friends?.count, 100) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } self.waitForExpectations(timeout: 1) @@ -105,16 +115,26 @@ class BatchedLoadTests: XCTestCase { (1...10).forEach { number in let expectation = self.expectation(description: "Loading query #\(number) from store") - store.load(query: query) { (result, error) in - XCTAssertNil(error) - XCTAssertNil(result?.errors) + store.load(query: query) { result in + defer { + expectation.fulfill() + } - guard let data = result?.data else { XCTFail(); return } - XCTAssertEqual(data.hero?.name, "R2-D2") - let friendsNames = data.hero?.friends?.compactMap { $0?.name } - XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) - - expectation.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + + guard let data = graphQLResult.data else { + XCTFail("No data returned with query!") + return + + } + XCTAssertEqual(data.hero?.name, "R2-D2") + let friendsNames = data.hero?.friends?.compactMap { $0?.name } + XCTAssertEqual(friendsNames, ["Luke Skywalker", "Han Solo", "Leia Organa"]) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } } diff --git a/Tests/ApolloTests/HTTPTransportTests.swift b/Tests/ApolloTests/HTTPTransportTests.swift index 3efb67ed7d..c3e3bb2d08 100644 --- a/Tests/ApolloTests/HTTPTransportTests.swift +++ b/Tests/ApolloTests/HTTPTransportTests.swift @@ -28,8 +28,7 @@ class HTTPTransportTests: XCTestCase { useGETForQueries: true, delegate: self) - private func validateHeroNameQueryResponse(response: GraphQLResponse?, - error: Error?, + private func validateHeroNameQueryResponse(result: Result, Error>, expectation: XCTestExpectation, file: StaticString = #file, line: UInt = #line) { @@ -37,58 +36,51 @@ class HTTPTransportTests: XCTestCase { expectation.fulfill() } - if let responseError = error as? GraphQLHTTPResponseError { - XCTFail("Unexpected response error: \(responseError.bodyDescription)", + switch result { + case .success(let graphQLResponse): + guard + let dictionary = graphQLResponse.body as? [String: AnyHashable], + let dataDict = dictionary["data"] as? [String: AnyHashable], + let heroDict = dataDict["hero"] as? [String: AnyHashable], + let name = heroDict["name"] as? String else { + XCTFail("No hero for you!", + file: file, + line: line) + return + } + + XCTAssertEqual(name, + "R2-D2", + file: file, + line: line) + case .failure(let error): + XCTFail("Unexpected response error: \(error)", file: file, line: line) - return - } - - guard let queryResponse = response else { - XCTFail("No response!", - file: file, - line: line) - return - } - - guard - let dictionary = queryResponse.body as? [String: AnyHashable], - let dataDict = dictionary["data"] as? [String: AnyHashable], - let heroDict = dataDict["hero"] as? [String: AnyHashable], - let name = heroDict["name"] as? String else { - XCTFail("No hero for you!", - file: file, - line: line) - return } - - XCTAssertEqual(name, - "R2-D2", - file: file, - line: line) } func testPreflightDelegateTellingRequestNotToSend() { self.shouldSend = false let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire)) { response, error in + let cancellable = self.networkTransport.send(operation: HeroNameQuery(episode: .empire)) { result in defer { expectation.fulfill() } - guard let error = error else { + switch result { + case .success: XCTFail("Expected error not received when telling delegate not to send!") - return - } - - switch error { - case GraphQLHTTPRequestError.cancelledByDelegate: - // Correct! - break - default: - XCTFail("Expected `cancelledByDeveloper`, got \(error)") + case .failure(let error): + switch error { + case GraphQLHTTPRequestError.cancelledByDelegate: + // Correct! + break + default: + XCTFail("Expected `cancelledByDelegate`, got \(error)") + } } } @@ -114,8 +106,8 @@ class HTTPTransportTests: XCTestCase { self.updatedHeaders = ["Authorization": "Bearer HelloApollo"] let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { response, error in - self.validateHeroNameQueryResponse(response: response, error: error, expectation: expectation) + let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in + self.validateHeroNameQueryResponse(result: result, expectation: expectation) } guard @@ -141,8 +133,8 @@ class HTTPTransportTests: XCTestCase { func testPreflightDelegateNeitherModifyingOrStoppingRequest() { let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { response, error in - self.validateHeroNameQueryResponse(response: response, error: error, expectation: expectation) + let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in + self.validateHeroNameQueryResponse(result: result, expectation: expectation) } guard @@ -171,10 +163,10 @@ class HTTPTransportTests: XCTestCase { self.shouldModifyURLInWillSend = true let expectation = self.expectation(description: "Send operation completed") - let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { response, error in + let cancellable = self.networkTransport.send(operation: HeroNameQuery()) { result in // This should have retried twice - the first time `shouldModifyURLInWillSend` shoud remain the same and it'll fail again. XCTAssertEqual(self.retryCount, 2) - self.validateHeroNameQueryResponse(response: response, error: error, expectation: expectation) + self.validateHeroNameQueryResponse(result: result, expectation: expectation) } guard diff --git a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift index ef3ad4c769..9708515422 100644 --- a/Tests/ApolloWebsocketTests/MockWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/MockWebSocketTests.swift @@ -33,14 +33,14 @@ class MockWebSocketTests: XCTestCase { func testLocalSingleSubscription() throws { let expectation = self.expectation(description: "Single subscription") - client.subscribe(subscription: ReviewAddedSubscription()) { (result, error) in + client.subscribe(subscription: ReviewAddedSubscription()) { result in defer { expectation.fulfill() } - - if error != nil { XCTFail("Error response"); return } - - guard let result = result else { XCTFail("No subscription result"); return } - - XCTAssertEqual(result.data?.reviewAdded?.stars, 5) + switch result { + case .success(let graphQLResult): + XCTAssertEqual(graphQLResult.data?.reviewAdded?.stars, 5) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } let message : GraphQLMap = [ @@ -67,7 +67,7 @@ class MockWebSocketTests: XCTestCase { let expectation = self.expectation(description: "Missing subscription") expectation.isInverted = true - client.subscribe(subscription: ReviewAddedSubscription()) { (result, error) in + client.subscribe(subscription: ReviewAddedSubscription()) { _ in expectation.fulfill() } @@ -77,12 +77,25 @@ class MockWebSocketTests: XCTestCase { func testLocalErrorUnknownId() throws { let expectation = self.expectation(description: "Unknown id for subscription") - client.subscribe(subscription: ReviewAddedSubscription()) { (result, error) in + client.subscribe(subscription: ReviewAddedSubscription()) { result in defer { expectation.fulfill() } - // Expecting error and no result - XCTAssertNil(result) - XCTAssertNotNil(error) + switch result { + case .success: + XCTFail("This should have caused an error!") + case .failure(let error): + if let webSocketError = error as? WebSocketError { + switch webSocketError.kind { + case .unprocessedMessage: + // Correct! + break + default: + XCTFail("Unexpected websocket error: \(error)") + } + } else { + XCTFail("Unexpected error: \(error)") + } + } } let message : GraphQLMap = [ diff --git a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift index b5990648fe..7c30775f02 100644 --- a/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsSubscriptionTests.swift @@ -21,14 +21,25 @@ class StarWarsSubscriptionTests: XCTestCase { func testSubscribeReviewJediEpisode() { let expectation = self.expectation(description: "Subscribe single review") - let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { (result, error) in - guard let data = result?.data else { XCTFail("No subscription result data"); return } - - XCTAssertEqual(data.reviewAdded?.episode, .jedi) - XCTAssertEqual(data.reviewAdded?.stars, 6) - XCTAssertEqual(data.reviewAdded?.commentary, "This is the greatest movie!") + let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { result in + defer { + expectation.fulfill() + } - expectation.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No subscription result data") + return + } + + XCTAssertEqual(data.reviewAdded?.episode, .jedi) + XCTAssertEqual(data.reviewAdded?.stars, 6) + XCTAssertEqual(data.reviewAdded?.commentary, "This is the greatest movie!") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } client.perform(mutation: CreateReviewForEpisodeMutation(episode: .jedi, review: ReviewInput(stars: 6, commentary: "This is the greatest movie!"))) @@ -40,13 +51,24 @@ class StarWarsSubscriptionTests: XCTestCase { func testSubscribeReviewAnyEpisode() { let expectation = self.expectation(description: "Subscribe any episode") - let sub = client.subscribe(subscription: ReviewAddedSubscription()) { (result, error) in - guard let data = result?.data else { XCTFail("No subscription result data"); return } - - XCTAssertEqual(data.reviewAdded?.stars, 13) - XCTAssertEqual(data.reviewAdded?.commentary, "This is an even greater movie!") + let sub = client.subscribe(subscription: ReviewAddedSubscription()) { result in + defer { + expectation.fulfill() + } - expectation.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No subscription result data") + return + } + + XCTAssertEqual(data.reviewAdded?.stars, 13) + XCTAssertEqual(data.reviewAdded?.commentary, "This is an even greater movie!") + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } client.perform(mutation: CreateReviewForEpisodeMutation(episode: .empire, review: ReviewInput(stars: 13, commentary: "This is an even greater movie!"))) @@ -59,12 +81,23 @@ class StarWarsSubscriptionTests: XCTestCase { let expectation = self.expectation(description: "Subscription to specific episode - expecting timeout") expectation.isInverted = true - let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { (result, error) in - guard let data = result?.data else { XCTFail("No subscription result data"); return } - - XCTAssertNotEqual(data.reviewAdded?.episode, .jedi) + let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { result in + defer { + expectation.fulfill() + } - expectation.fulfill() + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No subscription result data") + return + } + + XCTAssertNotEqual(data.reviewAdded?.episode, .jedi) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } } client.perform(mutation: CreateReviewForEpisodeMutation(episode: .empire, review: ReviewInput(stars: 10, commentary: "This is an even greater movie!"))) @@ -77,7 +110,7 @@ class StarWarsSubscriptionTests: XCTestCase { let expectation = self.expectation(description: "Subscription then cancel - expecting timeput") expectation.isInverted = true - let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { (result, error) in + let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { _ in XCTFail("Received subscription after cancel") } @@ -93,18 +126,23 @@ class StarWarsSubscriptionTests: XCTestCase { let expectation = self.expectation(description: "Multiple reviews") expectation.expectedFulfillmentCount = count - let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .empire)) { (result, error) in - if let error = error { XCTFail("Error while performing subscription: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No subscription result"); return } - - if let errors = result.errors { - XCTFail("Errors in subscription result: \(errors)") + let sub = client.subscribe(subscription: ReviewAddedSubscription(episode: .empire)) { result in + defer { + expectation.fulfill() + } + + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No subscription result data") + return + } + + XCTAssertEqual(data.reviewAdded?.episode, .empire) + case .failure(let error): + XCTFail("Unexpected error: \(error)") } - - guard let data = result.data else { XCTFail("No subscription result data"); return } - - XCTAssertEqual(data.reviewAdded?.episode, .empire) - expectation.fulfill() } for i in 1...count { @@ -123,25 +161,53 @@ class StarWarsSubscriptionTests: XCTestCase { let count = 20 let expectation = self.expectation(description: "Multiple reviews") - expectation.expectedFulfillmentCount = count*2 - - let subAll = client.subscribe(subscription: ReviewAddedSubscription()) { (result, error) in - guard let _ = result?.data else { XCTFail("No subscription result data"); return } + expectation.expectedFulfillmentCount = count * 2 + + let subAll = client.subscribe(subscription: ReviewAddedSubscription()) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertNotNil(graphQLResult.data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - let subEmpire = client.subscribe(subscription: ReviewAddedSubscription(episode: .empire)) { (result, error) in - guard let _ = result?.data else { XCTFail("No subscription result data"); return } + let subEmpire = client.subscribe(subscription: ReviewAddedSubscription(episode: .empire)) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertNotNil(graphQLResult.data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - let subJedi = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { (result, error) in - guard let _ = result?.data else { XCTFail("No subscription result data"); return } + let subJedi = client.subscribe(subscription: ReviewAddedSubscription(episode: .jedi)) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertNotNil(graphQLResult.data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } - let subNewHope = client.subscribe(subscription: ReviewAddedSubscription(episode: .newhope)) { (result, error) in - guard let _ = result?.data else { XCTFail("No subscription result data"); return } + let subNewHope = client.subscribe(subscription: ReviewAddedSubscription(episode: .newhope)) { result in + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + XCTAssertNotNil(graphQLResult.data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") + } + expectation.fulfill() } diff --git a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift index 60fa32ec53..0a6aa720b0 100755 --- a/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift +++ b/Tests/ApolloWebsocketTests/StarWarsWebSocketTests.swift @@ -6,7 +6,7 @@ import StarWarsAPI // import StarWarsAPI -class StarWarsWebsSocketTests: XCTestCase { +class StarWarsWebSocketTests: XCTestCase { let SERVER = "http://localhost:8080/websocket" // MARK: Queries @@ -277,19 +277,21 @@ class StarWarsWebsSocketTests: XCTestCase { let expectation = self.expectation(description: "Fetching query") - client.fetch(query: query) { (result, error) in + client.fetch(query: query) { result in defer { expectation.fulfill() } - if let error = error { XCTFail("Error while fetching query: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No query result"); return } - - if let errors = result.errors { - XCTFail("Errors in query result: \(errors)") + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No query result data") + return + } + + completionHandler(data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") } - - guard let data = result.data else { XCTFail("No query result data"); return } - - completionHandler(data) } waitForExpectations(timeout: 5, handler: nil) @@ -304,19 +306,21 @@ class StarWarsWebsSocketTests: XCTestCase { let expectation = self.expectation(description: "Performing mutation") - client.perform(mutation: mutation) { (result, error) in + client.perform(mutation: mutation) { result in defer { expectation.fulfill() } - - if let error = error { XCTFail("Error while performing mutation: \(error.localizedDescription)"); return } - guard let result = result else { XCTFail("No mutation result"); return } - - if let errors = result.errors { - XCTFail("Errors in mutation result: \(errors)") + + switch result { + case .success(let graphQLResult): + XCTAssertNil(graphQLResult.errors) + guard let data = graphQLResult.data else { + XCTFail("No mutation result data") + return + } + + completionHandler(data) + case .failure(let error): + XCTFail("Unexpected error: \(error)") } - - guard let data = result.data else { XCTFail("No mutation result data"); return } - - completionHandler(data) } waitForExpectations(timeout: 5, handler: nil)