Skip to content

Commit

Permalink
Add new error codes to handle HTTP 422
Browse files Browse the repository at this point in the history
Also wrap the runRequest(_:_:) function in
an async wrapper.
  • Loading branch information
okhan-okbay-cko committed Jan 18, 2024
1 parent b75e158 commit d5ac718
Show file tree
Hide file tree
Showing 11 changed files with 214 additions and 20 deletions.
2 changes: 1 addition & 1 deletion .github/scripts/lintEditedFiles.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ git fetch origin main
echo "Fetched"

# Use a conditional to check if there are edited files
if EDITED_FILES=$(git diff HEAD origin/main --name-only --diff-filter=d | grep "\.swift" | grep -v "\.swiftlint\.yml" | xargs echo | tr ' ' ','); then
if EDITED_FILES=$(git diff HEAD origin/main --name-only --diff-filter=d | grep "\.swift" | grep -v "\.swiftlint\.yml" | xargs echo | tr ' ' ',' | sed 's/,/, /g'); then
echo "Got edited files"
echo $EDITED_FILES

Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/verify-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v3

- name: Build
- name: Build the Package
run: |
set -o pipefail && xcodebuild -scheme CheckoutNetwork -destination "platform=iOS Simulator,name=iPhone 14 Pro,OS=latest"
- name: Run Tests
run: |
set -o pipefail && xcodebuild -scheme CheckoutNetwork -destination "platform=iOS Simulator,name=iPhone 14 Pro,OS=latest" test
55 changes: 55 additions & 0 deletions .swiftlint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
included:
- Source

excluded:
- Tests

opt_in_rules:
- array_init
- closure_end_indentation
- closure_spacing
- collection_alignment
- colon # promote to error
- convenience_type
- discouraged_object_literal
- empty_collection_literal
- empty_count
- empty_string
- enum_case_associated_values_count
- fatal_error_message
- first_where
- force_unwrapping
- implicitly_unwrapped_optional
- last_where
- legacy_random
- literal_expression_end_indentation
- multiline_arguments
- multiline_function_chains
- multiline_literal_brackets
- multiline_parameters
- multiline_parameters_brackets
- operator_usage_whitespace
- overridden_super_call
- pattern_matching_keywords
- prefer_self_type_over_type_of_self
- redundant_nil_coalescing
- redundant_type_annotation
- strict_fileprivate
- toggle_bool
- trailing_closure
- unneeded_parentheses_in_closure_argument
- yoda_condition

disabled_rules:
- multiline_parameters_brackets

analyzer_rules:
- unused_import

line_length:
warning: 160
ignores_urls: true

identifier_name:
excluded:
- id
8 changes: 5 additions & 3 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import PackageDescription
let package = Package(
name: "CheckoutNetwork",
platforms: [
.iOS(.v11),
.iOS(.v13),
],
products: [
.library(
Expand All @@ -19,12 +19,14 @@ let package = Package(
],
targets: [
.target(
name: "CheckoutNetwork"),
name: "CheckoutNetwork",
path: "Sources/CheckoutNetwork"),
.target(
name: "CheckoutNetworkFakeClient",
dependencies: ["CheckoutNetwork"]),
.testTarget(
name: "CheckoutNetworkTests",
dependencies: ["CheckoutNetwork"]),
dependencies: ["CheckoutNetwork"],
path: "Tests"),
]
)
5 changes: 4 additions & 1 deletion Sources/CheckoutNetwork/CheckoutClientInterface.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ public protocol CheckoutClientInterface {
/// 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)
Expand Down
40 changes: 32 additions & 8 deletions Sources/CheckoutNetwork/CheckoutNetworkClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,17 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
completionHandler(.failure(error))
return
}
if let responseError = self.getErrorFromResponse(response) {
completionHandler(.failure(responseError))
return
}

guard let data = data else {
completionHandler(.failure(CheckoutNetworkError.noDataResponseReceived))
return
}

if let responseError = self.getErrorFromResponse(response, data: data) {
completionHandler(.failure(responseError))
return
}

do {
let dataResponse = try JSONDecoder().decode(T.self, from: data)
completionHandler(.success(dataResponse))
Expand All @@ -55,7 +57,20 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
task.resume()
}
}


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 All @@ -68,7 +83,7 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
completionHandler(error)
return
}
if let responseError = self.getErrorFromResponse(response) {
if let responseError = self.getErrorFromResponse(response, data: nil) {
completionHandler(responseError)
return
}
Expand All @@ -87,11 +102,20 @@ public class CheckoutNetworkClient: CheckoutClientInterface {
return taskID
}

private func getErrorFromResponse(_ response: URLResponse?) -> Error? {
guard let response = response as? HTTPURLResponse else {
private func getErrorFromResponse(_ response: URLResponse?, data: Data?) -> Error? {
guard let response = response as? HTTPURLResponse else {
return CheckoutNetworkError.unexpectedResponseCode(code: 0)
}

guard response.statusCode != 422 else {
do {
let errorReason = try JSONDecoder().decode(ErrorReason.self, from: data ?? Data())
return CheckoutNetworkError.invalidData(reason: errorReason)
} catch {
return CheckoutNetworkError.invalidDataResponseReceivedWithNoData
}
}

guard response.statusCode >= 200,
response.statusCode < 300 else {
return CheckoutNetworkError.unexpectedResponseCode(code: response.statusCode)
Expand Down
7 changes: 7 additions & 0 deletions Sources/CheckoutNetwork/CheckoutNetworkError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ public enum CheckoutNetworkError: Error, Equatable {

/// Network call and completion appear valid but no data was returned making the parsing impossible. Use different call if no data is expected
case noDataResponseReceived

/// Network response returned with HTTP Code 422
case invalidData(reason: ErrorReason)


/// HTTP code 422 received with no meaningful data alongside
case invalidDataResponseReceivedWithNoData
}
20 changes: 20 additions & 0 deletions Sources/CheckoutNetwork/Models/ErrorReason.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// InvalidData.swift
//
//
// Created by Okhan Okbay on 18/01/2024.
//

import Foundation

public struct ErrorReason: Decodable, Equatable {
public let requestID: String
public let errorType: String
public let errorCodes: [String]

enum CodingKeys: String, CodingKey {
case requestID = "request_id"
case errorType = "error_type"
case errorCodes = "error_codes"
}
}
14 changes: 10 additions & 4 deletions Sources/CheckoutNetworkFakeClient/CheckoutNetworkFakeClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,23 @@ import Foundation
import CheckoutNetwork

final public class CheckoutNetworkFakeClient: CheckoutClientInterface {

public var calledRequests: [(config: RequestConfiguration, completion: Any)] = []


public var calledAsyncRequests: [RequestConfiguration] = []
public var dataToBeReturned: Decodable!

public func runRequest<T: Decodable>(with configuration: RequestConfiguration,
completionHandler: @escaping CompletionHandler<T>) {
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))
}

}
73 changes: 73 additions & 0 deletions Tests/CheckoutNetworkTests/AsyncWrapperTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// AsyncWrapperTests.swift
//
//
// Created by Okhan Okbay on 18/01/2024.
//

@testable import CheckoutNetwork
import XCTest

final class AsyncWrapperTests: XCTestCase {

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
client.expectedError = nil
let result: FakeObject = try await client.runRequest(with: testConfig)
XCTAssertEqual(client.configuration.request, testConfig.request)
XCTAssertEqual(client.runRequestCallCount, 1)
XCTAssertEqual(result, expectedResult)
}

func test_whenRunRequestReturnsError_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.expectedResult = nil
client.expectedError = expectedError

do {
let _: FakeObject = 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)
}
}
}

enum FakeError: Error {
case someError
}

class CheckoutNetworkClientSpy: CheckoutNetworkClient {
private(set) var runRequestCallCount: Int = 0
private(set) var configuration: RequestConfiguration!

var expectedResult: 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 {
completionHandler(.success(result as! T))
} else if let error = expectedError {
completionHandler(.failure(error))
}
}
}

4 changes: 2 additions & 2 deletions Tests/CheckoutNetworkTests/CheckoutNetworkClientTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ final class CheckoutNetworkClientTests: XCTestCase {

let testConfig = try! RequestConfiguration(path: FakePath.testServices)
client.runRequest(with: testConfig) { (result: Result<FakeObject, Error>) in }

XCTAssertEqual(client.tasks.count, 1)
XCTAssertTrue(client.tasks.values.first === fakeDataTask)
XCTAssertTrue(fakeDataTask.wasStarted)
Expand Down Expand Up @@ -73,7 +73,7 @@ final class CheckoutNetworkClientTests: XCTestCase {
XCTAssertEqual(failure as NSError, expectedError)
}
}

XCTAssertFalse(client.tasks.isEmpty)
let requestCompletion = fakeSession.calledDataTasks.first!.completion
requestCompletion(expectedData, expectedResponse, expectedError)
Expand Down

0 comments on commit d5ac718

Please sign in to comment.