diff --git a/Sources/CheckoutNetwork/CheckoutClientInterface.swift b/Sources/CheckoutNetwork/CheckoutClientInterface.swift index 9227fd3..08e08a2 100644 --- a/Sources/CheckoutNetwork/CheckoutClientInterface.swift +++ b/Sources/CheckoutNetwork/CheckoutClientInterface.swift @@ -10,21 +10,31 @@ import Foundation /// Interface for a network client that can support Checkout networking requirements public protocol CheckoutClientInterface { + // MARK: Type Assignments + /// Completion handler that will return a result containing a decodable object or an error typealias CompletionHandler = ((Result) -> Void) /// Completion handler that will return errors if it fails or nothing if it completes as expected typealias NoDataResponseCompletionHandler = ((Error?) -> Void) - + + // MARK: Traditional Base Methods + /// Create, customise and run a request with the given configuration, calling the completion handler once completed func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping CompletionHandler) - /// Async wrapper of func runRequest(_:_:) with CompletionHandler - func runRequest(with configuration: RequestConfiguration) async throws -> T - /// Create, customise and run a request with the given configuration, calling the completion handler once completed func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping NoDataResponseCompletionHandler) + + + // MARK: Async Wrappers + + /// Async wrapper of func runRequest(_:_:) with CompletionHandler + func runRequest(with configuration: RequestConfiguration) async throws -> T + + /// Async wrapper of func runRequest(_:_:) with NoDataResponseCompletionHandler + func runRequest(with configuration: RequestConfiguration) async throws } diff --git a/Sources/CheckoutNetwork/CheckoutNetworkClient.swift b/Sources/CheckoutNetwork/CheckoutNetworkClient.swift index efda55f..952679d 100644 --- a/Sources/CheckoutNetwork/CheckoutNetworkClient.swift +++ b/Sources/CheckoutNetwork/CheckoutNetworkClient.swift @@ -58,19 +58,6 @@ public class CheckoutNetworkClient: CheckoutClientInterface { } } - public func runRequest(with configuration: RequestConfiguration) async throws -> T { - return try await withCheckedThrowingContinuation { continuation in - runRequest(with: configuration) { (result: Result) in - switch result { - case .success(let response): - continuation.resume(returning: response) - case .failure(let error): - continuation.resume(throwing: error) - } - } - } - } - public func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping NoDataResponseCompletionHandler) { taskQueue.sync { @@ -112,7 +99,7 @@ public class CheckoutNetworkClient: CheckoutClientInterface { let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data()) return CheckoutNetworkError.invalidData(reason: errorReason) } catch { - return CheckoutNetworkError.invalidDataResponseReceivedWithNoData + return CheckoutNetworkError.noDataResponseReceived } } @@ -123,3 +110,34 @@ public class CheckoutNetworkClient: CheckoutClientInterface { return nil } } + +// MARK: Async Wrappers +public extension CheckoutNetworkClient { + + func runRequest(with configuration: RequestConfiguration) async throws -> T { + return try await withCheckedThrowingContinuation { continuation in + runRequest(with: configuration) { (result: Result) in + switch result { + case .success(let response): + continuation.resume(returning: response) + case .failure(let error): + continuation.resume(throwing: error) + } + } + } + } + + func runRequest(with configuration: RequestConfiguration) async throws { + return try await withCheckedThrowingContinuation { continuation in + runRequest(with: configuration) { (error: Error?) in + + guard let error = error else { + continuation.resume(returning: Void()) + return + } + + continuation.resume(throwing: error) + } + } + } +} diff --git a/Sources/CheckoutNetwork/CheckoutNetworkError.swift b/Sources/CheckoutNetwork/CheckoutNetworkError.swift index c85b79d..a98ba20 100644 --- a/Sources/CheckoutNetwork/CheckoutNetworkError.swift +++ b/Sources/CheckoutNetwork/CheckoutNetworkError.swift @@ -15,13 +15,9 @@ public enum CheckoutNetworkError: Error, Equatable { /// Network response was not in the 200 range case unexpectedResponseCode(code: Int) - /// Network call and completion appear valid but no data was returned making the parsing impossible. Use different call if no data is expected + /// Network call and completion appear valid but no data was returned making the parsing impossible. Use runRequest method with NoDataResponseCompletionHandler if no data is expected (HTTP 204 is a success case with no content being returned) case noDataResponseReceived /// Network response returned with HTTP Code 422 case invalidData(reason: ErrorReason) - - - /// HTTP code 422 received with no meaningful data alongside - case invalidDataResponseReceivedWithNoData } diff --git a/Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift b/Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift index 80d7cad..32d9828 100644 --- a/Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift +++ b/Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift @@ -19,13 +19,19 @@ final public class CheckoutNetworkFakeClient: CheckoutClientInterface { calledRequests.append((config: configuration, completion: completionHandler)) } - public func runRequest(with configuration: CheckoutNetwork.RequestConfiguration) async throws -> T { - calledAsyncRequests.append(configuration) - return dataToBeReturned as! T - } - public func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping NoDataResponseCompletionHandler) { calledRequests.append((configuration, completionHandler)) } } + +extension CheckoutNetworkFakeClient { + public func runRequest(with configuration: CheckoutNetwork.RequestConfiguration) async throws -> T { + calledAsyncRequests.append(configuration) + return dataToBeReturned as! T + } + + public func runRequest(with configuration: RequestConfiguration) async throws { + calledAsyncRequests.append(configuration) + } +} diff --git a/Tests/CheckoutNetworkTests/AsyncWrapperTests.swift b/Tests/CheckoutNetworkTests/AsyncWrapperTests.swift index 247c4d6..1c418ed 100644 --- a/Tests/CheckoutNetworkTests/AsyncWrapperTests.swift +++ b/Tests/CheckoutNetworkTests/AsyncWrapperTests.swift @@ -8,8 +8,10 @@ @testable import CheckoutNetwork import XCTest -final class AsyncWrapperTests: XCTestCase { +final class AsyncWrapperTests: XCTestCase {} +// MARK: Run Request whilst expecting a data response +extension AsyncWrapperTests { func test_whenRunRequestReturnsData_ThenAsyncRunRequestPropagatesIt() async throws { let fakeSession = FakeSession() let fakeDataTask = FakeDataTask() @@ -17,13 +19,13 @@ final class AsyncWrapperTests: XCTestCase { let client = CheckoutNetworkClientSpy(session: fakeSession) let testConfig = try! RequestConfiguration(path: FakePath.testServices) - let expectedResult = FakeObject(id: "some response") - client.expectedResult = expectedResult + let expectedResponseBody = FakeObject(id: "some response") + client.expectedResponseBody = expectedResponseBody client.expectedError = nil - let result: FakeObject = try await client.runRequest(with: testConfig) + let responseBody: FakeObject = try await client.runRequest(with: testConfig) XCTAssertEqual(client.configuration.request, testConfig.request) XCTAssertEqual(client.runRequestCallCount, 1) - XCTAssertEqual(result, expectedResult) + XCTAssertEqual(responseBody, expectedResponseBody) } func test_whenRunRequestReturnsError_ThenAsyncRunRequestPropagatesIt() async throws { @@ -34,7 +36,7 @@ final class AsyncWrapperTests: XCTestCase { let testConfig = try! RequestConfiguration(path: FakePath.testServices) let expectedError = FakeError.someError - client.expectedResult = nil + client.expectedResponseBody = nil client.expectedError = expectedError do { @@ -47,3 +49,46 @@ final class AsyncWrapperTests: XCTestCase { } } } + +// MARK: Run Request whilst expecting no data response (HTTP 204) + +extension AsyncWrapperTests { + func test_whenRunRequestWithNoDataReturnsNoError_ThenAsyncRunRequestPropagatesIt() async throws { + let fakeSession = FakeSession() + let fakeDataTask = FakeDataTask() + fakeSession.calledDataTasksReturn = fakeDataTask + let client = CheckoutNetworkClientSpy(session: fakeSession) + let testConfig = try! RequestConfiguration(path: FakePath.testServices) + + client.expectedResponseBody = nil + client.expectedError = nil + do { + try await client.runRequest(with: testConfig) + XCTAssertEqual(client.configuration.request, testConfig.request) + XCTAssertEqual(client.runRequestCallCount, 1) + } catch { + XCTFail("Should have not thrown an error \(error)") + } + } + + func test_whenRunRequestWithNoDataReturnsError_ThenAsyncRunRequestPropagatesIt() async throws { + let fakeSession = FakeSession() + let fakeDataTask = FakeDataTask() + fakeSession.calledDataTasksReturn = fakeDataTask + let client = CheckoutNetworkClientSpy(session: fakeSession) + let testConfig = try! RequestConfiguration(path: FakePath.testServices) + + let expectedError = FakeError.someError + client.expectedResponseBody = nil + client.expectedError = expectedError + + do { + try await client.runRequest(with: testConfig) + XCTFail("An error was expected to be thrown") + } catch let error as FakeError { + XCTAssertEqual(client.configuration.request, testConfig.request) + XCTAssertEqual(client.runRequestCallCount, 1) + XCTAssertEqual(error, expectedError) + } + } +} diff --git a/Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift b/Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift index dcace4c..522be58 100644 --- a/Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift +++ b/Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift @@ -11,17 +11,29 @@ class CheckoutNetworkClientSpy: CheckoutNetworkClient { private(set) var runRequestCallCount: Int = 0 private(set) var configuration: RequestConfiguration! - var expectedResult: FakeObject? + var expectedResponseBody: FakeObject? var expectedError: Error? override func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.CompletionHandler) where T : Decodable { runRequestCallCount += 1 self.configuration = configuration - if let result = expectedResult { + if let result = expectedResponseBody { completionHandler(.success(result as! T)) } else if let error = expectedError { completionHandler(.failure(error)) } } + + override func runRequest(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.NoDataResponseCompletionHandler) { + runRequestCallCount += 1 + self.configuration = configuration + + guard let error = expectedError else { + completionHandler(nil) + return + } + + completionHandler(error) + } }