From a26cfe7ebc7b00cda47021a7513a280dced70a46 Mon Sep 17 00:00:00 2001 From: Daniel Koster Date: Tue, 16 Dec 2025 21:29:47 -0300 Subject: [PATCH 1/2] Done: NetworkRequestFactory ported --- Sources/Errors/Error+Additions.swift | 23 +++ Sources/Errors/HTTPStatusCode.swift | 139 ++++++++++++++++++ Sources/Errors/RequestError.swift | 51 +++++++ Sources/Logger.swift | 2 - .../Network Settings/HostEnvironment.swift | 24 --- Sources/Request/HTTPRequest.swift | 48 ++++++ Sources/Request/HTTPResponse.swift | 62 ++++++++ .../NetworkRequestFactory+Codable.swift | 45 ++++++ .../NetworkRequestFactory+Combine.swift | 24 +++ .../NetworkRequestFactory.swift | 20 +++ .../NetworkRequestFactoryErrorValidator.swift | 38 +++++ .../URLSession/URLSessionProtocol.swift | 48 ++++++ .../URLSession/URLSessionRequestFactory.swift | 73 +++++++++ .../URLSessionProtocol/MockDataModel.swift | 30 ++++ .../URLSessionProtocol/ResponsesStubs.swift | 31 ++++ .../URLSessionDataTaskMock.swift | 56 +++++++ .../URLSessionDownloadMock.swift | 37 +++++ .../URLSessionDownloadTaskMock.swift | 21 +++ .../URLSessionProtocol/URLSessionMock.swift | 92 ++++++++++++ .../URLSessionMockWithDelay.swift | 29 ++++ .../QuickHatchHTTPTests.swift | 6 - .../TestCases/Request/HTTPRequestTests.swift | 29 ++++ .../NetworkRequestFactory+CombineTests.swift | 76 ++++++++++ .../NetworkRequestFactory+ObjectTests.swift | 106 +++++++++++++ .../URLSessionDataTasksTests.swift | 65 ++++++++ ...SessionNetworkRequestFactoryTestCase.swift | 51 +++++++ ...URLSessionNetworkRequestFactoryTests.swift | 118 +++++++++++++++ Tests/TestCases/Resources/DataMock.json | 5 + Tests/TestCases/Resources/swifticon.png | Bin 0 -> 5538 bytes 29 files changed, 1317 insertions(+), 32 deletions(-) create mode 100755 Sources/Errors/Error+Additions.swift create mode 100755 Sources/Errors/HTTPStatusCode.swift create mode 100755 Sources/Errors/RequestError.swift delete mode 100644 Sources/Network Settings/HostEnvironment.swift create mode 100644 Sources/Request/HTTPRequest.swift create mode 100644 Sources/Request/HTTPResponse.swift create mode 100644 Sources/RequestFactory/NetworkRequestFactory+Codable.swift create mode 100644 Sources/RequestFactory/NetworkRequestFactory+Combine.swift create mode 100644 Sources/RequestFactory/NetworkRequestFactory.swift create mode 100644 Sources/RequestFactory/URLSession/NetworkRequestFactoryErrorValidator.swift create mode 100644 Sources/RequestFactory/URLSession/URLSessionProtocol.swift create mode 100644 Sources/RequestFactory/URLSession/URLSessionRequestFactory.swift create mode 100644 Tests/Mocks/URLSessionProtocol/MockDataModel.swift create mode 100644 Tests/Mocks/URLSessionProtocol/ResponsesStubs.swift create mode 100644 Tests/Mocks/URLSessionProtocol/URLSessionDataTaskMock.swift create mode 100644 Tests/Mocks/URLSessionProtocol/URLSessionDownloadMock.swift create mode 100644 Tests/Mocks/URLSessionProtocol/URLSessionDownloadTaskMock.swift create mode 100644 Tests/Mocks/URLSessionProtocol/URLSessionMock.swift create mode 100644 Tests/Mocks/URLSessionProtocol/URLSessionMockWithDelay.swift delete mode 100644 Tests/TestCases/QuickHatchHTTPTests/QuickHatchHTTPTests.swift create mode 100644 Tests/TestCases/Request/HTTPRequestTests.swift create mode 100644 Tests/TestCases/RequestFactory/NetworkRequestFactory+CombineTests.swift create mode 100644 Tests/TestCases/RequestFactory/NetworkRequestFactory+ObjectTests.swift create mode 100644 Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift create mode 100644 Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTestCase.swift create mode 100644 Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTests.swift create mode 100644 Tests/TestCases/Resources/DataMock.json create mode 100644 Tests/TestCases/Resources/swifticon.png diff --git a/Sources/Errors/Error+Additions.swift b/Sources/Errors/Error+Additions.swift new file mode 100755 index 0000000..1deff8b --- /dev/null +++ b/Sources/Errors/Error+Additions.swift @@ -0,0 +1,23 @@ +// +// Error+Additions.swift +// QuickHatch +// +// Created by Daniel Koster on 3/31/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +extension Error { + + public var requestWasCancelled: Bool { + return (self as NSError).code == -999 + } + + public var isUnauthorized: Bool { + if let error = self as? RequestError { + return error == .unauthorized + } + return false + } +} diff --git a/Sources/Errors/HTTPStatusCode.swift b/Sources/Errors/HTTPStatusCode.swift new file mode 100755 index 0000000..600df1e --- /dev/null +++ b/Sources/Errors/HTTPStatusCode.swift @@ -0,0 +1,139 @@ +// +// HTTPStatusCode.swift +// QuickHatch +// +// Created by Daniel Koster on 3/30/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation +/** + HTTP status codes as per http://en.wikipedia.org/wiki/List_of_HTTP_status_codes + + The RF2616 standard is completely covered (http://www.ietf.org/rfc/rfc2616.txt) + */ +public enum HTTPStatusCode: Int, Sendable { + // Informational + case httpContinue = 100 + case switchingProtocols = 101 + case processing = 102 + + // Success + case oK = 200 + case created = 201 + case accepted = 202 + case nonAuthoritativeInformation = 203 + case noContent = 204 + case resetContent = 205 + case partialContent = 206 + case multiStatus = 207 + case alreadyReported = 208 + case iMUsed = 226 + + // Redirections + case multipleChoices = 300 + case movedPermanently = 301 + case found = 302 + case seeOther = 303 + case notModified = 304 + case useProxy = 305 + case switchProxy = 306 + case temporaryRedirect = 307 + case permanentRedirect = 308 + + // Client Errors + case badRequest = 400 + case unauthorized = 401 + case paymentRequired = 402 + case forbidden = 403 + case notFound = 404 + case methodNotAllowed = 405 + case notAcceptable = 406 + case proxyAuthenticationRequired = 407 + case requestTimeout = 408 + case conflict = 409 + case gone = 410 + case lengthRequired = 411 + case preconditionFailed = 412 + case requestEntityTooLarge = 413 + case requestURITooLong = 414 + case unsupportedMediaType = 415 + case requestedRangeNotSatisfiable = 416 + case expectationFailed = 417 + case imATeapot = 418 + case authenticationTimeout = 419 + case unprocessableEntity = 422 + case locked = 423 + case failedDependency = 424 + case upgradeRequired = 426 + case preconditionRequired = 428 + case tooManyRequests = 429 + case requestHeaderFieldsTooLarge = 431 + case loginTimeout = 440 + case noResponse = 444 + case retryWith = 449 + case unavailableForLegalReasons = 451 + case requestHeaderTooLarge = 494 + case certError = 495 + case noCert = 496 + case hTTPToHTTPS = 497 + case tokenExpired = 498 + case clientClosedRequest = 499 + + // Server Errors + case internalServerError = 500 + case notImplemented = 501 + case badGateway = 502 + case serviceUnavailable = 503 + case gatewayTimeout = 504 + case hTTPVersionNotSupported = 505 + case variantAlsoNegotiates = 506 + case insufficientStorage = 507 + case loopDetected = 508 + case bandwidthLimitExceeded = 509 + case notExtended = 510 + case networkAuthenticationRequired = 511 + case networkTimeoutError = 599 +} + +extension HTTPStatusCode { + /// Informational - Request received, continuing process. + public var isInformational: Bool { + return self.rawValue >= 100 && self.rawValue <= 199 + } + /// Success - The action was successfully received, understood, and accepted. + public var isSuccess: Bool { + return self.rawValue >= 200 && self.rawValue <= 299 + } + /// Redirection - Further action must be taken in order to complete the request. + public var isRedirection: Bool { + return self.rawValue >= 300 && self.rawValue <= 399 + } + /// Client Error - The request contains bad syntax or cannot be fulfilled. + public var isClientError: Bool { + return self.rawValue >= 400 && self.rawValue <= 499 + } + /// Server Error - The server failed to fulfill an apparently valid request. + public var isServerError: Bool { + return self.rawValue >= 500 && self.rawValue <= 599 + } + +} + +extension HTTPStatusCode { + /// - returns: a localized string suitable for displaying to users that describes the specified status code. + public var localizedReasonPhrase: String { + return HTTPURLResponse.localizedString(forStatusCode: rawValue) + } +} + +// MARK: - Printing + +extension HTTPStatusCode: CustomDebugStringConvertible, CustomStringConvertible { + public var description: String { + return "\(rawValue) - \(localizedReasonPhrase)" + } + public var debugDescription: String { + return "HTTPStatusCode:\(description)" + } +} diff --git a/Sources/Errors/RequestError.swift b/Sources/Errors/RequestError.swift new file mode 100755 index 0000000..1548594 --- /dev/null +++ b/Sources/Errors/RequestError.swift @@ -0,0 +1,51 @@ +// +// RequestError.swift +// QuickHatch +// +// Created by Daniel Koster on 3/30/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public enum RequestPollingError: Error, Sendable { + case attemptsOverflow +} + +public enum ImageError: Error { + case malformedError +} + +public enum RequestError: Error, Equatable, Sendable { + + public static func == (lhs: RequestError, rhs: RequestError) -> Bool { + switch (lhs, rhs) { + case (.unauthorized, .unauthorized): return true + case (.serializationError, .serializationError): return true + case (.unknownError(let statusCodeA), .unknownError(let statusCodeB)): return statusCodeA == statusCodeB + case (.cancelled, .cancelled): return true + case (.noResponse, .noResponse):return true + case (.requestWithError(let statusCodeA), .requestWithError(let statusCodeB)): + return statusCodeA.rawValue == statusCodeB.rawValue + case (.invalidParameters, .invalidParameters): return true + case (.malformedRequest, .malformedRequest): return true + case (.other, .other): return true + default: return false + } + } + + public static func map(error: Error) -> RequestError { + if error.requestWasCancelled { return .cancelled } + return (error as? RequestError) ?? .other(error: error) + } + + case unauthorized + case unknownError(statusCode: Int) + case cancelled + case noResponse + case requestWithError(statusCode: HTTPStatusCode) + case serializationError(error: Error) + case invalidParameters + case malformedRequest + case other(error: Error) +} diff --git a/Sources/Logger.swift b/Sources/Logger.swift index 7c87377..336dcc2 100644 --- a/Sources/Logger.swift +++ b/Sources/Logger.swift @@ -8,8 +8,6 @@ import Foundation -//public let log = Log("🌐QuickHatch🌐 -") - public struct LogsShortcuts { public static let quickhatch = "🌐QuickHatch🌐 - " public static let commandModule = "\(LogsShortcuts.quickhatch)Command -> " diff --git a/Sources/Network Settings/HostEnvironment.swift b/Sources/Network Settings/HostEnvironment.swift deleted file mode 100644 index d814d25..0000000 --- a/Sources/Network Settings/HostEnvironment.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// NetworkEnvironment.swift -// QuickHatch -// -// Created by QuickHatch on 05/03/2019. -// Copyright © 2019 DaVinci Labs. All rights reserved. -// - -import Foundation - -public protocol HostEnvironment { - var baseURL: String { get } - var headers: [String: String] { get } -} - -public class GenericHostEnvironment: HostEnvironment { - public var headers: [String: String] - public var baseURL: String - - public init(headers: [String: String], baseURL: String) { - self.headers = headers - self.baseURL = baseURL - } -} diff --git a/Sources/Request/HTTPRequest.swift b/Sources/Request/HTTPRequest.swift new file mode 100644 index 0000000..564a862 --- /dev/null +++ b/Sources/Request/HTTPRequest.swift @@ -0,0 +1,48 @@ +// +// HTTPRequest.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/13/25. +// +import Foundation +import Combine + +public protocol DataTask: NSObjectProtocol { + func resume() + func suspend() + func cancel() +} + +public protocol HTTPRequest { + var headers: [String: String] { get } + var body: Data? { get } + var url: String { get } + var method: HTTPMethod { get } +} + +public protocol HTTPRequestActionable { + associatedtype ResponseType: Codable + func response(queue: DispatchQueue) async -> Result + var responsePublisher: any Publisher, Never> { get } +} + +public struct QHHTTPRequest: HTTPRequest, URLRequestProtocol { + public let headers: [String : String] + public let body: Data? + public let url: String + public let method: HTTPMethod + + public init(headers: [String : String] = [:], + body: Data? = nil, + url: String, + method: HTTPMethod) { + self.headers = headers + self.body = body + self.url = url + self.method = method + } + + public func asURLRequest() throws -> URLRequest { + return try URLRequest(url: url, method: method, headers: headers) + } +} diff --git a/Sources/Request/HTTPResponse.swift b/Sources/Request/HTTPResponse.swift new file mode 100644 index 0000000..b3a7fff --- /dev/null +++ b/Sources/Request/HTTPResponse.swift @@ -0,0 +1,62 @@ +// +// HTTPRespone.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/13/25. +// + +import Foundation + +public protocol HTTPResponse { + var statusCode: HTTPStatusCode { get } + var headers: [AnyHashable: Any] { get } + var body: Data? { get } +} + +public extension HTTPResponse { + func decode(decoder: JSONDecoder) throws -> Response { + if let body = body { + let decodedBody = try decoder.decode(T.self, from: body) + return Response(data: decodedBody, statusCode: statusCode, headers: headers) + } + throw RequestError.noResponse + } +} + +public struct QHHTTPResponse: HTTPResponse { + public let statusCode: HTTPStatusCode + public let headers: [AnyHashable : Any] + public let body: Data? + + public init(body: Data?, urlResponse: URLResponse) { + self.body = body + self.headers = (urlResponse as? HTTPURLResponse)?.allHeaderFields ?? [:] + self.statusCode = HTTPStatusCode(rawValue: (urlResponse as? HTTPURLResponse)?.statusCode ?? -1) ?? .serviceUnavailable + } +} + +public struct Response { + public let data: Value + public let statusCode: HTTPStatusCode + public let headers: [AnyHashable: Any] + + public init(data: Value, + statusCode: HTTPStatusCode, + headers: [AnyHashable: Any]) { + self.data = data + self.statusCode = statusCode + self.headers = headers + } + + public func map(transform: (Value) -> NewValue) -> Response { + return Response(data: transform(data), statusCode: statusCode, headers: headers) + } + + public func flatMap (transform: (Value) -> Response) -> Response { + return transform(data) + } + + public func filter(query: (Value) -> Bool) -> Response { + return query(data) ? Response(data: data, statusCode: statusCode, headers: headers) : Response(data: nil, statusCode: statusCode, headers: headers) + } +} diff --git a/Sources/RequestFactory/NetworkRequestFactory+Codable.swift b/Sources/RequestFactory/NetworkRequestFactory+Codable.swift new file mode 100644 index 0000000..c3fc469 --- /dev/null +++ b/Sources/RequestFactory/NetworkRequestFactory+Codable.swift @@ -0,0 +1,45 @@ +// +// NetworkRequestFactory+Codable.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 8/9/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public extension NetworkRequestFactory { + func response(request: URLRequest, + dispatchQueue: DispatchQueue = .main, + jsonDecoder: JSONDecoder = JSONDecoder(), + completionHandler completion: @Sendable @escaping (Result, Error>) -> Void) -> DataTask { + return data(request: request, dispatchQueue: dispatchQueue) { result in + switch result { + case .failure(let error): + completion(.failure(error)) + case .success(let response): + do { + let decodedResponse: Response = try response.decode(decoder: jsonDecoder) + completion(.success(decodedResponse)) + } catch let decoderError { + completion(Result.failure(RequestError.serializationError(error: decoderError))) + } + } + } + } + + func response(request: URLRequest, + jsonDecoder: JSONDecoder = JSONDecoder()) async throws -> Response { + let response = try await data(request: request) + do { + return try response.decode(decoder: jsonDecoder) + } + catch let requestError as RequestError { + throw requestError + } + catch let error { + throw RequestError.serializationError(error: error) + } + + } +} diff --git a/Sources/RequestFactory/NetworkRequestFactory+Combine.swift b/Sources/RequestFactory/NetworkRequestFactory+Combine.swift new file mode 100644 index 0000000..ae471ad --- /dev/null +++ b/Sources/RequestFactory/NetworkRequestFactory+Combine.swift @@ -0,0 +1,24 @@ +// +// NetworkRequestFactory+Combine.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/7/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import Foundation +import Combine + +public extension NetworkRequestFactory { + + func response(urlRequest: URLRequest, + jsonDecoder: JSONDecoder = JSONDecoder()) -> AnyPublisher, Error> { + return dataPublisher(request: urlRequest) + .tryMap { try $0.decode(decoder: jsonDecoder) } + .mapError { + if $0 is Swift.DecodingError { return RequestError.serializationError(error: $0) } + return $0 + } + .eraseToAnyPublisher() + } +} diff --git a/Sources/RequestFactory/NetworkRequestFactory.swift b/Sources/RequestFactory/NetworkRequestFactory.swift new file mode 100644 index 0000000..91ac5f9 --- /dev/null +++ b/Sources/RequestFactory/NetworkRequestFactory.swift @@ -0,0 +1,20 @@ +// +// NetworkRequestFactory.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/25/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation +import Combine + +public protocol NetworkRequestFactory { + func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask + + func dataPublisher(request: URLRequest) -> AnyPublisher + + func data(request: URLRequest) async throws -> HTTPResponse +} diff --git a/Sources/RequestFactory/URLSession/NetworkRequestFactoryErrorValidator.swift b/Sources/RequestFactory/URLSession/NetworkRequestFactoryErrorValidator.swift new file mode 100644 index 0000000..73940cb --- /dev/null +++ b/Sources/RequestFactory/URLSession/NetworkRequestFactoryErrorValidator.swift @@ -0,0 +1,38 @@ +// +// NetworkRequestFactoryErrorValidator.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 6/5/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +class NetworkRequestFactoryErrorValidator { + static func validate(data: Data?, + response: URLResponse?, + error: Error? = nil) -> RequestError? { + if let urlError = error as? URLError, + urlError.code == URLError.cancelled { + return RequestError.cancelled + } + guard error == nil || !error!.requestWasCancelled else { + return RequestError.cancelled + } + guard let urlResponse = response as? HTTPURLResponse else { + return RequestError.noResponse + } + guard urlResponse.statusCode >= 200 && urlResponse.statusCode <= 201 else { + var error = RequestError.unknownError(statusCode: urlResponse.statusCode) + if let httpStatusCode = HTTPStatusCode(rawValue: urlResponse.statusCode) { + error = RequestError.requestWithError(statusCode: httpStatusCode) + } + return error + } + guard data != nil else { + return RequestError.noResponse + + } + return nil + } +} diff --git a/Sources/RequestFactory/URLSession/URLSessionProtocol.swift b/Sources/RequestFactory/URLSession/URLSessionProtocol.swift new file mode 100644 index 0000000..0442abe --- /dev/null +++ b/Sources/RequestFactory/URLSession/URLSessionProtocol.swift @@ -0,0 +1,48 @@ +// +// URLSessionProtocol.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/7/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import Foundation +import Combine + +public protocol URLSessionProtocol { + func task(with request: URLRequest, completionHandler: @Sendable @escaping (Data?, URLResponse?, Error?) -> Void) -> DataTask + + func task(request: URLRequest) async throws -> (Data, URLResponse) + + func taskPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> +} + +extension URLSessionDataTask: DataTask { + open override func resume() { + self.resume() + } + + open override func cancel() { + self.cancel() + } + + open override func suspend() { + self.suspend() + } +} + +extension URLSession: URLSessionProtocol { + + public func task(request: URLRequest) async throws -> (Data, URLResponse) { + return try await data(for: request) + } + + public func task(with request: URLRequest, + completionHandler: @Sendable @escaping (Data?, URLResponse?, Error?) -> Void) -> DataTask { + return self.dataTask(with: request, completionHandler: completionHandler) + } + + public func taskPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + return self.dataTaskPublisher(for: request).eraseToAnyPublisher() + } +} diff --git a/Sources/RequestFactory/URLSession/URLSessionRequestFactory.swift b/Sources/RequestFactory/URLSession/URLSessionRequestFactory.swift new file mode 100644 index 0000000..c1dcf22 --- /dev/null +++ b/Sources/RequestFactory/URLSession/URLSessionRequestFactory.swift @@ -0,0 +1,73 @@ +// +// URLSessionLayer.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/25/17. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation +import Combine + +public final class URLSessionRequestFactory: NetworkRequestFactory { + private let session: URLSessionProtocol + + public init(urlSession: URLSessionProtocol) { + self.session = urlSession + } + + public func data(request: URLRequest, + dispatchQueue: DispatchQueue, + completionHandler completion: @Sendable @escaping (Result) -> Void) -> DataTask { + return session.task(with: request) { (data: Data?,response: URLResponse?,error: Error?) in + dispatchQueue.async { + if let requestError = NetworkRequestFactoryErrorValidator.validate(data: data, + response: response, + error: error) { + completion(Result.failure(requestError)) + return + } + guard let data = data, let urlResponse = response else { + completion(Result.failure(RequestError.noResponse)) + return + } + let response = QHHTTPResponse(body: data, urlResponse: urlResponse) + completion(.success(response)) + } + } + } + + public func dataPublisher(request: URLRequest) -> AnyPublisher { + return session.taskPublisher(for: request) + .tryMap { response in + if let requestError = NetworkRequestFactoryErrorValidator.validate(data: response.data, + response: response.response) { + throw requestError + } + let httpResponse = QHHTTPResponse(body: response.data, urlResponse: response.response) + return httpResponse + } + .mapError { RequestError.map(error: $0) } + .eraseToAnyPublisher() + } + + public func data(request: URLRequest) async throws -> HTTPResponse { + do { + let response = try await session.task(request: request) + let httpResponse = QHHTTPResponse(body: response.0, urlResponse: response.1) + if let requestError = NetworkRequestFactoryErrorValidator.validate(data: response.0, response: response.1) { + throw requestError + } + return httpResponse + } + catch let requestError as RequestError { + throw requestError + } + catch let error { + if let requestError = NetworkRequestFactoryErrorValidator.validate(data: nil, response: nil, error: error) { + throw requestError + } + throw RequestError.other(error: error) + } + } +} diff --git a/Tests/Mocks/URLSessionProtocol/MockDataModel.swift b/Tests/Mocks/URLSessionProtocol/MockDataModel.swift new file mode 100644 index 0000000..1d7a6f9 --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/MockDataModel.swift @@ -0,0 +1,30 @@ +// +// FakeDataModel.swift +// QuickHatchTests +// +// Created by Daniel Koster on 6/5/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +public class DataModel: Codable, @unchecked Sendable { + var name: String? + var nick: String + var age: Int? + + public init(name: String, nick: String, age: Int) { + self.name = name + self.nick = nick + self.age = age + } +} + +public extension DataModel { + static func getMock() throws -> DataModel { + let path = Bundle(for: self).path(forResource: "DataMock", ofType: "json")! + let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .dataReadingMapped) + let dataModel = try JSONDecoder().decode(DataModel.self, from: data) + return dataModel + } +} diff --git a/Tests/Mocks/URLSessionProtocol/ResponsesStubs.swift b/Tests/Mocks/URLSessionProtocol/ResponsesStubs.swift new file mode 100644 index 0000000..49e6235 --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/ResponsesStubs.swift @@ -0,0 +1,31 @@ +// +// ResponsesStubs.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 12/15/25. +// + +import Foundation +import QuickHatchHTTP + +public struct URLSessionMocks { + public static func anyResponse(statusCode: Int) -> HTTPURLResponse { + return HTTPURLResponse(url: URL(string: "www.google.com")!, + statusCode: statusCode, + httpVersion: "1.1", + headerFields: nil)! + } + + public static var anyDataModelSample: Data { + let dataModel = DataModel(name: "dan", nick: "sp", age: 12) + if let encodedData = try? JSONEncoder().encode(dataModel) { + return encodedData + } + return Data() + } + + public static func anyURLSessionLayer(urlSession: URLSessionProtocol) -> URLSessionRequestFactory { + let urlSessionLayer = URLSessionRequestFactory(urlSession: urlSession) + return urlSessionLayer + } +} diff --git a/Tests/Mocks/URLSessionProtocol/URLSessionDataTaskMock.swift b/Tests/Mocks/URLSessionProtocol/URLSessionDataTaskMock.swift new file mode 100644 index 0000000..03459d1 --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/URLSessionDataTaskMock.swift @@ -0,0 +1,56 @@ +// +// URLSessionDataTaskMock.swift +// QuickHatchTests +// +// Created by Daniel Koster on 6/5/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation + +class URLSessionDataTaskMock: URLSessionDataTask { + + private let closure: () -> Void + + init(closure: @escaping () -> Void) { + self.closure = closure + } + + override func resume() { + closure() + } +} + +class URLSessionDataTaskMockWithDelay: URLSessionDataTaskMock { + private let delay: Double + + init(delay: Double, closure: @escaping () -> Void) { + self.delay = delay + super.init(closure: closure) + } + + override func resume() { + DispatchQueue.main.asyncAfter(deadline: .now() + delay) { + super.resume() + } + } + + override func cancel() { + super.resume() + } +} + +class FakeURLSessionDataTask: URLSessionDataTask { + public var resumed = false + override func resume() { + resumed = true + } + public var canceled = false + override func cancel() { + canceled = true + } + public var suspended = false + override func suspend() { + suspended = true + } +} diff --git a/Tests/Mocks/URLSessionProtocol/URLSessionDownloadMock.swift b/Tests/Mocks/URLSessionProtocol/URLSessionDownloadMock.swift new file mode 100644 index 0000000..ea63f90 --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/URLSessionDownloadMock.swift @@ -0,0 +1,37 @@ +// +// URLSessionDownloadMock.swift +// QuickHatch +// +// Created by Daniel Koster on 5/14/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import Foundation + +class URLSessionDownloadMock: URLSession { + private let downloadingURL: URL? + private let error: Error? + private let urlResponse: URLResponse? + internal weak var downloadDelegate: URLSessionDownloadDelegate? + + init(url: URL?, error: Error? = nil, urlResponse: URLResponse? = nil, delegate: URLSessionDownloadDelegate? = nil) { + self.downloadingURL = url + self.error = error + self.urlResponse = urlResponse + self.downloadDelegate = delegate + } + + override func downloadTask(with request: URLRequest, + completionHandler: @escaping (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask { + let url = downloadingURL + let response = urlResponse + let error = self.error + let mockTask = URLSessionDownloadTaskMock { + completionHandler(url,response, error) + } + downloadDelegate?.urlSession?(self, downloadTask: mockTask, didWriteData: 80, totalBytesWritten: 60, totalBytesExpectedToWrite: 100) + downloadDelegate?.urlSession?(self, downloadTask: mockTask, didWriteData: 80, totalBytesWritten: 80, totalBytesExpectedToWrite: 100) + downloadDelegate?.urlSession?(self, downloadTask: mockTask, didWriteData: 80, totalBytesWritten: 100, totalBytesExpectedToWrite: 100) + return mockTask + } +} diff --git a/Tests/Mocks/URLSessionProtocol/URLSessionDownloadTaskMock.swift b/Tests/Mocks/URLSessionProtocol/URLSessionDownloadTaskMock.swift new file mode 100644 index 0000000..be1f76b --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/URLSessionDownloadTaskMock.swift @@ -0,0 +1,21 @@ +// +// URLSessionDownloadTaskMock.swift +// QuickHatch +// +// Created by Daniel Koster on 5/14/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import Foundation + +class URLSessionDownloadTaskMock: URLSessionDownloadTask { + private let closure: () -> Void + + init(closure: @escaping () -> Void) { + self.closure = closure + } + + override func resume() { + closure() + } +} diff --git a/Tests/Mocks/URLSessionProtocol/URLSessionMock.swift b/Tests/Mocks/URLSessionProtocol/URLSessionMock.swift new file mode 100644 index 0000000..8d95c55 --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/URLSessionMock.swift @@ -0,0 +1,92 @@ +// +// URLSessionMock.swift +// QuickHatchTests +// +// Created by Daniel Koster on 6/5/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation +import QuickHatchHTTP +import Combine + +class URLSessionProtocolMock: URLSessionProtocol, @unchecked Sendable { + func task(request: URLRequest) async throws -> (Data, URLResponse) { + if let data = data, let response = urlResponse { + return (data, response) + } + throw error ?? RequestError.noResponse + } + + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + var data: Data? + var error: Error? + var urlResponse: URLResponse? + + init(data: Data? = nil, error: Error? = nil, urlResponse: URLResponse? = nil) { + self.data = data + self.error = error + self.urlResponse = urlResponse + } + + func task(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> DataTask { + let data = self.data + let error = self.error + let urlResponse = self.urlResponse + return URLSessionDataTaskMock { + completionHandler(data, urlResponse, error) + } + } + + var urlError: URLError = URLError(.cancelled) + + func taskPublisher(for request: URLRequest) -> AnyPublisher<(data: Data, response: URLResponse), URLError> { + return Just((Data(), URLResponse())) + .tryMap { _ in + if let validData = self.data, let validResponse = self.urlResponse { + return (validData, validResponse) + } + throw urlError + } + .mapError { _ in + return urlError + }.eraseToAnyPublisher() + } +} + +class URLSessionMock: URLSession, @unchecked Sendable { + typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void + var data: Data? + var error: Error? + var urlResponse: URLResponse? + + init(data: Data? = nil, error: Error? = nil, urlResponse: URLResponse? = nil) { + self.data = data + self.error = error + self.urlResponse = urlResponse + } + + override func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask { + let data = self.data + let error = self.error + let urlResponse = self.urlResponse + return URLSessionDataTaskMock { + completionHandler(data, urlResponse, error) + } + } +} + +class URLSessionMockResponses: URLSessionProtocolMock, @unchecked Sendable { + private var responses: [(Data?,Error?,URLResponse?)] = [] + + init(responses: [(Data?,Error?,URLResponse?)]) { + self.responses = responses + } + + override func task(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> DataTask { + let firstResponse = responses.popLast() + return URLSessionDataTaskMock { + completionHandler(firstResponse?.0, firstResponse?.2, firstResponse?.1) + } + } +} diff --git a/Tests/Mocks/URLSessionProtocol/URLSessionMockWithDelay.swift b/Tests/Mocks/URLSessionProtocol/URLSessionMockWithDelay.swift new file mode 100644 index 0000000..dd2761f --- /dev/null +++ b/Tests/Mocks/URLSessionProtocol/URLSessionMockWithDelay.swift @@ -0,0 +1,29 @@ +// +// URLSessionMockWithDelay.swift +// QuickHatch +// +// Created by Daniel Koster on 5/14/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import Foundation +import QuickHatchHTTP + +class URLSessionMockWithDelay: URLSessionProtocolMock { + private var delay: Double + + init(data: Data? = nil, error: Error? = nil, urlResponse: URLResponse? = nil, delay: Double) { + self.delay = delay + super.init(data: data, error: error, urlResponse: urlResponse) + } + + override func task(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> DataTask { + let data = self.data + let error = self.error + let urlResponse = self.urlResponse + return URLSessionDataTaskMockWithDelay(delay: delay) { + completionHandler(data, urlResponse, error) + } + } + +} diff --git a/Tests/TestCases/QuickHatchHTTPTests/QuickHatchHTTPTests.swift b/Tests/TestCases/QuickHatchHTTPTests/QuickHatchHTTPTests.swift deleted file mode 100644 index afc1bed..0000000 --- a/Tests/TestCases/QuickHatchHTTPTests/QuickHatchHTTPTests.swift +++ /dev/null @@ -1,6 +0,0 @@ -import Testing -@testable import QuickHatchHTTP - -@Test func example() async throws { - // Write your test here and use APIs like `#expect(...)` to check expected conditions. -} diff --git a/Tests/TestCases/Request/HTTPRequestTests.swift b/Tests/TestCases/Request/HTTPRequestTests.swift new file mode 100644 index 0000000..b70c61f --- /dev/null +++ b/Tests/TestCases/Request/HTTPRequestTests.swift @@ -0,0 +1,29 @@ +// +// HTTPRequestTests.swift +// QuickHatchHTTP +// +// Created by Daniel Koster on 10/13/25. +// +import QuickHatchHTTP +import Foundation +import Testing + +struct HTTPRequestTests { + + @Test + func asURLRequest_expectHeadersCorrectlyParsed() throws { + let sut = QHHTTPRequest(headers: ["Content-Type": "json"], + url: "quickhatch.com", + method: .get) + + let result = try sut.asURLRequest() + + let headers = try #require(result.allHTTPHeaderFields) + let url = try #require(result.url?.absoluteString) + let method = try #require(result.httpMethod) + + #expect(url == "quickhatch.com") + #expect(headers == ["Content-Type": "json"]) + #expect(method == "GET") + } +} diff --git a/Tests/TestCases/RequestFactory/NetworkRequestFactory+CombineTests.swift b/Tests/TestCases/RequestFactory/NetworkRequestFactory+CombineTests.swift new file mode 100644 index 0000000..5d803d8 --- /dev/null +++ b/Tests/TestCases/RequestFactory/NetworkRequestFactory+CombineTests.swift @@ -0,0 +1,76 @@ +// +// NetworkRequestFactory+CombineTests.swift +// QuickHatchTests +// +// Created by Daniel Koster on 10/7/20. +// Copyright © 2020 DaVinci Labs. All rights reserved. +// + +import XCTest +import QuickHatchHTTP +@testable import QuickHatchHTTPMocks +import Combine + +class QHNetworkRequestFactory_CombineTests: URLSessionNetworkRequestFactoryTestCase { + var subscriptions: Set = [] + + func testResponseSerializationErrorCase() { + let expectation = XCTestExpectation() + let fakeUrlSession = URLSessionMock(data: self.getArrayModelSample, urlResponse: getResponse(statusCode: 200)) + let subject = sut(urlSession: fakeUrlSession) + subject.response(urlRequest: fakeURLRequest).sink(receiveCompletion: { completion in + switch completion { + case .failure(let error): XCTAssertEqual(RequestError.serializationError(error: MockError.mock), RequestError.map(error: error)) + case .finished: break + } + expectation.fulfill() + }, receiveValue: { (data : Response) in + XCTAssert(false) + expectation.fulfill() + }).store(in: &subscriptions) + wait(for: [expectation], timeout: 1.0) + } + + func testBadRequestError() { + let expectation = XCTestExpectation() + let dataURLSession = URLSessionProtocolMock(data: Data(), urlResponse: getResponse(statusCode: 400)) + let subject = sut(urlSession: dataURLSession) + subject.response(urlRequest: fakeURLRequest) + .sink(receiveCompletion: { completion in + switch completion { + case .failure(let error): + XCTAssertEqual(RequestError.requestWithError(statusCode: .badRequest), RequestError.map(error: error)) + case .finished: break + } + expectation.fulfill() + }, receiveValue: { (_ : Response) in + XCTAssert(false) + expectation.fulfill() + }).store(in: &subscriptions) + wait(for: [expectation], timeout: 1.0) + } + + func testCancelledError() { + let expectation = XCTestExpectation() + let dataURLSession = URLSessionProtocolMock() + dataURLSession.urlError = URLError(.cancelled) + let subject = sut(urlSession: dataURLSession) + subject.response(urlRequest: fakeURLRequest) + .sink(receiveCompletion: { completion in + switch completion { + case .failure(let error): + XCTAssertEqual(RequestError.cancelled, RequestError.map(error: error)) + case .finished: break + } + expectation.fulfill() + }, receiveValue: { (_ : Response) in + XCTAssert(false) + expectation.fulfill() + }).store(in: &subscriptions) + wait(for: [expectation], timeout: 1.0) + } +} + +enum MockError: Error { + case mock +} diff --git a/Tests/TestCases/RequestFactory/NetworkRequestFactory+ObjectTests.swift b/Tests/TestCases/RequestFactory/NetworkRequestFactory+ObjectTests.swift new file mode 100644 index 0000000..a2d76c4 --- /dev/null +++ b/Tests/TestCases/RequestFactory/NetworkRequestFactory+ObjectTests.swift @@ -0,0 +1,106 @@ +// +// URLSessionLayer+ObjectTests.swift +// QuickHatch +// +// Created by Daniel Koster on 10/18/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Foundation +import QuickHatchHTTP +import XCTest +@testable import QuickHatchHTTPMocks + +final class URLSessionNetworkRequestFactory_ObjectTests: URLSessionNetworkRequestFactoryTestCase { + + func test_response_whenfailureSerializationDataUsingObject_expectCorrectError() throws { + let fakeUrlSession = URLSessionProtocolMock(data: getArrayModelSample, + urlResponse: getResponse(statusCode: 200)) + let urlSessionLayer = sut(urlSession: fakeUrlSession) + let url = try XCTUnwrap(URL(string: "www.google.com")) + urlSessionLayer.response(request: try URLRequest(url: url, + method: .get)) { (result: Result, Error>) in + switch result { + case .success: + XCTAssert(false) + case .failure(let error): + if let requestError = error as? RequestError { + XCTAssert(requestError == .serializationError(error: RequestError.unauthorized)) + } else { + XCTAssert(false) + } + } + }.resume() + } + + func test_response_whenSerializationError_expectCorrectError() throws { + let data = try JSONSerialization.data(withJSONObject: ["name": "hey"], options: .prettyPrinted) + let dataURLSession = URLSessionProtocolMock(data: data, urlResponse: getResponse(statusCode: 200)) + let urlSessionLayer = sut(urlSession: dataURLSession) + let url = try XCTUnwrap(URL(string: "www.google.com")) + + urlSessionLayer.response(request: URLRequest(url: url)) { (result: Result, Error>) in + switch result { + case .failure(let error): + if let reqError = error as? RequestError { + XCTAssert(reqError == RequestError.serializationError(error: RequestError.unauthorized)) + } + case .success: + XCTAssert(false) + } + }.resume() + } + + func test_response_whenSuccess_expectFullDataUsingObject() throws { + let fakeUrlSession = URLSessionProtocolMock(data: self.getDataModelSample, urlResponse: getResponse(statusCode: 200)) + let urlSessionLayer = sut(urlSession: fakeUrlSession) + let url = try XCTUnwrap(URL(string: "www.google.com")) + + urlSessionLayer.response(request: try URLRequest(url: url, + method: .get)) { (result: Result, Error>) in + switch result { + case .success(let dataModel): + XCTAssert(dataModel.data.age! == 12) + case .failure: + XCTAssert(false) + } + }.resume() + } + func test_response_whenUnknownErrorUsingStringObject_expectCorrectError() throws { + let unauthorizedUrlSession = URLSessionProtocolMock(urlResponse: getResponse(statusCode: 524)) + let urlSessionLayer = sut(urlSession: unauthorizedUrlSession) + let url = try XCTUnwrap(URL(string: "www.google.com")) + + urlSessionLayer.response(request: try URLRequest(url: url, + method: .get)) { (result: Result, Error>) in + switch result { + case .success: + XCTAssert(false) + case .failure(let error): + if let requestError = error as? RequestError { + XCTAssert(requestError == .unknownError(statusCode: 524)) + } else { + XCTAssert(false) + } + } + }.resume() + } + + func test_response_whenCancelledError_expectCorrectError() throws { + let dataURLSession = URLSessionProtocolMock(error: NSError(domain: "", code: -999, userInfo: nil), urlResponse: getResponse(statusCode: 200)) + let urlSessionLayer = sut(urlSession: dataURLSession) + let url = try XCTUnwrap(URL(string: "www.google.com")) + + urlSessionLayer.response(request: URLRequest(url: url)) { (result: Result, Error>) in + switch result { + case .failure(let error): + if let reqError = error as? RequestError { + XCTAssert(reqError == RequestError.cancelled) + } + case .success: + XCTAssert(false) + } + }.resume() + } + +} diff --git a/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift b/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift new file mode 100644 index 0000000..deeb330 --- /dev/null +++ b/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift @@ -0,0 +1,65 @@ +// +// URLSessionDataTasksTests.swift +// QuickHatchTests +// +// Created by Daniel Koster on 8/15/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import Testing +import QuickHatchHTTP +import Foundation + + +struct URLSessionDataTasksTests { + + @Test(arguments: [(RequestError.cancelled, false), (RequestError.unauthorized, true)]) + func testUnauthorizedError(requestError: RequestError, expectedResult: Bool) { + #expect(requestError.isUnauthorized == expectedResult) + } + + @Test(arguments: [(-999, true), + (-99, false)]) + func testRequestWasCancelled(errorCode: Int, expectedReslt: Bool) { + let error = NSError(domain: "", code: errorCode, userInfo: nil) + #expect(error.requestWasCancelled == expectedReslt) + } + + @Test func testHTTPStatusCode() { + let httpStatus = 200 + #expect(HTTPStatusCode(rawValue: httpStatus) != nil) + } + + @Test func testHTTPStatusCodeIsSuccess() { + let httpStatus = 200 + let status = HTTPStatusCode(rawValue: httpStatus)! + #expect(status.isSuccess) + } + + @Test func testHTTPStatusCodeIsInfo() { + let httpStatus = 100 + let status = HTTPStatusCode(rawValue: httpStatus)! + #expect(status.isInformational) + } + + @Test func testHTTPStatusCodeIsClientError() { + let httpStatus = 404 + let status = HTTPStatusCode(rawValue: httpStatus)! + #expect(status.isClientError) + } + + @Test func testHTTPStatusCodeIsServer() { + let httpStatus = 500 + let status = HTTPStatusCode(rawValue: httpStatus)! + #expect(status.isServerError) + #expect(status.description == "500 - internal server error") + #expect(status.debugDescription == "HTTPStatusCode:500 - internal server error") + } + + @Test func testHTTPStatusCodeIsRedirection() { + let httpStatus = 300 + let status = HTTPStatusCode(rawValue: httpStatus)! + #expect(status.isRedirection) + } + +} diff --git a/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTestCase.swift b/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTestCase.swift new file mode 100644 index 0000000..77a9fb0 --- /dev/null +++ b/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTestCase.swift @@ -0,0 +1,51 @@ +// +// URLSessionLayerBase.swift +// QuickHatchTests +// +// Created by Daniel Koster on 10/11/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import XCTest +import QuickHatchHTTP +@testable import QuickHatchHTTPMocks + +class URLSessionNetworkRequestFactoryTestCase: XCTestCase { + + func getResponse(statusCode: Int) -> HTTPURLResponse { + return HTTPURLResponse(url: URL(string: "www.google.com")!, + statusCode: statusCode, + httpVersion: "1.1", + headerFields: nil)! + } + + var getDataModelSample: Data { + let dataModel = DataModel(name: "dan", nick: "sp", age: 12) + if let encodedData = try? JSONEncoder().encode(dataModel) { + return encodedData + } + return Data() + } + + let fakeURLRequest = URLRequest(url: URL(fileURLWithPath: "")) + + var getArrayModelSample: Data { + let dataModel = DataModel(name: "dan", nick: "sp", age: 12) + let dataModel2 = DataModel(name: "dani", nick: "sp1", age: 13) + let array = [dataModel,dataModel2] + if let encodedData = try? JSONEncoder().encode(array) { + return encodedData + } + return Data() + } + + func sut(urlSession: URLSessionProtocol, unauthorizedCode: Int = 401) -> URLSessionRequestFactory { + let urlSessionLayer = URLSessionRequestFactory(urlSession: urlSession) + return urlSessionLayer + } + + func getSessionMock(data: Data? = nil, response: URLResponse? = nil, error: Error? = nil) -> URLSessionProtocol { + return URLSessionProtocolMock(data: data, error: error ,urlResponse: response) + } + +} diff --git a/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTests.swift b/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTests.swift new file mode 100644 index 0000000..b3cbfb7 --- /dev/null +++ b/Tests/TestCases/RequestFactory/URLSessionNetworkRequestFactoryTests.swift @@ -0,0 +1,118 @@ +// +// URLSessionLayerTests.swift +// NetworkingLayerTests +// +// Created by Daniel Koster on 6/5/19. +// Copyright © 2019 DaVinci Labs. All rights reserved. +// + +import XCTest +import QuickHatchHTTP +@testable import QuickHatchHTTPMocks +import Testing +import Combine + +class URLSessionNetworkRequestFactoryTests { + private var cancellable = Set() + + struct AsyncDataReponseTestCase : Sendable { + let response: URLSessionProtocolMock + let expectedError: RequestError? + let expectedResult: Data? + + init(response: URLSessionProtocolMock, + expectedError: RequestError? = nil, + expectedResult: Data? = nil) { + self.response = response + self.expectedError = expectedError + self.expectedResult = expectedResult + } + } + + struct AsyncDataResponseTestCases { + static let unauthorizedResponse = AsyncDataReponseTestCase(response: URLSessionProtocolMock(data: Data(), + error: nil, + urlResponse: URLSessionMocks.anyResponse(statusCode: 401)), + expectedError: RequestError.requestWithError(statusCode: .unauthorized)) + static let urlErrorCancelledResponse = AsyncDataReponseTestCase(response: URLSessionProtocolMock(data: nil, + error: URLError(URLError.cancelled), + urlResponse: nil), + expectedError: RequestError.cancelled) + static let noResponse = AsyncDataReponseTestCase(response: URLSessionProtocolMock(data: nil, + error: nil, + urlResponse: nil), + expectedError: RequestError.noResponse) + static let correctDataResponse = AsyncDataReponseTestCase(response: URLSessionProtocolMock(data: Data(), + error: nil, + urlResponse: URLSessionMocks.anyResponse(statusCode: 200))) + static let correctResponse = AsyncDataReponseTestCase(response: URLSessionProtocolMock(data: URLSessionMocks.anyDataModelSample, + error: nil, + urlResponse: URLSessionMocks.anyResponse(statusCode: 200)), + expectedResult: URLSessionMocks.anyDataModelSample) + } + + @Test(arguments: [AsyncDataResponseTestCases.unauthorizedResponse, + AsyncDataResponseTestCases.urlErrorCancelledResponse, + AsyncDataResponseTestCases.correctDataResponse]) + func dataPublisher(testCase: AsyncDataReponseTestCase) async throws { + testCase.response.data = Data() + let sut = URLSessionRequestFactory(urlSession: testCase.response) + + let url = try #require(URL(string: "www.google.com")) + let dummyRequest = try URLRequest(url: url, method: .get) + await confirmation("") { confirmation in + sut.dataPublisher(request: dummyRequest) + .sink(receiveCompletion: { completion in + switch completion { + case .failure(let error): + if let requestError = error as? RequestError { + #expect(requestError == testCase.expectedError) + confirmation.confirm() + } + case .finished: break + } + }, receiveValue: { response in + #expect(response.body != nil) + confirmation.confirm() + }) + .store(in: &cancellable) + } + } + + @Test(arguments: [AsyncDataResponseTestCases.unauthorizedResponse, + AsyncDataResponseTestCases.urlErrorCancelledResponse, + AsyncDataResponseTestCases.noResponse, + AsyncDataResponseTestCases.correctDataResponse]) + func asyncDataResponse(testCase: AsyncDataReponseTestCase) async throws { + let sut = URLSessionRequestFactory(urlSession: testCase.response) + + let url = try #require(URL(string: "www.google.com")) + let dummyRequest = try URLRequest(url: url, method: .get) + do { + let response = try await sut.data(request: dummyRequest) + #expect(response.body != nil) + } catch let error as RequestError { + #expect(error == testCase.expectedError) + } + } + + @Test(arguments: [AsyncDataResponseTestCases.unauthorizedResponse, + AsyncDataResponseTestCases.urlErrorCancelledResponse, + AsyncDataResponseTestCases.noResponse, + AsyncDataResponseTestCases.correctResponse]) + func asyncDataMappedResponse(testCase: AsyncDataReponseTestCase) async throws { + let sut = URLSessionRequestFactory(urlSession: testCase.response) + + let url = try #require(URL(string: "www.google.com")) + let dummyRequest = try URLRequest(url: url, method: .get) + do { + let response: Response = try await sut.response(request: dummyRequest) + let expectedResult = try #require(testCase.expectedResult) + let expectedData = try JSONDecoder().decode(DataModel.self, from: expectedResult) + #expect(response.data.name == expectedData.name) + + } catch let error as RequestError { + #expect(error == testCase.expectedError) + } + } +} diff --git a/Tests/TestCases/Resources/DataMock.json b/Tests/TestCases/Resources/DataMock.json new file mode 100644 index 0000000..00b22b8 --- /dev/null +++ b/Tests/TestCases/Resources/DataMock.json @@ -0,0 +1,5 @@ +{ + "name": "Pelican", + "age": 21, + "nick": "nick" +} diff --git a/Tests/TestCases/Resources/swifticon.png b/Tests/TestCases/Resources/swifticon.png new file mode 100644 index 0000000000000000000000000000000000000000..af99582dfe1cfdfe876896f6f772ad8f70d17a07 GIT binary patch literal 5538 zcmV;T6-xjE5A4JT<%Zqd%@Q}xhPO1PN3 zGKPxK-6#=byNOarg@oTb!gvX~TT?Q!`&Rc_st8~9Oa%Wbqu~*qmhP-23I8o=nbCLnu`>RG(_Kl% zQiT6bq?~m@t5xPNaQa$`u>|4Eu2lO4BCX+xW!WfBrzOT?VZyh4c|PA{wv9oyn?ZC` zI0`6AxGG~`!tefS7{epW{yVFv?Oy?C6K5nvttYRV$wvTA)4ZvX3 z*fE!H%tQFCa0VEW=lAtM#)<;|oTJZb2)9-A(6i9UxFGf+55aU*1>tLU;89#OFg8A7 zi$gjJnJOoIJE*szA@NuDOz7F04&+JJ3_2Krg#XATis^K!>8f;nX++&@pCE`F0p}Yq z!spxQ;CsahnTwp+wGnmG+(8C}!(g5fL^!?=f82S1SoHiNefx^28x05|rl1%OLKrdO zXy1oe^xSfO?u5F#hapbX5N@MHbp22XVz?1?V-M;nbfAK88+#V!=BkR08XAc{;z$)@ zCgDfoNoZ=!t3V_-b9zPu`QL2BEW&sT2EW=jJ}Pkkd}-(vAZ8GLiH00TPry!;Pqbx5 z5Z_^l83~+p!hzVPpq&By!nuUrBaNsVQ_xg~rY}^b5>8}BlT2K<0z}q~G@@>Tq8p8- zy8bl6e^Rf2t)^K)D_UP}lu(~R!f?#e0ysIL{g#q3^sRz=MC{ml6!A%eWc3`J5{9R= z;W}-q#s&t|XXQ}TVfct$P+}Gxix4h*t+a9q(yi-8(pi=hpGIwsHaR6sM)nkap81x7tb&E2>PwJ1g_EB!Htq<>e8((T$kv+slBHZ)K z4620i&GNcG`!dX_{O4)UhUSjIx+E5(9rYFBGq5AEHWI?KmG&5nx!h&&w%<`DQ9(h` zm`eU4q^a9ftL1x40Rc2m9H&Zl}=%%{e1KBf?%McJo(}n!xrkN9OmJz(c6DW@h5auF6-I#+ij2#GJBlfCn z8<-QBy&=QEyqoQ#B1AtN6t&_EG7zkJHeqtgzSzl>{kFst?TUUGXF8Gkv&4;;N&;nO+(9T z8qDY=n+db-ZGlqLvW5kc7=- z^LyVaX;OvLi_6Dtf=1%Im@s7&268&>8=^Z*%o)$Y-K8UUof0P1T1g3Y;{(!&^i4=8 zHKbRe)4eCw6K=zE{P*Ewre*1}B%VNBp8Eu;lt3d9;bV6D->r=vq!yxG(bNL1lj$ub4YogwGa@F0RJa_2qSi5t`9{r*2b!+y>VvpUaISDNXo<-2nb=5=w9gI z^>{5I`-&j03+6VkBekXAMnp-%0chM{6bXyHP&)zMO*HfqAp6XefNIc?A84C*Mu6)4fJ@e_FOT<<; z_RlGcJB0}KHl&-PFyTYaAQDaEah2(=ZU!eDqF+8h6R1|Ao57q6FqQ#7mB#k)cqJvyeuFhluz<6av{MfdASindUaf(^W^m#@ zL-!Ln$>fLR)dW8sYePs}bIzOwgoTbGN!k*-l4tZ`d%tNjV;sUOgt zQeb^XFop67A%U#;7e?as+MecrV6PjCiVoJ9TYm8-n^3m_8Gf|A5M~_Zlp?YcBHG^M zAN$={7a)eiAF=qWJQH>QQULSl%+7=_B~sM!dB*MeeSbGCdKGqKcaXjp93{P};6wER zF3wL#Y5C?ANkoBLJhzm;IolGi=O%JkF~YC3Ow{!|H39a~pI>OOB%4&8TNJ;J>CKFJ zVzziuT~FIk%q9RrkiYO#ZXO}1$U!OxNtSGNx49&kB!inkUN@H7x9p%T_ys`A@b`eQ zOFd5(hVD3}gJiwd|k0X>%fEWPQjm^u8Tnq0NhG zv>K=mCrdJud484VsV{35Q%_8(Xlg$AE_;$4PxD<(Td3K&EHjyB3}Ygzk@2J0Xjvof zzyLvMQ!j4Qa;Dkjh?6w?U^mQC9Pj(JTh?JRDHGJBFOV;yQnNo}G) zZNQX)EVNH$+p~yZRu7b|k8Va)QeP?%ByJ;*8eq5rg=w!aqBj~ZiyxSSf5gbFJn#(j zr>4Li-Vm7}aRHTSZi8k_D^rLQ0;Y)UTiYr;(F0qOi3aS2TN{SK-q4c^XdP>;m6|)n z<)C2+#Jag+NVwlZEXV>4`qET{G$BN%45DrfUpMfv44GY^t#(p-xT?cJQy-a3BQqHs z`r|c#aHRbxK8(fDN+Iew{%5n7O~@PCM)h$sC0j9vAURV$)eedQ%eSIZLg!WMC@0i- zIdcP69YBbW>iIN(fBdf?)&Y48tiF$)5r3r>A#GTh9R3T4pw*d972_~KL}davAR%Nm z|4@M~y&#qe!(pleJU-s2_lLLNwt9u9#4te?XYh}9tn9c)Or zA{I|MOO?(KlZamxB0}m+y}5=(*#G97~Q4n`#HiQ4O5--HeJNU=?RnkkPE}mUC6+ZCs%Sg>A0TsK+@ zE>)ArA>e9@G`YL%M)Z%5h}LBQQdeN{HPg`4#TUq(%3Zp%prWZ?d*zd zRp*_jXfLYUZfk_fLSjTW6J2rQt)9At?Yt8>AdD75^W>&9({OvE4#{-C zU1N+>`YHRo5xzzLjQ&_3W;n2tnD_dUEWE4HWr_z?Bzx?NshB_y6)5ZYlj)LP!*h;s+uX{0>lU@{KdR^8vo2~VDKAJ9PheIb=F7F&>r|D=uUUxm~WGkHS%H@iV>%4bT`MDUN zOm*D@bzQ^PIZ!eE|8!m3th(jG66f5mXYk0+HgT+0b<0L7Wj}}-@nT8u=hM6=$17$P zuTra$DH@5l1I@04pMsn~HOYAOvPEZH-41BKof9Qoi=*OwzJE;ua~f;`juMK``XY^A zwHXB^@QG}aAY6!J1M8?b8DE9WR{Z5LsU-+yhhuQw`ujcAi0>^4cH+kgWoHOuQfbME z5+!U+=qzE#*_U*$D(_I0%wg0ehEJgdonLCrnt$B+$M%zkPZAEFL3@-={kXDZd_{^D zfZdN2j1i`sBaG?3y0RDrsz;`Yk?UB%XF;Pw)y0&hhZrk*71(2qat{lkYu zM{m@Lp2?oR=xA;Ju4}aKX$lm*vpY(dah{UbjQSsqa6$!A(IeU8cOzsS@@aL})#S`d zt9{LmS=(9~VcM;ZfiRsm|72G+F1`+WBzKh&$MEI6{n|<+vSt{CT@2O5RJBBwWC*iR1J) z7;U5V!AT;!jyWTvZp7l5LlQZtJcY0}0qzLnUOT##+8o>LAEm2$h>Y6dr~9X5&`%Iy z4N&B8ZO2*PbSpACrlAiysYhia6iIhd@ZEXg`wz3^F#OeYE9k7I@ia%-&iNxWZ?#1C z%1Q39e9m#A3%1C6&rqBM@c2fcMQ66Vt&NxneurhPHp*3K%E@8SRJcND8qE)AZJl+x z&Rb3TC1jD@C7W4{6BV}DZqin>l2$i{`y91{=yYwwjTl8j_?Y`C;VgTl@DU49)`{YeHQzgzuOu#VO*z39A;Te=Hf1U9|E(;kke9Dd?=^ojh* zlmey@E;_}Fy)d^3nKf4ZZrh&=*mWUlu!8J$>Sk};I8dw5iPVA6)RbbR5Y`?`7PWgp zvKJ#Cb|0(By5*l{=5@sDBDfFy)a|VY+s2IhbseQ1g{f>1rW0mf;sts*_9p|3P>MR6 z*=gzNbezo&9dE=EAB`73#rO^=boBtjLIySEMf|ysC&v?3xgn4~r$M9=)>cXpD-s>2 zGNcpMl-v$mt*H3Ycqn1*<3Bt;1SB%gAjl&8>xg8V1&~Qt+x}(5Y(M~EZTok}qGZC9 zm$3HrR}r&}44AMs_>+j3#?#xy2|d?e>5lzE!bFf*D@<7XB$aOUE+B5Dg=+Z;Ymc3Z z&(Fvyv`GoJ3lY{9B`$|mFSg8nQ;;KAaLM+TS{|dcpF~VAH^~+rqG003wC1LIDy}Dx; z65@1Uq=S|ttkqtYzcd^<-APu?Dkk)XOx44MfWAfpF%<}1SqgJoh3wP_q^be zynp@z-+wE`tEngS^|E^9IS Date: Tue, 16 Dec 2025 21:36:12 -0300 Subject: [PATCH 2/2] fixed linting --- Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift b/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift index deeb330..d1dd6cd 100644 --- a/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift +++ b/Tests/TestCases/RequestFactory/URLSessionDataTasksTests.swift @@ -10,7 +10,6 @@ import Testing import QuickHatchHTTP import Foundation - struct URLSessionDataTasksTests { @Test(arguments: [(RequestError.cancelled, false), (RequestError.unauthorized, true)])