Skip to content

Commit

Permalink
Adding an async wrapper for no data success case
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
okhan-okbay-cko committed Feb 1, 2024
1 parent 4bfb354 commit 4281105
Show file tree
Hide file tree
Showing 6 changed files with 123 additions and 36 deletions.
18 changes: 14 additions & 4 deletions Sources/CheckoutNetwork/CheckoutClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T> = ((Result<T, Error>) -> 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<T: Decodable>(with configuration: RequestConfiguration,
completionHandler: @escaping CompletionHandler<T>)

/// Async wrapper of func runRequest(_:_:) with CompletionHandler<T>
func runRequest<T: Decodable>(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<T>
func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T

/// Async wrapper of func runRequest(_:_:) with NoDataResponseCompletionHandler
func runRequest(with configuration: RequestConfiguration) async throws
}

46 changes: 32 additions & 14 deletions Sources/CheckoutNetwork/CheckoutNetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,19 +58,6 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
}
}

public func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (result: Result<T, Error>) 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 {
Expand Down Expand Up @@ -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
}
}

Expand All @@ -123,3 +110,34 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
return nil
}
}

// MARK: Async Wrappers
public extension CheckoutNetworkClient {

func runRequest<T: Decodable>(with configuration: RequestConfiguration) async throws -> T {
return try await withCheckedThrowingContinuation { continuation in
runRequest(with: configuration) { (result: Result<T, Error>) 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)
}
}
}
}
6 changes: 1 addition & 5 deletions Sources/CheckoutNetwork/CheckoutNetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
16 changes: 11 additions & 5 deletions Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,19 @@ final public class CheckoutNetworkFakeClient: CheckoutClientInterface {
calledRequests.append((config: configuration, completion: completionHandler))
}

public func runRequest<T: Decodable>(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<T: Decodable>(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)
}
}
57 changes: 51 additions & 6 deletions Tests/CheckoutNetworkTests/AsyncWrapperTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,24 @@
@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()
fakeSession.calledDataTasksReturn = fakeDataTask
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 {
Expand All @@ -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 {
Expand All @@ -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)
}
}
}
16 changes: 14 additions & 2 deletions Tests/CheckoutNetworkTests/Helpers/CheckoutNetworkClientSpy.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(with configuration: RequestConfiguration, completionHandler: @escaping CheckoutNetworkClient.CompletionHandler<T>) 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)
}
}

0 comments on commit 4281105

Please sign in to comment.