From 4281105e4b085e87bd1e9163043d0ee4dc9c54fd Mon Sep 17 00:00:00 2001 From: Okhan Okbay Date: Thu, 1 Feb 2024 16:20:47 +0000 Subject: [PATCH] Adding an async wrapper for no data success case This means for HTTP 204 response code. When we receive it, there is no additional content in the body of the response. We were handling this before with another method that has a different completionHandler. Now it's wrapped in an async method to be used from an async context. --- .../CheckoutClientInterface.swift | 18 ++++-- .../CheckoutNetworkClient.swift | 46 ++++++++++----- .../CheckoutNetworkError.swift | 6 +- .../CheckoutNetworkFakeClient.swift | 16 ++++-- .../AsyncWrapperTests.swift | 57 +++++++++++++++++-- .../Helpers/CheckoutNetworkClientSpy.swift | 16 +++++- 6 files changed, 123 insertions(+), 36 deletions(-) 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) + } }