├── .DS_Store
├── Sources
└── EZNetworking
│ ├── Other
│ ├── EmptyResponse.swift
│ ├── Extensions
│ │ ├── String-ext.swift
│ │ └── Data-ext.swift
│ ├── WebSocketTaskProtocol.swift
│ └── URLSessionTaskProtocol.swift
│ ├── Util
│ ├── Uploader
│ │ ├── Helpers
│ │ │ ├── UploadStreamEvent.swift
│ │ │ ├── UploadTypealias.swift
│ │ │ └── DefaultUploadTaskInterceptor.swift
│ │ ├── DataUploader
│ │ │ ├── Protocols
│ │ │ │ └── DataUploadable.swift
│ │ │ └── DataUploader.swift
│ │ └── FileUploader
│ │ │ ├── Protocols
│ │ │ └── FileUploadable.swift
│ │ │ └── FileUploader.swift
│ ├── Performers
│ │ ├── Protocols
│ │ │ └── RequestPerformable.swift
│ │ └── RequestPerformer.swift
│ ├── Downloader
│ │ ├── Protocols
│ │ │ └── FileDownloadable.swift
│ │ ├── Helpers
│ │ │ └── DefaultDownloadTaskInterceptor.swift
│ │ └── FileDownloader.swift
│ └── WebSocket
│ │ └── Helpers
│ │ ├── DefaultWebSocketTaskInterceptor.swift
│ │ └── WebSocketConnectionState.swift
│ ├── HTTP
│ ├── Body
│ │ ├── HTTPBody.swift
│ │ └── MultipartFormData
│ │ │ ├── MultipartFormPart.swift
│ │ │ └── MultipartFormData.swift
│ ├── Parameters
│ │ └── HTTPParameters.swift
│ ├── Methods
│ │ └── HTTPMethod.swift
│ └── Headers
│ │ ├── HTTPHeader.swift
│ │ ├── MimeType.swift
│ │ └── AuthorizationType.swift
│ ├── Interceptors
│ ├── Protocols
│ │ ├── MetricsInterceptor.swift
│ │ ├── CacheInterceptor.swift
│ │ ├── RedirectInterceptor.swift
│ │ ├── UploadTaskInterceptor.swift
│ │ ├── TaskLifecycleInterceptor.swift
│ │ ├── AuthenticationInterceptor.swift
│ │ ├── StreamTaskInterceptor.swift
│ │ ├── DownloadTaskInterceptor.swift
│ │ ├── DataTaskInterceptor.swift
│ │ └── WebSocketTaskInterceptor.swift
│ ├── SessionDelegate+Extensions
│ │ ├── SessionDelegate+URLSessionDelegate.swift
│ │ ├── SessionDelegate+URLSessionWebSocketDelegate.swift
│ │ ├── SessionDelegate+URLSessionStreamDelegate.swift
│ │ ├── SessionDelegate+URLSessionDownloadDelegate.swift
│ │ ├── SessionDelegate+URLSessionDataDelegate.swift
│ │ └── SessionDelegate+URLSessionTaskDelegate.swift
│ └── SessionDelegate.swift
│ ├── Error
│ ├── HTTPError
│ │ ├── StatusCodeTypes
│ │ │ ├── HTTPInformationalStatus.swift
│ │ │ ├── HTTPRedirectionStatus.swift
│ │ │ ├── HTTPSuccessStatus.swift
│ │ │ ├── HTTPServerErrorStatus.swift
│ │ │ └── HTTPClientErrorStatus.swift
│ │ └── HTTPError.swift
│ ├── NetworkingError.swift
│ ├── InternalError
│ │ └── InternalError.swift
│ └── WebSocketError
│ │ └── WebSocketError.swift
│ ├── Encoders
│ ├── HTTPHeaderEncoder.swift
│ └── HTTPParameterEncoder.swift
│ ├── Decoding
│ └── RequestDecoder.swift
│ ├── Request
│ ├── RequestFactory.swift
│ ├── Request.swift
│ └── RequestBuilder.swift
│ └── Validator
│ └── ResponseValidator.swift
├── Tests
└── EZNetworkingTests
│ ├── Mocks
│ ├── Mock-Codeable.swift
│ ├── Mock-JSON.swift
│ └── MockURLResponseValidator.swift
│ ├── Util
│ ├── Performers
│ │ ├── Mocks
│ │ │ ├── MockURLSessionDataTask.swift
│ │ │ └── MockRequestPerformerURLSession.swift
│ │ └── RequestPerformable_asyncAwait_Tests.swift
│ ├── Uploader
│ │ ├── Mocks
│ │ │ ├── MockURLSessionUploadTask.swift
│ │ │ └── MockUploadTaskInterceptor.swift
│ │ ├── Helpers
│ │ │ └── DefaultUploadTaskInterceptor.swift
│ │ ├── DataUploader
│ │ │ └── Mocks
│ │ │ │ └── MockDataUploaderURLSession.swift
│ │ └── FileUploader
│ │ │ └── Mocks
│ │ │ └── MockFileUploaderURLSession.swift
│ ├── Downloader
│ │ ├── Mocks
│ │ │ ├── MockURLSessionDownloadTask.swift
│ │ │ ├── FileDownloader_MockDownloadTaskInterceptor.swift
│ │ │ └── MockFileDownloaderURLSession.swift
│ │ └── Helpers
│ │ │ └── DefaultDownloadTaskInterceptorTests.swift
│ └── WebSocket
│ │ ├── Mocks
│ │ ├── MockWebSockerURLSession.swift
│ │ ├── MockWebSocketTaskInterceptor.swift
│ │ └── MockURLSessionWebSocketTask.swift
│ │ └── Helpers
│ │ ├── WebSocketConnectionStateTests.swift
│ │ └── DefaultWebSocketTaskInterceptorTests.swift
│ ├── HTTP
│ ├── Parameters
│ │ └── HTTPParameterTests.swift
│ ├── Body
│ │ ├── HTTPBodyTests.swift
│ │ └── MultipartFormData
│ │ │ └── MultipartFormPartTests.swift
│ ├── Headers
│ │ └── HTTPHeaderTests.swift
│ └── Methods
│ │ └── HTTPMethodTests.swift
│ ├── Error
│ ├── HTTPError
│ │ ├── StatusCodeTypes
│ │ │ ├── HTTPInformationalStatusTests.swift
│ │ │ ├── HTTPRedirectionStatusTests.swift
│ │ │ ├── HTTPSuccessStatusTests.swift
│ │ │ ├── HTTPServerErrorStatusTests.swift
│ │ │ └── HTTPClientErrorStatusTests.swift
│ │ └── HTTPErrorTests.swift
│ ├── InternalError
│ │ └── InternalErrorTests.swift
│ └── WebSocketError
│ │ └── WebSocketErrorTests.swift
│ ├── Encoders
│ ├── HTTPParameterEncoderTests.swift
│ └── HTTPHeaderEncoderTests.swift
│ ├── Decoding
│ └── RequestDecoderTests.swift
│ ├── Other
│ └── Extensions
│ │ └── String-extTests.swift
│ ├── Request
│ ├── RequestBuilderTests.swift
│ ├── RequestFactoryTests.swift
│ └── RequestTests.swift
│ ├── Interceptors
│ └── SessoionDelegate+Extensions+Tests
│ │ ├── SessionDelegate+URLSessionDownloadDelegate+Tests.swift
│ │ ├── SessionDelegate+URLSessionDelegate+Tests.swift
│ │ ├── SessionDelegate+URLSessionStreamDelegate+Tests.swift
│ │ ├── SessionDelegate+URLSessionWebSocketDelegate+Tests.swift
│ │ ├── SessionDelegate+URLSessionDataDelegate+Tests.swift
│ │ └── SessionDelegate+URLSessionTaskDelegate+Tests.swift
│ └── Validator
│ └── ResponseValidatorTests.swift
├── .swiftpm
└── xcode
│ ├── package.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ └── IDEWorkspaceChecks.plist
│ └── xcuserdata
│ └── albertodominguez.xcuserdatad
│ └── xcschemes
│ └── xcschememanagement.plist
├── .gitignore
├── Package.swift
└── LICENSE
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Aldo10012/EZNetworking/HEAD/.DS_Store
--------------------------------------------------------------------------------
/Sources/EZNetworking/Other/EmptyResponse.swift:
--------------------------------------------------------------------------------
1 | public struct EmptyResponse: Decodable {
2 | public init() { }
3 | }
4 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Mocks/Mock-Codeable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | struct Person: Codable {
4 | var name: String
5 | var age: Int
6 | }
7 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/Helpers/UploadStreamEvent.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum UploadStreamEvent {
4 | case progress(Double)
5 | case success(Data)
6 | case failure(NetworkingError)
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/Helpers/UploadTypealias.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias UploadProgressHandler = (Double) -> Void
4 | public typealias UploadCompletionHandler = (Result) -> Void
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Ignore a specific file
2 |
3 | .swiftpm/xcode/package.xcworkspace/xcuserdata/albertodominguez.xcuserdatad/UserInterfaceState.xcuserstate
4 |
5 | .swiftpm/xcode/xcuserdata/albertodominguez.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist
6 |
7 | .build/
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Body/HTTPBody.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public typealias HTTPBody = DataConvertible
4 |
5 | public protocol DataConvertible {
6 | func toData() -> Data?
7 | }
8 |
9 | extension Data: DataConvertible {
10 | public func toData() -> Data? { self }
11 | }
12 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Other/Extensions/String-ext.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension String {
4 | /// generate random boundary for multi-part-form data
5 | public static func getRandomMultiPartFormBoundary() -> String {
6 | return "EZNetworking.Boundary.\(UUID().uuidString)"
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Parameters/HTTPParameters.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct HTTPParameter: Equatable {
4 | let key: String
5 | let value: String
6 |
7 | public init(key: String, value: String) {
8 | self.key = key
9 | self.value = value
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/MetricsInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting URL metrics collection.
4 | public protocol MetricsInterceptor: AnyObject {
5 | /// Intercepts metrics after task completion.
6 | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics)
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/CacheInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting URL cache operations
4 | public protocol CacheInterceptor: AnyObject {
5 | /// Intercepts cache responses before they are cached for a specific data task.
6 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, willCacheResponse proposedResponse: CachedURLResponse) async -> CachedURLResponse?
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/RedirectInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting URL redirect operations.
4 | public protocol RedirectInterceptor: AnyObject {
5 | /// Intercepts URL redirection before the request is performed.
6 | func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest?
7 | }
8 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/UploadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting upload tasks specifically.
4 | public protocol UploadTaskInterceptor: AnyObject {
5 | /// Track the progress of the upload process
6 | var progress: (Double) -> Void { get set }
7 |
8 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64)
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/StatusCodeTypes/HTTPInformationalStatus.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum HTTPInformationalStatus: Error {
4 | static func description(from statusCode: Int) -> String {
5 | return HTTPInformationalStatus.descriptions[statusCode] ?? "Unknown Informational Status"
6 | }
7 |
8 | private static let descriptions: [Int: String] = [
9 | 100: "Continue",
10 | 101: "Switching Protocols",
11 | 102: "Processing"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Performers/Mocks/MockURLSessionDataTask.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockURLSessionDataTask: URLSessionDataTask {
5 | private let closure: () -> Void
6 | var didCancel: Bool = false
7 |
8 | init(closure: @escaping () -> Void) {
9 | self.closure = closure
10 | }
11 |
12 | override func resume() {
13 | closure()
14 | }
15 |
16 | override func cancel() {
17 | didCancel = true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Uploader/Mocks/MockURLSessionUploadTask.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockURLSessionUploadTask: URLSessionUploadTask {
5 | private let closure: () -> Void
6 | var didCancel: Bool = false
7 |
8 | init(closure: @escaping () -> Void) {
9 | self.closure = closure
10 | }
11 |
12 | override func resume() {
13 | closure()
14 | }
15 |
16 | override func cancel() {
17 | didCancel = true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Downloader/Mocks/MockURLSessionDownloadTask.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockURLSessionDownloadTask: URLSessionDownloadTask {
5 | private let closure: () -> Void
6 | var didCancel: Bool = false
7 |
8 | init(closure: @escaping () -> Void) {
9 | self.closure = closure
10 | }
11 |
12 | override func resume() {
13 | closure()
14 | }
15 |
16 | override func cancel() {
17 | didCancel = true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Encoders/HTTPHeaderEncoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol HTTPHeaderEncoder {
4 | func encodeHeaders(for urlRequest: inout URLRequest, with headers: [HTTPHeader])
5 | }
6 |
7 | public struct HTTPHeaderEncoderImpl: HTTPHeaderEncoder {
8 | public init() {}
9 |
10 | public func encodeHeaders(for urlRequest: inout URLRequest, with headers: [HTTPHeader]) {
11 | for header in headers {
12 | urlRequest.setValue(header.value, forHTTPHeaderField: header.key)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Performers/Protocols/RequestPerformable.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public protocol RequestPerformable {
5 | func perform(request: Request, decodeTo decodableObject: T.Type) async throws -> T
6 | func performTask(request: Request, decodeTo decodableObject: T.Type, completion: @escaping((Result) -> Void)) -> URLSessionDataTask?
7 | func performPublisher(request: Request, decodeTo decodableObject: T.Type) -> AnyPublisher
8 | }
9 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/HTTP/Parameters/HTTPParameterTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPParameter")
5 | final class HTTPParameterTests {
6 |
7 | @Test("test HTTPParameter .key and .value")
8 | func testHTTPParameterKeyAndValue() {
9 | let key = "param_key"
10 | let value = "param_value"
11 | let parameter = HTTPParameter(key: key, value: value)
12 |
13 | #expect(parameter.key == key)
14 | #expect(parameter.value == value)
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Decoding/RequestDecoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol RequestDecodable {
4 | func decode(_ type: T.Type, from data: Data) throws -> T
5 | }
6 |
7 | public struct RequestDecoder: RequestDecodable {
8 | public init() {}
9 | public func decode(_ type: T.Type, from data: Data) throws -> T {
10 | do {
11 | return try JSONDecoder().decode(T.self, from: data)
12 | } catch {
13 | throw NetworkingError.internalError(.couldNotParse)
14 | }
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Uploader/Mocks/MockUploadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockUploadTaskInterceptor: UploadTaskInterceptor {
5 | var progress: (Double) -> Void
6 | init(progress: @escaping (Double) -> Void) {
7 | self.progress = progress
8 | }
9 |
10 | var didCallDidSendBodyData = false
11 |
12 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
13 | didCallDidSendBodyData = true
14 | progress(1)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/HTTP/Body/HTTPBodyTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test HTTPBody")
6 | class HTTPBodyTests {
7 |
8 | @Test("test Data is HTTPBody")
9 | func testDataIsHTTPBody() {
10 | #expect(Data() is HTTPBody)
11 | }
12 |
13 | @Test("test Data is DataConvertible")
14 | func testDataIsDataConvertible() {
15 | #expect(Data() is DataConvertible)
16 | }
17 |
18 | @Test("test Data is equal to data.toData()")
19 | func testDataIsEqualToDataToData() {
20 | let data = Data()
21 | #expect(data == data.toData())
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/StatusCodeTypes/HTTPRedirectionStatus.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum HTTPRedirectionStatus: Error {
4 | static func description(from statusCode: Int) -> String {
5 | return HTTPRedirectionStatus.descriptions[statusCode] ?? "Unknown Redirection Status"
6 | }
7 |
8 | private static let descriptions: [Int: String] = [
9 | 300: "Multiple Choices",
10 | 301: "Moved Permanently",
11 | 302: "Found",
12 | 303: "See Other",
13 | 304: "Not Modified",
14 | 305: "Use Proxy",
15 | 307: "Temporary Redirect",
16 | 308: "Permanent Redirect"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Methods/HTTPMethod.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Standard HTTP Methods
4 | public enum HTTPMethod: String, CaseIterable {
5 | /// Retrieve data from server
6 | case GET
7 | /// Submit data to server
8 | case POST
9 | /// Update existing resource
10 | case PUT
11 | /// Delete a resource
12 | case DELETE
13 | /// Update partial resource
14 | case PATCH
15 | /// Get headers only (without body)
16 | case HEAD
17 | /// Get available methods for a resource
18 | case OPTIONS
19 | /// Trace request path
20 | case TRACE
21 | /// Connect to server (for proxies)
22 | case CONNECT
23 | }
24 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/DataUploader/Protocols/DataUploadable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 |
4 | public protocol DataUploadable {
5 | func uploadData(_ data: Data, with request: Request, progress: UploadProgressHandler?) async throws -> Data
6 | func uploadDataTask(_ data: Data, with request: Request, progress: UploadProgressHandler?, completion: @escaping(UploadCompletionHandler)) -> URLSessionUploadTask?
7 | func uploadDataPublisher(_ data: Data, with request: Request, progress: UploadProgressHandler?) -> AnyPublisher
8 | func uploadDataStream(_ data: Data, with request: Request) -> AsyncStream
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/TaskLifecycleInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting task lifecycle events.
4 | public protocol TaskLifecycleInterceptor: AnyObject {
5 | /// Intercepts when a task completes with or without an error.
6 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?)
7 |
8 | /// Intercepts when a task is waiting for connectivity.
9 | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask)
10 |
11 | /// Intercepts when a task is created.
12 | func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask)
13 | }
14 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/FileUploader/Protocols/FileUploadable.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 |
4 | public protocol FileUploadable {
5 | func uploadFile(_ fileURL: URL, with request: Request, progress: UploadProgressHandler?) async throws -> Data
6 | func uploadFileTask(_ fileURL: URL, with request: Request, progress: UploadProgressHandler?, completion: @escaping(UploadCompletionHandler)) -> URLSessionUploadTask?
7 | func uploadFilePublisher(_ fileURL: URL, with request: Request, progress: UploadProgressHandler?) -> AnyPublisher
8 | func uploadFileStream(_ fileURL: URL, with request: Request) -> AsyncStream
9 | }
10 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionDelegate {
4 | public func urlSession(_ session: URLSession,
5 | didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
6 | await authenticationInterceptor?.urlSession(session, didReceive: challenge) ?? (.performDefaultHandling, nil)
7 | }
8 |
9 | public func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
10 | taskLifecycleInterceptor?.urlSession(session, didCreateTask: task)
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/StatusCodeTypes/HTTPSuccessStatus.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum HTTPSuccessStatus: Error {
4 | static func description(from statusCode: Int) -> String {
5 | return HTTPSuccessStatus.descriptions[statusCode] ?? "Unknown Success Status"
6 | }
7 |
8 | private static let descriptions: [Int: String] = [
9 | 200: "OK",
10 | 201: "Created",
11 | 202: "Accepted",
12 | 203: "Non-Authoritative Information",
13 | 204: "No Content",
14 | 205: "Reset Content",
15 | 206: "Partial Content",
16 | 207: "Multi-Status",
17 | 208: "Already Reported",
18 | 226: "IM Used"
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/Helpers/DefaultUploadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal class DefaultUploadTaskInterceptor: UploadTaskInterceptor {
4 | var progress: (Double) -> Void
5 |
6 | init(progress: @escaping (Double) -> Void = { _ in }) {
7 | self.progress = progress
8 | }
9 |
10 | func urlSession(_ session: URLSession, task: URLSessionTask, didSendBodyData bytesSent: Int64, totalBytesSent: Int64, totalBytesExpectedToSend: Int64) {
11 | guard totalBytesExpectedToSend > 0 else {
12 | return
13 | }
14 | let currentProgress = Double(totalBytesSent) / Double(totalBytesExpectedToSend)
15 | progress(currentProgress)
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/AuthenticationInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting and handling URL authentication challenges.
4 | public protocol AuthenticationInterceptor: AnyObject {
5 | /// Intercepts authentication challenges for a specific URLSession task.
6 | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
7 |
8 | /// Intercepts authentication challenges for the entire URLSession.
9 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?)
10 | }
11 |
--------------------------------------------------------------------------------
/.swiftpm/xcode/xcuserdata/albertodominguez.xcuserdatad/xcschemes/xcschememanagement.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | SchemeUserState
6 |
7 | EZNetworking.xcscheme_^#shared#^_
8 |
9 | orderHint
10 | 0
11 |
12 |
13 | SuppressBuildableAutocreation
14 |
15 | EZNetworking
16 |
17 | primary
18 |
19 |
20 | EZNetworkingTests
21 |
22 | primary
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/StatusCodeTypes/HTTPInformationalStatusTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPInformationalStatus")
5 | final class HTTPInformationalStatusTests {
6 |
7 | @Test("test HTTPInformationalStatus maps status code to description", arguments: zip(map.keys, map.values))
8 | func testHTTPInformationalStatusMapsStatusCodeToDescription(statusCode: Int, description: String) {
9 | #expect(HTTPInformationalStatus.description(from: statusCode) == description)
10 | }
11 |
12 | private static let map: [Int: String] = [
13 | 100: "Continue",
14 | 101: "Switching Protocols",
15 | 102: "Processing",
16 | -1: "Unknown Informational Status"
17 | ]
18 | }
19 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/StatusCodeTypes/HTTPServerErrorStatus.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum HTTPServerErrorStatus: Error {
4 | static func description(from statusCode: Int) -> String {
5 | return HTTPServerErrorStatus.descriptions[statusCode] ?? "Unknown Server Error"
6 | }
7 |
8 | private static let descriptions: [Int: String] = [
9 | 500: "Internal Server Error",
10 | 501: "Not Implemented",
11 | 502: "Bad Gateway",
12 | 503: "Service Unavailable",
13 | 504: "Gateway Timeout",
14 | 505: "HTTP Version Not Supported",
15 | 506: "Variant Also Negotiates",
16 | 507: "Insufficient Storage",
17 | 508: "Loop Detected",
18 | 510: "Not Extended",
19 | 511: "Network Authentication Required"
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Other/WebSocketTaskProtocol.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | // protocol to allow unit testing and mocking for URLSessionWebSocketTask
4 | public protocol WebSocketTaskProtocol {
5 | func resume()
6 | func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
7 | func sendPing(pongReceiveHandler: @escaping @Sendable ((any Error)?) -> Void)
8 |
9 | func send(_ message: URLSessionWebSocketTask.Message) async throws
10 | func send(_ message: URLSessionWebSocketTask.Message, completionHandler: @escaping @Sendable (Error?) -> Void)
11 |
12 | func receive() async throws -> URLSessionWebSocketTask.Message
13 | func receive(completionHandler: @escaping @Sendable (Result) -> Void)
14 | }
15 |
16 | extension URLSessionWebSocketTask: WebSocketTaskProtocol {}
17 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Encoders/HTTPParameterEncoder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol HTTPParameterEncoder {
4 | func encodeParameters(for urlRequest: inout URLRequest, with parameters: [HTTPParameter]) throws
5 | }
6 |
7 | public struct HTTPParameterEncoderImpl: HTTPParameterEncoder {
8 | public init() {}
9 | public func encodeParameters(for urlRequest: inout URLRequest, with parameters: [HTTPParameter]) throws {
10 | guard let url = urlRequest.url else {
11 | throw NetworkingError.internalError(.noURL)
12 | }
13 |
14 | if var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false), !parameters.isEmpty {
15 | urlComponents.queryItems = parameters.map { URLQueryItem(name: $0.key, value: $0.value) }
16 | urlRequest.url = urlComponents.url
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/NetworkingError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum NetworkingError: Error {
4 | case internalError(InternalError) /// any internal error
5 | case httpError(HTTPError) /// any HRRP error
6 | case urlError(URLError) /// any URL error
7 | }
8 |
9 | extension NetworkingError: Equatable {
10 | public static func ==(lhs: NetworkingError, rhs: NetworkingError) -> Bool {
11 | switch (lhs, rhs) {
12 | case let (.internalError(error1), .internalError(error2)):
13 | return error1 == error2
14 |
15 | case let (.httpError(error1), .httpError(error2)):
16 | return error1.statusCode == error2.statusCode
17 |
18 | case let (.urlError(error), .urlError(error2)):
19 | return error == error2
20 |
21 | default:
22 | return false
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionWebSocketDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionWebSocketDelegate {
4 | public func urlSession(_ session: URLSession,
5 | webSocketTask: URLSessionWebSocketTask,
6 | didOpenWithProtocol protocol: String?) {
7 | webSocketTaskInterceptor?.urlSession(session, webSocketTask: webSocketTask, didOpenWithProtocol: `protocol`)
8 | }
9 |
10 | public func urlSession(_ session: URLSession,
11 | webSocketTask: URLSessionWebSocketTask,
12 | didCloseWith closeCode: URLSessionWebSocketTask.CloseCode,
13 | reason: Data?) {
14 | webSocketTaskInterceptor?.urlSession(session, webSocketTask: webSocketTask, didCloseWith: closeCode, reason: reason)
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/StreamTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting stream tasks specifically.
4 | public protocol StreamTaskInterceptor: AnyObject {
5 | /// Intercepts when a stream task's read is closed.
6 | func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask)
7 |
8 | /// Intercepts when a stream task's write is closed.
9 | func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask)
10 |
11 | /// Intercepts when a better route is discovered for a stream task.
12 | func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask)
13 |
14 | /// Intercepts when a stream task becomes input and output streams.
15 | func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream)
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Downloader/Protocols/FileDownloadable.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public typealias DownloadProgressHandler = (Double) -> Void
5 | public typealias DownloadCompletionHandler = (Result) -> Void
6 |
7 | public protocol FileDownloadable {
8 | func downloadFile(from serverUrl: URL, progress: DownloadProgressHandler?) async throws -> URL
9 | func downloadFileTask(from serverUrl: URL, progress: DownloadProgressHandler?, completion: @escaping(DownloadCompletionHandler)) -> URLSessionDownloadTask
10 | func downloadFilePublisher(from serverUrl: URL, progress: DownloadProgressHandler?) -> AnyPublisher
11 | func downloadFileStream(from serverUrl: URL) -> AsyncStream
12 | }
13 |
14 | public enum DownloadStreamEvent {
15 | case progress(Double)
16 | case success(URL)
17 | case failure(NetworkingError)
18 | }
19 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/StatusCodeTypes/HTTPRedirectionStatusTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPRedirectionStatus")
5 | final class HTTPRedirectionStatusTests {
6 |
7 | @Test("test HTTPRedirectionStatus maps status code to description", arguments: zip(map.keys, map.values))
8 | func testHTTPRedirectionStatusMapsStatusCodeToDescription(statusCode: Int, description: String) {
9 | #expect(HTTPRedirectionStatus.description(from: statusCode) == description)
10 | }
11 |
12 | private static let map: [Int: String] = [
13 | 300: "Multiple Choices",
14 | 301: "Moved Permanently",
15 | 302: "Found",
16 | 303: "See Other",
17 | 304: "Not Modified",
18 | 305: "Use Proxy",
19 | 307: "Temporary Redirect",
20 | 308: "Permanent Redirect",
21 | -1: "Unknown Redirection Status"
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/StatusCodeTypes/HTTPSuccessStatusTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPSuccessStatus")
5 | final class HTTPSuccessStatusTests {
6 |
7 | @Test("test HTTPSuccessStatus maps status code to description", arguments: zip(map.keys, map.values))
8 | func testHTTPSuccessStatusMapsStatusCodeToDescription(statusCode: Int, description: String) {
9 | #expect(HTTPSuccessStatus.description(from: statusCode) == description)
10 | }
11 |
12 | private static let map: [Int: String] = [
13 | 200: "OK",
14 | 201: "Created",
15 | 202: "Accepted",
16 | 203: "Non-Authoritative Information",
17 | 204: "No Content",
18 | 205: "Reset Content",
19 | 206: "Partial Content",
20 | 207: "Multi-Status",
21 | 208: "Already Reported",
22 | 226: "IM Used",
23 | -1: "Unknown Success Status"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/WebSocket/Helpers/DefaultWebSocketTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | internal class DefaultWebSocketTaskInterceptor: WebSocketTaskInterceptor {
4 | var onEvent: ((WebSocketTaskEvent) -> Void)?
5 |
6 | init(onEvent: ((WebSocketTaskEvent) -> Void)? = nil) {
7 | self.onEvent = onEvent
8 | }
9 |
10 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
11 | onEvent?(.didOpenWithProtocol(protocolStr: `protocol`))
12 | }
13 |
14 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: any Error) {
15 | onEvent?(.didOpenWithError(error: error))
16 | }
17 |
18 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
19 | onEvent?(.didClose(code: closeCode, reason: reason))
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/DownloadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting download tasks specifically.
4 | public protocol DownloadTaskInterceptor: AnyObject {
5 | /// Track the progress of the download process
6 | var progress: (Double) -> Void { get set }
7 |
8 | /// Intercepts when a download task finishes downloading to a location.
9 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL)
10 |
11 | /// Intercepts progress updates during the download task.
12 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)
13 |
14 | /// Intercepts when a download task is resumed.
15 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64)
16 | }
17 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/DataTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting data tasks specifically.
4 | public protocol DataTaskInterceptor: AnyObject {
5 | /// Intercepts data received for a specific data task.
6 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data)
7 |
8 | /// Intercepts when a data task transitions to a download task.
9 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask)
10 |
11 | /// Intercepts when a data task transitions to a stream task.
12 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome streamTask: URLSessionStreamTask)
13 |
14 | /// Intercepts the response for a data task before it is processed.
15 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition
16 | }
17 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Encoders/HTTPParameterEncoderTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test HTTPParameterEncoderImpl")
6 | final class HTTPParameterEncoderTests {
7 | private let sut = HTTPParameterEncoderImpl()
8 |
9 | @Test("test URL Query Parameters Are Added")
10 | func testURLQueryParametersAreAdded() throws {
11 | let url = try #require(URL(string: "https://www.example.com"))
12 | var urlRequest = URLRequest(url: url)
13 |
14 | try sut.encodeParameters(for: &urlRequest, with: [
15 | HTTPParameter(key: "key_1", value: "value_1"),
16 | HTTPParameter(key: "key_2", value: "value_2"),
17 | HTTPParameter(key: "key_3", value: "value_3")
18 | ])
19 |
20 | let expectedURL = "https://www.example.com?key_1=value_1&key_2=value_2&key_3=value_3"
21 | #expect(urlRequest.url?.absoluteString == expectedURL)
22 | }
23 |
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Mocks/Mock-JSON.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | @testable import EZNetworking
3 |
4 | struct MockData {
5 | static var mockPersonJsonData: Data {
6 | let jsonString = """
7 | {
8 | "name": "John",
9 | "age": 30
10 | }
11 | """
12 | return Data(jsonString: jsonString)!
13 | }
14 |
15 | static var invalidMockPersonJsonData: Data {
16 | let jsonString = """
17 | {
18 | "Name": "John",
19 | "Age": 30
20 | }
21 | """
22 | return Data(jsonString: jsonString)!
23 | }
24 |
25 | static func imageUrlData(from imageUrlString: String) -> Data? {
26 | guard let url = URL(string: imageUrlString) else {
27 | return nil
28 | }
29 | do {
30 | let data = try Data(contentsOf: url)
31 | return data
32 | } catch {
33 | return nil
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Package.swift:
--------------------------------------------------------------------------------
1 | // swift-tools-version: 5.9
2 | // The swift-tools-version declares the minimum version of Swift required to build this package.
3 |
4 | import PackageDescription
5 |
6 | let package = Package(
7 | name: "EZNetworking",
8 | platforms: [
9 | .iOS(.v15) // Set the minimum iOS version here
10 | ],
11 | products: [
12 | // Products define the executables and libraries a package produces, making them visible to other packages.
13 | .library(
14 | name: "EZNetworking",
15 | targets: ["EZNetworking"]),
16 | ],
17 | targets: [
18 | // Targets are the basic building blocks of a package, defining a module or a test suite.
19 | // Targets can depend on other targets in this package and products from dependencies.
20 | .target(
21 | name: "EZNetworking"),
22 | .testTarget(
23 | name: "EZNetworkingTests",
24 | dependencies: ["EZNetworking"]),
25 | ]
26 | )
27 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Downloader/Mocks/FileDownloader_MockDownloadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class FileDownloader_MockDownloadTaskInterceptor: DownloadTaskInterceptor {
5 | var progress: (Double) -> Void
6 |
7 | init(progress: @escaping (Double) -> Void) {
8 | self.progress = progress
9 | }
10 |
11 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
12 | progress(1)
13 | }
14 |
15 | var didCallDidWriteData = false
16 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
17 | didCallDidWriteData = true
18 | progress(1)
19 | }
20 |
21 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
22 | progress(1)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Encoders/HTTPHeaderEncoderTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test HTTPHeaderEncoderImpl")
6 | final class HTTPHeaderEncoderTests {
7 | private let sut = HTTPHeaderEncoderImpl()
8 |
9 | @Test("test allHTTPHeaderFields is set to injected headers")
10 | func testAllHTTPHeaderFieldsIsSetToInjectedHeaders() throws {
11 | let url = try #require(URL(string: "https://www.example.com"))
12 | var urlRequest = URLRequest(url: url)
13 |
14 | sut.encodeHeaders(for: &urlRequest, with: [
15 | .accept(.json),
16 | .contentType(.json),
17 | .authorization(.bearer("My_API_KEY"))
18 | ])
19 |
20 | let expextedHeaders = [
21 | "Accept": "application/json",
22 | "Content-Type": "application/json",
23 | "Authorization": "Bearer My_API_KEY"
24 | ]
25 | #expect(urlRequest.allHTTPHeaderFields == expextedHeaders)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/StatusCodeTypes/HTTPServerErrorStatusTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPServerErrorStatus")
5 | final class HTTPServerErrorStatusTests {
6 |
7 | @Test("test HTTPServerErrorStatus maps status code to description", arguments: zip(map.keys, map.values))
8 | func testHTTPServerErrorStatusMapsStatusCodeToDescription(statusCode: Int, description: String) {
9 | #expect(HTTPServerErrorStatus.description(from: statusCode) == description)
10 | }
11 |
12 | private static let map: [Int: String] = [
13 | 500: "Internal Server Error",
14 | 501: "Not Implemented",
15 | 502: "Bad Gateway",
16 | 503: "Service Unavailable",
17 | 504: "Gateway Timeout",
18 | 505: "HTTP Version Not Supported",
19 | 506: "Variant Also Negotiates",
20 | 507: "Insufficient Storage",
21 | 508: "Loop Detected",
22 | 510: "Not Extended",
23 | 511: "Network Authentication Required",
24 | -1: "Unknown Server Error"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Decoding/RequestDecoderTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test RequestDecoder")
5 | final class RequestDecoderTests {
6 | private let sut = RequestDecoder()
7 |
8 | @Test("can decode valid mock JSON into Decodable object")
9 | func canDecodeValidMockJSONIntoDecodableObject() throws {
10 | do {
11 | let person = try sut.decode(Person.self, from: MockData.mockPersonJsonData)
12 | #expect(person.name == "John")
13 | #expect(person.age == 30)
14 | } catch {
15 | Issue.record("Unexpected error)")
16 | }
17 | }
18 |
19 | @Test("thorws error if tries to decode invalid mock json")
20 | func throwsErrorIfTriesToDecodeInvalidMockJSON() throws {
21 | do {
22 | _ = try sut.decode(Person.self, from: MockData.invalidMockPersonJsonData)
23 | Issue.record("Unexpected error)")
24 | } catch let error as NetworkingError {
25 | #expect(error == NetworkingError.internalError(.couldNotParse))
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/Protocols/WebSocketTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Protocol for intercepting WebSocket tasks specifically.
4 | public protocol WebSocketTaskInterceptor: AnyObject {
5 | var onEvent: ((WebSocketTaskEvent) -> Void)? { get set }
6 |
7 | /// Intercepts when a WebSocket task opens with a protocol.
8 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?)
9 |
10 | /// Intercepts when a WebSocket task closes with a code and reason.
11 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?)
12 |
13 | /// Intercepts when a task completes with an error.
14 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error)
15 | }
16 |
17 | public enum WebSocketTaskEvent {
18 | case didOpenWithProtocol(protocolStr: String?)
19 | case didOpenWithError(error: Error)
20 | case didClose(code: URLSessionWebSocketTask.CloseCode, reason: Data?)
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/WebSocket/Helpers/WebSocketConnectionState.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum WebSocketConnectionState: Equatable {
4 | case idle
5 | case disconnected
6 | case connecting
7 | case connected(protocol: String?)
8 | case connectionLost(reason: WebSocketError)
9 | case failed(error: WebSocketError)
10 |
11 | public static func == (lhs: WebSocketConnectionState, rhs: WebSocketConnectionState) -> Bool {
12 | switch (lhs, rhs) {
13 | case (.idle, .idle):
14 | return true
15 | case (.disconnected, .disconnected):
16 | return true
17 | case (.connecting, .connecting):
18 | return true
19 | case (.connected(let lhsProto), .connected(let rhsProto)):
20 | return lhsProto == rhsProto
21 | case (.connectionLost(let lhsError), .connectionLost(let rhsError)):
22 | return lhsError == rhsError
23 | case (.failed(let lhsError), .failed(let rhsError)):
24 | return lhsError == rhsError
25 | default:
26 | return false
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Alberto Dominguez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Mocks/MockURLResponseValidator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | struct MockURLResponseValidator: ResponseValidator {
5 | var throwError: NetworkingError? = nil
6 |
7 | func validateNoError(_ error: (any Error)?) throws {
8 | if let throwError = throwError {
9 | throw throwError
10 | }
11 | }
12 |
13 | func validateStatus(from urlResponse: URLResponse?) throws {
14 | if let throwError = throwError {
15 | throw throwError
16 | }
17 | }
18 |
19 | func validateData(_ data: Data?) throws -> Data {
20 | if let throwError = throwError {
21 | throw throwError
22 | }
23 | guard let data else {
24 | throw NetworkingError.internalError(.noData)
25 | }
26 | return data
27 | }
28 |
29 | func validateUrl(_ url: URL?) throws -> URL {
30 | if let throwError = throwError {
31 | throw throwError
32 | }
33 | guard let url else {
34 | throw NetworkingError.internalError(.noURL)
35 | }
36 | return url
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Other/URLSessionTaskProtocol.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol URLSessionTaskProtocol {
4 |
5 | func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask
6 |
7 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask
8 |
9 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask
10 |
11 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask
12 |
13 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol
14 | }
15 |
16 | extension URLSession: URLSessionTaskProtocol {
17 | public func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
18 | let task: URLSessionWebSocketTask = self.webSocketTask(with: request)
19 | return task as WebSocketTaskProtocol
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Downloader/Helpers/DefaultDownloadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | /// Default implementation of DownloadTaskInterceptor
5 | internal class DefaultDownloadTaskInterceptor: DownloadTaskInterceptor {
6 | var progress: (Double) -> Void
7 |
8 | init(progress: @escaping (Double) -> Void = { _ in }) {
9 | self.progress = progress
10 | }
11 |
12 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
13 | progress(1.0)
14 | }
15 |
16 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
17 | let currentProgress = Double(totalBytesWritten) / Double(totalBytesExpectedToWrite)
18 | progress(currentProgress)
19 | }
20 |
21 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
22 | let currentProgress = Double(fileOffset) / Double(expectedTotalBytes)
23 | progress(currentProgress)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/InternalError/InternalErrorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test InternalError")
5 | final class InternalErrorTests {
6 |
7 | @Test("test InternalError Is Equatable", arguments: zip(InternalErrorList, InternalErrorList))
8 | func testCouldNotParseIsEquatable(inputA: InternalError, inputB: InternalError) {
9 | #expect(inputA == inputB)
10 | }
11 |
12 | @Test("test Different InternalError Are Not Equatable")
13 | func testDifferentInternalErrorAreNotEquatable() {
14 | #expect(InternalError.unknown != InternalError.couldNotParse)
15 | }
16 |
17 | private static let InternalErrorList: [InternalError] = [
18 | InternalError.noURL,
19 | InternalError.couldNotParse,
20 | InternalError.invalidError,
21 | InternalError.noData,
22 | InternalError.noResponse,
23 | InternalError.requestFailed(NetworkingError.httpError(.init(statusCode: 400, headers: [:]))),
24 | InternalError.noRequest,
25 | InternalError.noHTTPURLResponse,
26 | InternalError.invalidImageData,
27 | InternalError.lostReferenceOfSelf,
28 | InternalError.unknown,
29 | ]
30 | }
31 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionStreamDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionStreamDelegate {
4 | public func urlSession(_ session: URLSession,
5 | readClosedFor streamTask: URLSessionStreamTask) {
6 | streamTaskInterceptor?.urlSession(session, readClosedFor: streamTask)
7 | }
8 |
9 | public func urlSession(_ session: URLSession,
10 | writeClosedFor streamTask: URLSessionStreamTask) {
11 | streamTaskInterceptor?.urlSession(session, writeClosedFor: streamTask)
12 | }
13 |
14 | public func urlSession(_ session: URLSession,
15 | betterRouteDiscoveredFor streamTask: URLSessionStreamTask) {
16 | streamTaskInterceptor?.urlSession(session, betterRouteDiscoveredFor: streamTask)
17 | }
18 |
19 | public func urlSession(_ session: URLSession,
20 | streamTask: URLSessionStreamTask,
21 | didBecome inputStream: InputStream,
22 | outputStream: OutputStream) {
23 | streamTaskInterceptor?.urlSession(session, streamTask: streamTask, didBecome: inputStream, outputStream: outputStream)
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/InternalError/InternalError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum InternalError: Error {
4 | case noURL
5 | case couldNotParse
6 | case invalidError
7 | case noData
8 | case noResponse
9 | case requestFailed(Error)
10 | case noRequest
11 | case noHTTPURLResponse
12 | case invalidImageData
13 | case lostReferenceOfSelf
14 | case unknown
15 | }
16 |
17 | extension InternalError: Equatable {
18 | public static func ==(lhs: InternalError, rhs: InternalError) -> Bool {
19 | switch (lhs, rhs) {
20 | case (.unknown, .unknown),
21 | (.noURL, .noURL),
22 | (.couldNotParse, .couldNotParse),
23 | (.invalidError, .invalidError),
24 | (.noData, .noData),
25 | (.noResponse, .noResponse),
26 | (.noRequest, .noRequest),
27 | (.noHTTPURLResponse, .noHTTPURLResponse),
28 | (.invalidImageData, .invalidImageData),
29 | (.lostReferenceOfSelf, .lostReferenceOfSelf):
30 | return true
31 |
32 | case let (.requestFailed(lhsError), .requestFailed(rhsError)):
33 | return (lhsError as NSError) == (rhsError as NSError)
34 |
35 | default:
36 | return false
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionDownloadDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionDownloadDelegate {
4 | public func urlSession(_ session: URLSession,
5 | downloadTask: URLSessionDownloadTask,
6 | didFinishDownloadingTo location: URL) {
7 | downloadTaskInterceptor?.urlSession(session, downloadTask: downloadTask, didFinishDownloadingTo: location)
8 | }
9 |
10 | public func urlSession(_ session: URLSession,
11 | downloadTask: URLSessionDownloadTask,
12 | didWriteData bytesWritten: Int64,
13 | totalBytesWritten: Int64,
14 | totalBytesExpectedToWrite: Int64) {
15 | downloadTaskInterceptor?.urlSession(session, downloadTask: downloadTask, didWriteData: bytesWritten, totalBytesWritten: totalBytesWritten, totalBytesExpectedToWrite: totalBytesExpectedToWrite)
16 | }
17 |
18 | public func urlSession(_ session: URLSession,
19 | downloadTask: URLSessionDownloadTask,
20 | didResumeAtOffset fileOffset: Int64,
21 | expectedTotalBytes: Int64) {
22 | downloadTaskInterceptor?.urlSession(session, downloadTask: downloadTask, didResumeAtOffset: fileOffset, expectedTotalBytes: expectedTotalBytes)
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/StatusCodeTypes/HTTPClientErrorStatus.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | enum HTTPClientErrorStatus: Error {
4 | static func description(from statusCode: Int) -> String {
5 | HTTPClientErrorStatus.descriptions[statusCode] ?? "Unknown Client Error"
6 | }
7 |
8 | private static let descriptions: [Int: String] = [
9 | 400: "Bad Request",
10 | 401: "Unauthorized",
11 | 402: "Payment Required",
12 | 403: "Forbidden",
13 | 404: "Not Found",
14 | 405: "Method Not Allowed",
15 | 406: "Not Acceptable",
16 | 407: "Proxy Authentication Required",
17 | 408: "Request Timeout",
18 | 409: "Conflict",
19 | 410: "Gone",
20 | 411: "Length Required",
21 | 412: "Precondition Failed",
22 | 413: "Payload Too Large",
23 | 414: "URI Too Long",
24 | 415: "Unsupported Media Type",
25 | 416: "Range Not Satisfiable",
26 | 417: "Expectation Failed",
27 | 418: "I'm a teapot",
28 | 421: "Misdirected Request",
29 | 422: "Unprocessable Entity",
30 | 423: "Locked",
31 | 424: "Failed Dependency",
32 | 425: "Too Early",
33 | 426: "Upgrade Required",
34 | 428: "Precondition Required",
35 | 429: "Too Many Requests",
36 | 431: "Request Header Fields Too Large",
37 | 451: "Unavailable For Legal Reasons"
38 | ]
39 | }
40 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Request/RequestFactory.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol RequestFactory {
4 | func build(httpMethod: HTTPMethod,
5 | baseUrlString: String,
6 | parameters: [HTTPParameter]?,
7 | headers: [HTTPHeader]?,
8 | body: HTTPBody?,
9 | timeoutInterval: TimeInterval,
10 | cachePolicy: URLRequest.CachePolicy) -> Request
11 | }
12 |
13 | public class RequestFactoryImpl: RequestFactory {
14 | private let headerEncoder: HTTPHeaderEncoder
15 | private let paramEncoder: HTTPParameterEncoder
16 |
17 | public init(headerEncoder: HTTPHeaderEncoder = HTTPHeaderEncoderImpl(),
18 | paramEncoder: HTTPParameterEncoder = HTTPParameterEncoderImpl()) {
19 | self.headerEncoder = headerEncoder
20 | self.paramEncoder = paramEncoder
21 | }
22 |
23 | public func build(httpMethod: HTTPMethod,
24 | baseUrlString: String,
25 | parameters: [HTTPParameter]?,
26 | headers: [HTTPHeader]? = nil,
27 | body: HTTPBody? = nil,
28 | timeoutInterval: TimeInterval = 60,
29 | cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
30 | ) -> Request {
31 | return EZRequest(httpMethod: httpMethod, baseUrlString: baseUrlString, parameters: parameters, headers: headers, body: body, timeoutInterval: timeoutInterval, cachePolicy: cachePolicy)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/HTTPErrorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPErrorTests")
5 | final class HTTPErrorTests {
6 |
7 | private static let statusCodeToCategoryMap: [Int: HTTPError.HTTPErrorCategory] = [
8 | 100: HTTPError.HTTPErrorCategory.informational,
9 | 200: HTTPError.HTTPErrorCategory.success,
10 | 300: HTTPError.HTTPErrorCategory.redirection,
11 | 400: HTTPError.HTTPErrorCategory.clientError,
12 | 500: HTTPError.HTTPErrorCategory.serverError
13 | ]
14 | @Test("test HTTPError category given status code", arguments: zip(statusCodeToCategoryMap.keys, statusCodeToCategoryMap.values))
15 | func testHTTPErrorCategoryGivenStatusCode(statusCode: Int, expectedCategory: HTTPError.HTTPErrorCategory) {
16 | let sut = HTTPError(statusCode: statusCode, headers: [:])
17 | #expect(sut.category == expectedCategory)
18 | }
19 |
20 | private static let statusCodeToDescriptionMap: [Int: String] = [
21 | 100: "Continue",
22 | 200: "OK",
23 | 300: "Multiple Choices",
24 | 400: "Bad Request",
25 | 500: "Internal Server Error"
26 | ]
27 | @Test("test HTTPError description given status code", arguments: zip(statusCodeToDescriptionMap.keys, statusCodeToDescriptionMap.values))
28 | func testHTTPErrorDescriptionGivenStatusCode(statusCode: Int, expectedDescription: String) {
29 | let sut = HTTPError(statusCode: statusCode, headers: [:])
30 | #expect(sut.description == expectedDescription)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/WebSocket/Mocks/MockWebSockerURLSession.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockWebSockerURLSession: URLSessionTaskProtocol {
5 | private let webSocketTask: MockURLSessionWebSocketTask
6 |
7 | var didCallWebSocketTaskInspectable = false
8 |
9 | init(webSocketTask: MockURLSessionWebSocketTask = MockURLSessionWebSocketTask()) {
10 | self.webSocketTask = webSocketTask
11 | }
12 |
13 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
14 | didCallWebSocketTaskInspectable = true
15 | return webSocketTask
16 | }
17 | }
18 |
19 | // MARK: unused methods
20 |
21 | extension MockWebSockerURLSession {
22 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
23 | fatalError("Should not be using in this mock")
24 | }
25 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
26 | fatalError("Should not be using in this mock")
27 | }
28 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
29 | fatalError("Should not be using in this mock")
30 | }
31 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
32 | fatalError("Should not be using in this mock")
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Other/Extensions/String-extTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test String Extensions")
6 | final class StringExtensinoTests {
7 |
8 | // MARK: - .getRandomMultiPartFormBoundary()
9 |
10 | private let prefix = "EZNetworking.Boundary."
11 |
12 | @Test("generated boundary has expected prefix and UUID suffix")
13 | func test_generatedBoundary_hasPrefixAndUuidSuffix() {
14 | let boundary = String.getRandomMultiPartFormBoundary()
15 | #expect(boundary.hasPrefix(prefix))
16 |
17 | let uuidPart = String(boundary.dropFirst(prefix.count))
18 | #expect(UUID(uuidString: uuidPart) != nil)
19 | }
20 |
21 | @Test("consecutive calls produce different values")
22 | func test_consecutiveCalls_areDifferent() {
23 | let first = String.getRandomMultiPartFormBoundary()
24 | let second = String.getRandomMultiPartFormBoundary()
25 | #expect(first != second)
26 | }
27 |
28 | @Test("multiple calls produce unique values")
29 | func test_multipleCalls_areUnique() {
30 | let iterations = 200
31 | var seen = Set()
32 | for _ in 0.. prefix.count)
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Request/Request.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol Request {
4 | var httpMethod: HTTPMethod { get }
5 | var baseUrlString: String { get }
6 | var parameters: [HTTPParameter]? { get }
7 | var headers: [HTTPHeader]? { get }
8 | var body: HTTPBody? { get }
9 | var timeoutInterval: TimeInterval { get }
10 | var cachePolicy: URLRequest.CachePolicy { get }
11 | }
12 |
13 | // default values
14 | public extension Request {
15 | var timeoutInterval: TimeInterval { 60 }
16 | var cachePolicy: URLRequest.CachePolicy { .useProtocolCachePolicy }
17 | }
18 |
19 | // additions
20 | public extension Request {
21 | var urlRequest: URLRequest? {
22 | guard let url = URL(string: baseUrlString) else {
23 | return nil
24 | }
25 |
26 | var request = URLRequest(url: url)
27 | request.httpMethod = httpMethod.rawValue
28 | request.httpBody = body?.toData()
29 | request.timeoutInterval = timeoutInterval
30 | request.cachePolicy = cachePolicy
31 |
32 | if let parameters = parameters {
33 | try? HTTPParameterEncoderImpl().encodeParameters(for: &request, with: parameters)
34 | }
35 |
36 | if let headers = headers {
37 | HTTPHeaderEncoderImpl().encodeHeaders(for: &request, with: headers)
38 | }
39 |
40 | return request
41 | }
42 | }
43 |
44 | internal struct EZRequest: Request {
45 | var httpMethod: HTTPMethod
46 | var baseUrlString: String
47 | var parameters: [HTTPParameter]?
48 | var headers: [HTTPHeader]?
49 | var body: HTTPBody?
50 | var timeoutInterval: TimeInterval
51 | var cachePolicy: URLRequest.CachePolicy
52 | }
53 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/HTTPError/StatusCodeTypes/HTTPClientErrorStatusTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPClientErrorStatus")
5 | final class HTTPClientErrorStatusTests {
6 |
7 | @Test("test HTTPClientErrorStatus maps status code to description", arguments: zip(map.keys, map.values))
8 | func testHTTPClientErrorStatusMapsStatusCodeToDescription(statusCode: Int, description: String) {
9 | #expect(HTTPClientErrorStatus.description(from: statusCode) == description)
10 | }
11 |
12 | private static let map: [Int: String] = [
13 | 400: "Bad Request",
14 | 401: "Unauthorized",
15 | 402: "Payment Required",
16 | 403: "Forbidden",
17 | 404: "Not Found",
18 | 405: "Method Not Allowed",
19 | 406: "Not Acceptable",
20 | 407: "Proxy Authentication Required",
21 | 408: "Request Timeout",
22 | 409: "Conflict",
23 | 410: "Gone",
24 | 411: "Length Required",
25 | 412: "Precondition Failed",
26 | 413: "Payload Too Large",
27 | 414: "URI Too Long",
28 | 415: "Unsupported Media Type",
29 | 416: "Range Not Satisfiable",
30 | 417: "Expectation Failed",
31 | 418: "I'm a teapot",
32 | 421: "Misdirected Request",
33 | 422: "Unprocessable Entity",
34 | 423: "Locked",
35 | 424: "Failed Dependency",
36 | 425: "Too Early",
37 | 426: "Upgrade Required",
38 | 428: "Precondition Required",
39 | 429: "Too Many Requests",
40 | 431: "Request Header Fields Too Large",
41 | 451: "Unavailable For Legal Reasons",
42 | -1: "Unknown Client Error"
43 | ]
44 | }
45 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/WebSocket/Mocks/MockWebSocketTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | let webSocketUrl = URL(string: "ws://127.0.0.1:8080/example")!
5 | var webSocketRequest: URLRequest { URLRequest(url: webSocketUrl) }
6 |
7 | class MockWebSocketTaskInterceptor: WebSocketTaskInterceptor {
8 | private let session = URLSession.shared
9 | private lazy var task: URLSessionWebSocketTask = {
10 | session.webSocketTask(with: webSocketUrl, protocols: [])
11 | }()
12 |
13 | var onEvent: ((WebSocketTaskEvent) -> Void)?
14 |
15 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
16 | onEvent?(.didOpenWithProtocol(protocolStr: `protocol`))
17 | }
18 |
19 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: any Error) {
20 | onEvent?(.didOpenWithError(error: error))
21 | }
22 |
23 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
24 | onEvent?(.didClose(code: closeCode, reason: reason))
25 | }
26 |
27 | // simulate methods
28 |
29 | func simulateOpenWithProtocol(_ proto: String?) {
30 | urlSession(session, webSocketTask: task, didOpenWithProtocol: proto)
31 | }
32 |
33 | func simulateDidCompleteWithError(error: any Error) {
34 | urlSession(session, task: task, didCompleteWithError: error)
35 | }
36 |
37 | func simulateDidCloseWithCloseCode(didCloseWith: URLSessionWebSocketTask.CloseCode, reason: Data?) {
38 | urlSession(session, webSocketTask: task, didCloseWith: didCloseWith, reason: reason)
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionDataDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionDataDelegate {
4 | public func urlSession(_ session: URLSession,
5 | dataTask: URLSessionDataTask,
6 | willCacheResponse proposedResponse: CachedURLResponse) async -> CachedURLResponse? {
7 | await cacheInterceptor?.urlSession(session, dataTask: dataTask, willCacheResponse: proposedResponse) ?? proposedResponse
8 | }
9 |
10 | public func urlSession(_ session: URLSession,
11 | dataTask: URLSessionDataTask,
12 | didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
13 | await dataTaskInterceptor?.urlSession(session, dataTask: dataTask, didReceive: response) ?? .allow
14 | }
15 |
16 | public func urlSession(_ session: URLSession,
17 | dataTask: URLSessionDataTask,
18 | didReceive data: Data) {
19 | dataTaskInterceptor?.urlSession(session, dataTask: dataTask, didReceive: data)
20 | }
21 |
22 | public func urlSession(_ session: URLSession,
23 | dataTask: URLSessionDataTask,
24 | didBecome downloadTask: URLSessionDownloadTask) {
25 | dataTaskInterceptor?.urlSession(session, dataTask: dataTask, didBecome: downloadTask)
26 | }
27 |
28 | public func urlSession(_ session: URLSession,
29 | dataTask: URLSessionDataTask,
30 | didBecome streamTask: URLSessionStreamTask) {
31 | dataTaskInterceptor?.urlSession(session, dataTask: dataTask, didBecome: streamTask)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Uploader/Helpers/DefaultUploadTaskInterceptor.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test DefaultUploadTaskInterceptor")
6 | final class DefaultUploadTaskInterceptorTests {
7 |
8 | @Test("test DefaultUploadTaskInterceptor can track 0% progress")
9 | func test_0_percent_progress_tracking() {
10 | var trackedProgress: Double = 0
11 | let sut = DefaultUploadTaskInterceptor { progress in
12 | trackedProgress = progress
13 | }
14 | sut.urlSession(.shared, task: mockDataTask, didSendBodyData: 0, totalBytesSent: 0, totalBytesExpectedToSend: 100)
15 | #expect(trackedProgress == 0)
16 | }
17 |
18 | @Test("test DefaultUploadTaskInterceptor can track 50% progress")
19 | func test_50_percent_progress_tracking() {
20 | var trackedProgress: Double = 0
21 | let sut = DefaultUploadTaskInterceptor { progress in
22 | trackedProgress = progress
23 | }
24 | sut.urlSession(.shared, task: mockDataTask, didSendBodyData: 0, totalBytesSent: 50, totalBytesExpectedToSend: 100)
25 | #expect(trackedProgress == 0.5)
26 | }
27 |
28 | @Test("test DefaultUploadTaskInterceptor can track 100% progress")
29 | func test_100_percent_progress_tracking() {
30 | var trackedProgress: Double = 0
31 | let sut = DefaultUploadTaskInterceptor { progress in
32 | trackedProgress = progress
33 | }
34 | sut.urlSession(.shared, task: mockDataTask, didSendBodyData: 0, totalBytesSent: 100, totalBytesExpectedToSend: 100)
35 | #expect(trackedProgress == 1)
36 | }
37 |
38 |
39 | private let mockUrl: URL = URL(string: "https://example.com")!
40 | private var mockDataTask: URLSessionDataTask { URLSessionDataTask() }
41 | }
42 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Performers/Mocks/MockRequestPerformerURLSession.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockRequestPerformerURLSession: URLSessionTaskProtocol {
5 | var data: Data?
6 | var urlResponse: URLResponse?
7 | var error: Error?
8 | var completion: ((Data?, URLResponse?, Error?) -> Void)?
9 | var sessionDelegate: SessionDelegate? = nil
10 |
11 | init(data: Data? = nil, urlResponse: URLResponse? = nil, error: Error? = nil) {
12 | self.data = data
13 | self.urlResponse = urlResponse
14 | self.error = error
15 | }
16 |
17 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
18 | self.completion = completionHandler
19 | return MockURLSessionDataTask {
20 | completionHandler(self.data, self.urlResponse, self.error)
21 | }
22 | }
23 | }
24 |
25 | // MARK: unused methods
26 |
27 | extension MockRequestPerformerURLSession {
28 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
29 | fatalError("Should not be using in this mock")
30 | }
31 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
32 | fatalError("Should not be using in this mock")
33 | }
34 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
35 | fatalError("Should not be using in this mock")
36 | }
37 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
38 | fatalError("Should not be using in this mock")
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/HTTPError/HTTPError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public struct HTTPError: Error {
4 | public let statusCode: Int
5 | public let headers: [AnyHashable: Any]
6 | public let category: HTTPErrorCategory
7 |
8 | public init(statusCode: Int, headers: [AnyHashable: Any] = [:]) {
9 | self.statusCode = statusCode
10 | self.headers = headers
11 | self.category = HTTPErrorCategory.from(statusCode: statusCode)
12 | }
13 |
14 | public var description: String {
15 | switch category {
16 | case .informational:
17 | return HTTPInformationalStatus.description(from: statusCode)
18 | case .success:
19 | return HTTPSuccessStatus.description(from: statusCode)
20 | case .redirection:
21 | return HTTPRedirectionStatus.description(from: statusCode)
22 | case .clientError:
23 | return HTTPClientErrorStatus.description(from: statusCode)
24 | case .serverError:
25 | return HTTPServerErrorStatus.description(from: statusCode)
26 | case .unknown:
27 | return "Unknown Status Code (\(statusCode))"
28 | }
29 | }
30 |
31 | public enum HTTPErrorCategory {
32 | case informational // 1xx
33 | case success // 2xx
34 | case redirection // 3xx
35 | case clientError // 4xx
36 | case serverError // 5xx
37 | case unknown // Other
38 |
39 | static func from(statusCode: Int) -> HTTPErrorCategory {
40 | switch statusCode {
41 | case 100...199: return .informational
42 | case 200...299: return .success
43 | case 300...399: return .redirection
44 | case 400...499: return .clientError
45 | case 500...599: return .serverError
46 | default: return .unknown
47 | }
48 | }
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Downloader/Helpers/DefaultDownloadTaskInterceptorTests.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | @testable import EZNetworking
3 | import Foundation
4 | import Testing
5 |
6 |
7 | @Suite("Test DefaultDownloadTaskInterceptor")
8 | final class DefaultDownloadTaskInterceptorTests {
9 |
10 | @Test("test .urlSession(_, downloadTask:_, didFinishDownloadingTo:_) tracks progress 100%")
11 | func test_didFinishDownloadingTo_tracksProgress_completion() {
12 | var trackedProgress: Double = 0
13 | let sut = DefaultDownloadTaskInterceptor { progress in
14 | trackedProgress = progress
15 | }
16 | sut.urlSession(.shared, downloadTask: mockDownloadTask, didFinishDownloadingTo: mockUrl)
17 | #expect(trackedProgress == 1.0)
18 | }
19 |
20 | @Test("test .urlSession(_, downloadTask:_, didResumeAtOffset:_, expectedTotalBytes:_) tracks partial progress")
21 | func test_didResumeAtOffset_tracksProgress() {
22 | var trackedProgress: Double = 0
23 | let sut = DefaultDownloadTaskInterceptor { progress in
24 | trackedProgress = progress
25 | }
26 | sut.urlSession(.shared, downloadTask: mockDownloadTask, didResumeAtOffset: 50, expectedTotalBytes: 100)
27 | #expect(trackedProgress == 0.5)
28 | }
29 |
30 | @Test("test .urlSession(_, downloadTask:_, didWriteData:_, totalBytesWritten:_, totalBytesExpectedToWrite:_) tracks partial progress")
31 | func test_didWriteData_tracksProgress_() {
32 | var trackedProgress: Double = 0
33 | let sut = DefaultDownloadTaskInterceptor { progress in
34 | trackedProgress = progress
35 | }
36 | sut.urlSession(.shared, downloadTask: mockDownloadTask, didWriteData: 10, totalBytesWritten: 50, totalBytesExpectedToWrite: 100)
37 | #expect(trackedProgress == 0.5)
38 | }
39 | }
40 |
41 | private let mockUrl: URL = URL(string: "https://example.com")!
42 | private var mockDownloadTask: URLSessionDownloadTask { URLSessionDownloadTask() }
43 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/HTTP/Body/MultipartFormData/MultipartFormPartTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test MultipartFormPart")
6 | class MultipartFormPartTests {
7 |
8 | @Test("explicit MultipartFormPart.file sets all properties")
9 | func test_MultipartFormPartfile_setsProperties() {
10 | let payload = "hello".data(using: .utf8)!
11 | let part = MultipartFormPart.filePart(name: "field",
12 | data: payload,
13 | filename: "file.txt",
14 | mimeType: .plain)
15 |
16 | #expect(part.name == "field")
17 | #expect(part.filename == "file.txt")
18 | #expect(part.mimeType == .plain)
19 | #expect(part.data == payload)
20 | #expect(part.contentLength == 5)
21 | }
22 |
23 | @Test("explicit MultipartFormPart.string sets all properties")
24 | func test_MultipartFormPartString_setsProperties() {
25 | let payload = "value".data(using: .utf8)!
26 | let part = MultipartFormPart.fieldPart(name: "file", value: "value")
27 |
28 | #expect(part.name == "file")
29 | #expect(part.filename == nil)
30 | #expect(part.mimeType == .plain)
31 | #expect(part.data == payload)
32 | #expect(part.contentLength == 5)
33 | }
34 |
35 | @Test("explicit MultipartFormPart.dataPart sets all properties")
36 | func test_MultipartFormPartData_setsProperties() {
37 | let payload = "value".data(using: .utf8)!
38 | let part = MultipartFormPart.dataPart(name: "field",
39 | data: payload,
40 | mimeType: .json)
41 | #expect(part.name == "field")
42 | #expect(part.filename == nil)
43 | #expect(part.mimeType == .json)
44 | #expect(part.data == payload)
45 | #expect(part.contentLength == 5)
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Other/Extensions/Data-ext.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public extension Data {
4 | /// Create `Data` from a UTF-8 `String`.
5 | init?(string: String) {
6 | guard let data = string.data(using: .utf8) else { return nil }
7 | self = data
8 | }
9 |
10 | /// Create `Data` by serializing a dictionary to JSON.
11 | init?(dictionary: [String: Any]) {
12 | guard let data = try? JSONSerialization.data(withJSONObject: dictionary, options: []) else { return nil }
13 | self = data
14 | }
15 |
16 | /// Encode an `Encodable` value to JSON `Data`.
17 | init?(encodable: T, encoder: JSONEncoder = JSONEncoder()) {
18 | guard let data = try? encoder.encode(encodable) else { return nil }
19 | self = data
20 | }
21 |
22 | /// Read file contents into `Data`.
23 | init?(fileURL url: URL) {
24 | guard let data = try? Data(contentsOf: url) else { return nil }
25 | self = data
26 | }
27 |
28 | /// Create `Data` from a JSON-formatted `String`.
29 | init?(jsonString: String) {
30 | self.init(string: jsonString)
31 | }
32 |
33 | /// Create `Data` from a Base64 encoded string.
34 | init?(base64: String) {
35 | guard let data = Data(base64Encoded: base64) else { return nil }
36 | self = data
37 | }
38 |
39 | /// Create `Data` from URLComponents' percent-encoded query.
40 | init?(urlComponents: URLComponents) {
41 | guard let query = urlComponents.percentEncodedQuery,
42 | let data = query.data(using: .utf8) else { return nil }
43 | self = data
44 | }
45 |
46 | /// Create `Data` from MultipartFormData
47 | init?(multipartFormData: MultipartFormData) {
48 | guard let data = multipartFormData.toData() else { return nil }
49 | self = data
50 | }
51 | }
52 |
53 | public extension Data {
54 | func appending(_ data: Data?) -> Data {
55 | guard let dataToAppend = data else { return self }
56 | var copy = self
57 | copy.append(dataToAppend)
58 | return copy
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Validator/ResponseValidator.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol ResponseValidator {
4 | func validateNoError(_ error: Error?) throws
5 | func validateStatus(from urlResponse: URLResponse?) throws
6 | func validateData(_ data: Data?) throws -> Data
7 | func validateUrl(_ url: URL?) throws -> URL
8 | }
9 |
10 | public struct ResponseValidatorImpl: ResponseValidator {
11 | public init() {}
12 |
13 | public func validateNoError(_ error: Error?) throws {
14 | if let error = error {
15 | if let urlError = error as? URLError {
16 | throw NetworkingError.urlError(urlError)
17 | }
18 | throw NetworkingError.internalError(.requestFailed(error))
19 | }
20 | }
21 |
22 | public func validateStatus(from urlResponse: URLResponse?) throws {
23 | guard let urlResponse else {
24 | throw NetworkingError.internalError(.noResponse)
25 | }
26 | guard let httpURLResponse = urlResponse as? HTTPURLResponse else {
27 | throw NetworkingError.internalError(.noHTTPURLResponse)
28 | }
29 | try validateStatusCodeAccepability(from: httpURLResponse)
30 | }
31 |
32 | public func validateData(_ data: Data?) throws -> Data {
33 | guard let data else {
34 | throw NetworkingError.internalError(.noData)
35 | }
36 | return data
37 | }
38 |
39 | public func validateUrl(_ url: URL?) throws -> URL {
40 | guard let url else {
41 | throw NetworkingError.internalError(.noURL)
42 | }
43 | return url
44 | }
45 |
46 | private func validateStatusCodeAccepability(from httpURLResponse: HTTPURLResponse) throws {
47 | let statusCode = HTTPError(statusCode: httpURLResponse.statusCode,
48 | headers: httpURLResponse.allHeaderFields)
49 |
50 | if statusCode.category == .success {
51 | return // successful http response (2xx) do not throw error
52 | }
53 | throw NetworkingError.httpError(statusCode)
54 | }
55 | }
56 |
57 | public typealias URLResponseHeaders = [AnyHashable: Any]
58 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class SessionDelegate: NSObject {
4 | public weak var cacheInterceptor: CacheInterceptor? = nil
5 | public weak var authenticationInterceptor: AuthenticationInterceptor? = nil
6 | public weak var redirectInterceptor: RedirectInterceptor? = nil
7 | public weak var metricsInterceptor: MetricsInterceptor? = nil
8 | public weak var taskLifecycleInterceptor: TaskLifecycleInterceptor? = nil
9 | public weak var dataTaskInterceptor: DataTaskInterceptor? = nil
10 | public weak var downloadTaskInterceptor: DownloadTaskInterceptor? = nil
11 | public weak var uploadTaskInterceptor: UploadTaskInterceptor? = nil
12 | public weak var streamTaskInterceptor: StreamTaskInterceptor? = nil
13 | public weak var webSocketTaskInterceptor: WebSocketTaskInterceptor? = nil
14 |
15 | public init(cacheInterceptor: CacheInterceptor? = nil,
16 | authenticationInterceptor: AuthenticationInterceptor? = nil,
17 | redirectInterceptor: RedirectInterceptor? = nil,
18 | metricsInterceptor: MetricsInterceptor? = nil,
19 | taskLifecycleInterceptor: TaskLifecycleInterceptor? = nil,
20 | dataTaskInterceptor: DataTaskInterceptor? = nil,
21 | downloadTaskInterceptor: DownloadTaskInterceptor? = nil,
22 | uploadTaskInterceptor: UploadTaskInterceptor? = nil,
23 | streamTaskInterceptor: StreamTaskInterceptor? = nil,
24 | webSocketTaskInterceptor: WebSocketTaskInterceptor? = nil) {
25 | self.cacheInterceptor = cacheInterceptor
26 | self.authenticationInterceptor = authenticationInterceptor
27 | self.redirectInterceptor = redirectInterceptor
28 | self.metricsInterceptor = metricsInterceptor
29 | self.taskLifecycleInterceptor = taskLifecycleInterceptor
30 | self.dataTaskInterceptor = dataTaskInterceptor
31 | self.downloadTaskInterceptor = downloadTaskInterceptor
32 | self.uploadTaskInterceptor = uploadTaskInterceptor
33 | self.streamTaskInterceptor = streamTaskInterceptor
34 | self.webSocketTaskInterceptor = webSocketTaskInterceptor
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/WebSocket/Helpers/WebSocketConnectionStateTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test WebSocketConnectionState")
6 | final class WebSocketConnectionStateTests {
7 | @Test("basic equality for simple states")
8 | func testBasicEquality() {
9 | #expect(WebSocketConnectionState.disconnected == .disconnected)
10 | #expect(WebSocketConnectionState.connecting == .connecting)
11 | #expect(WebSocketConnectionState.idle == .idle)
12 | }
13 |
14 | @Test("connected state equality with protocol values")
15 | func testConnectedEquality() {
16 | #expect(WebSocketConnectionState.connected(protocol: nil) == .connected(protocol: nil))
17 | #expect(WebSocketConnectionState.connected(protocol: "chat") == .connected(protocol: "chat"))
18 | #expect(WebSocketConnectionState.connected(protocol: "chat") != .connected(protocol: "video"))
19 | #expect(WebSocketConnectionState.connected(protocol: nil) != .connected(protocol: "chat"))
20 | }
21 |
22 | @Test("connectionLost and failed equality by underlying WebSocketError")
23 | func testErrorAssociatedEquality() {
24 | let err1 = WebSocketError.connectionFailed(underlying: URLError(.notConnectedToInternet))
25 | let err2 = WebSocketError.connectionFailed(underlying: URLError(.notConnectedToInternet))
26 | let other = WebSocketError.connectionFailed(underlying: NSError(domain: "x", code: 1))
27 |
28 | #expect(WebSocketConnectionState.connectionLost(reason: err1) == .connectionLost(reason: err2))
29 | #expect(WebSocketConnectionState.connectionLost(reason: err1) != .connectionLost(reason: other))
30 |
31 | #expect(WebSocketConnectionState.failed(error: err1) == .failed(error: err2))
32 | #expect(WebSocketConnectionState.failed(error: err1) != .failed(error: other))
33 | }
34 |
35 | @Test("different enum cases are not equal")
36 | func testDifferentCasesNotEqual() {
37 | #expect(WebSocketConnectionState.disconnected != .connecting)
38 | #expect(WebSocketConnectionState.disconnected != .connected(protocol: nil))
39 | #expect(WebSocketConnectionState.connecting != .connected(protocol: "p"))
40 | #expect(WebSocketConnectionState.connectionLost(reason: WebSocketError.taskCancelled) != .failed(error: WebSocketError.taskCancelled))
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Body/MultipartFormData/MultipartFormPart.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | /// Represents a single body part in a `multipart/form-data` HTTP request.
4 | public struct MultipartFormPart {
5 | /// The form field name associated with this part.
6 | public var name: String
7 |
8 | /// The raw binary data representing the body of this part.
9 | public var data: Data
10 |
11 | /// An optional filename to include in the `Content-Disposition` header.
12 | public var filename: String?
13 |
14 | /// The MIME type describing the content of the data payload.
15 | public var mimeType: MimeType
16 |
17 | /// The total size of the data payload, in bytes.
18 | public var contentLength: Int { data.count }
19 |
20 | /// The payload interpreted as a UTF-8 string.
21 | public var value: String? {
22 | get { String(data: data, encoding: .utf8) }
23 | set { data = newValue.map { Data($0.utf8) } ?? Data() }
24 | }
25 |
26 | // MARK: - Internal Initializers
27 |
28 | /// Creates a new multipart form part with binary data.
29 | internal init(name: String, data: Data, filename: String? = nil, mimeType: MimeType) {
30 | self.name = name
31 | self.data = data
32 | self.filename = filename
33 | self.mimeType = mimeType
34 | }
35 |
36 | /// Creates a new multipart form part from a plain text value.
37 | internal init(name: String, value: String) {
38 | self.init(name: name, data: Data(value.utf8), filename: nil, mimeType: .plain)
39 | }
40 | }
41 |
42 | // MARK: - Creation static methods
43 |
44 | public extension MultipartFormPart {
45 | /// Creates a text field part with a UTF-8 encoded value.
46 | static func fieldPart(name: String, value: String) -> MultipartFormPart {
47 | MultipartFormPart(name: name, value: value)
48 | }
49 |
50 | /// Creates a file upload part with binary data and metadata.
51 | static func filePart(name: String, data: Data, filename: String, mimeType: MimeType) -> MultipartFormPart {
52 | MultipartFormPart(name: name, data: data, filename: filename, mimeType: mimeType)
53 | }
54 |
55 | /// Creates a data-only part (no filename) with the given MIME type, useful for raw blobs or non-file fields.
56 | static func dataPart(name: String, data: Data, mimeType: MimeType) -> MultipartFormPart {
57 | MultipartFormPart(name: name, data: data, filename: nil, mimeType: mimeType)
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Headers/HTTPHeader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum HTTPHeader: Equatable {
4 | case accept(MimeType)
5 | case acceptCharset(String)
6 | case acceptEncoding(String)
7 | case acceptLanguage(String)
8 | case authorization(AuthorizationType)
9 | case cacheControl(String)
10 | case contentLength(String)
11 | case contentType(MimeType)
12 | case cookie(String)
13 | case host(String)
14 | case ifModifiedSince(String)
15 | case ifNoneMatch(String)
16 | case origin(String)
17 | case referer(String)
18 | case userAgent(String)
19 | case custom(key: String, value: String)
20 |
21 | var key: String {
22 | switch self {
23 | case .accept: return "Accept"
24 | case .acceptCharset: return "Accept-Charset"
25 | case .acceptEncoding: return "Accept-Encoding"
26 | case .acceptLanguage: return "Accept-Language"
27 | case .authorization: return "Authorization"
28 | case .cacheControl: return "Cache-Control"
29 | case .contentLength: return "Content-Length"
30 | case .contentType: return "Content-Type"
31 | case .cookie: return "Cookie"
32 | case .host: return "Host"
33 | case .ifModifiedSince: return "If-Modified-Since"
34 | case .ifNoneMatch: return "If-None-Match"
35 | case .origin: return "Origin"
36 | case .referer: return "Referer"
37 | case .userAgent: return "User-Agent"
38 | case .custom(let key, _): return key
39 | }
40 | }
41 |
42 | var value: String {
43 | switch self {
44 | case .accept(let accept): return accept.value
45 | case .acceptCharset(let value): return value
46 | case .acceptEncoding(let value): return value
47 | case .acceptLanguage(let value): return value
48 | case .authorization(let authentication): return authentication.value
49 | case .cacheControl(let value): return value
50 | case .contentLength(let value): return value
51 | case .contentType(let contentType): return contentType.value
52 | case .cookie(let value): return value
53 | case .host(let value): return value
54 | case .ifModifiedSince(let value): return value
55 | case .ifNoneMatch(let value): return value
56 | case .origin(let value): return value
57 | case .referer(let value): return value
58 | case .userAgent(let value): return value
59 | case .custom(_, let value): return value
60 | }
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Interceptors/SessionDelegate+Extensions/SessionDelegate+URLSessionTaskDelegate.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | extension SessionDelegate: URLSessionTaskDelegate {
4 | // Async authentication
5 | public func urlSession(_ session: URLSession,
6 | task: URLSessionTask,
7 | didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
8 | await authenticationInterceptor?.urlSession(session, task: task, didReceive: challenge) ?? (.performDefaultHandling, nil)
9 | }
10 |
11 | // Redirect Interception
12 | public func urlSession(_ session: URLSession,
13 | task: URLSessionTask,
14 | willPerformHTTPRedirection response: HTTPURLResponse,
15 | newRequest request: URLRequest) async -> URLRequest? {
16 | await redirectInterceptor?.urlSession(session, task: task, willPerformHTTPRedirection: response, newRequest: request) ?? request
17 | }
18 |
19 | // Metrics Interception
20 | public func urlSession(_ session: URLSession,
21 | task: URLSessionTask,
22 | didFinishCollecting metrics: URLSessionTaskMetrics) {
23 | metricsInterceptor?.urlSession(session, task: task, didFinishCollecting: metrics)
24 | }
25 |
26 | // Task Lifecycle Interception
27 | public func urlSession(_ session: URLSession,
28 | task: URLSessionTask,
29 | didCompleteWithError error: Error?) {
30 | taskLifecycleInterceptor?.urlSession(session, task: task, didCompleteWithError: error)
31 |
32 | if let error = error {
33 | webSocketTaskInterceptor?.urlSession(session, task: task, didCompleteWithError: error)
34 | }
35 | }
36 |
37 | public func urlSession(_ session: URLSession,
38 | taskIsWaitingForConnectivity task: URLSessionTask) {
39 | taskLifecycleInterceptor?.urlSession(session, taskIsWaitingForConnectivity: task)
40 | }
41 |
42 | public func urlSession(_ session: URLSession,
43 | task: URLSessionTask,
44 | didSendBodyData bytesSent: Int64,
45 | totalBytesSent: Int64,
46 | totalBytesExpectedToSend: Int64) {
47 | uploadTaskInterceptor?.urlSession(session, task: task, didSendBodyData: bytesSent, totalBytesSent: totalBytesSent, totalBytesExpectedToSend: totalBytesExpectedToSend)
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Request/RequestBuilderTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test RequestBuilder")
6 | final class RequestBuilderTests {
7 |
8 | @Test("test BuildURLRequest with valid parameters")
9 | func testBuildURLRequestWithValidParameters() {
10 | let builder = RequestBuilderImpl()
11 | let urlString = "https://example.com/api"
12 | let httpMethod = HTTPMethod.POST
13 | let parameters: [HTTPParameter] = [
14 | HTTPParameter(key: "key1", value: "value1"),
15 | HTTPParameter(key: "key2", value: "value2")
16 | ]
17 | let headers: [HTTPHeader] = [
18 | HTTPHeader.contentType(.json),
19 | HTTPHeader.authorization(.bearer("token"))
20 | ]
21 | let body = Data(jsonString: "{\"name\": \"John\"}")!
22 | let timeoutInterval: TimeInterval = 30
23 |
24 | let request = builder
25 | .setHttpMethod(httpMethod)
26 | .setBaseUrl(urlString)
27 | .setParameters(parameters)
28 | .setHeaders(headers)
29 | .setBody(body)
30 | .setTimeoutInterval(timeoutInterval)
31 | .build()
32 |
33 | #expect(request != nil)
34 | #expect(request?.baseUrlString == "https://example.com/api")
35 | #expect(request?.httpMethod == httpMethod)
36 | #expect(request?.parameters == parameters)
37 | #expect(request?.body?.toData() == body)
38 | #expect(request?.timeoutInterval == timeoutInterval)
39 | #expect(request?.headers == headers)
40 | #expect(request?.cachePolicy == .useProtocolCachePolicy)
41 | }
42 |
43 | @Test("test BuildURLRequest with no parameters and headers")
44 | func testBuildURLRequestWithNoParametersAndHeaders() {
45 | let builder = RequestBuilderImpl()
46 | let urlString = "https://example.com/api"
47 | let httpMethod = HTTPMethod.PUT
48 |
49 | let request = builder
50 | .setHttpMethod(httpMethod)
51 | .setBaseUrl(urlString)
52 | .setTimeoutInterval(60)
53 | .build()
54 |
55 | #expect(request != nil)
56 | #expect(request?.baseUrlString == "https://example.com/api")
57 | #expect(request?.httpMethod == httpMethod)
58 | #expect(request?.parameters == nil)
59 | #expect(request?.body == nil)
60 | #expect(request?.timeoutInterval == 60)
61 | #expect(request?.headers == nil)
62 | #expect(request?.cachePolicy == .useProtocolCachePolicy)
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Body/MultipartFormData/MultipartFormData.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public class MultipartFormData: DataConvertible {
4 |
5 | // MARK: - Helper Types
6 |
7 | enum Constants {
8 | static let crlf = "\r\n"
9 | }
10 |
11 | enum BoundaryGenerator {
12 | enum BoundaryType {
13 | case initial, encapsulated, final
14 | }
15 |
16 | static func boundaryData(forBoundaryType boundaryType: BoundaryType, boundary: String) -> Data {
17 | let boundaryText = switch boundaryType {
18 | case .initial:
19 | "--\(boundary)\(Constants.crlf)"
20 | case .encapsulated:
21 | "\(Constants.crlf)--\(boundary)\(Constants.crlf)"
22 | case .final:
23 | "\(Constants.crlf)--\(boundary)--\(Constants.crlf)"
24 | }
25 | return Data(boundaryText.utf8)
26 | }
27 | }
28 |
29 | // MARK: Variables
30 |
31 | public let parts: [MultipartFormPart]
32 | public let boundary: String
33 |
34 | // MARK: Init
35 |
36 | public init(parts: [MultipartFormPart], boundary: String) {
37 | self.parts = parts
38 | self.boundary = boundary
39 | }
40 |
41 | // MARK: Data
42 |
43 | public func toData() -> Data? {
44 | var data = Data()
45 |
46 | // 1️⃣ Append initial boundary
47 | data.append(BoundaryGenerator.boundaryData(forBoundaryType: .initial, boundary: boundary))
48 |
49 | // 2️⃣ Append each form part
50 | for (index, part) in parts.enumerated() {
51 | var headers = "Content-Disposition: form-data; name=\"\(part.name)\""
52 |
53 | // Include filename if this part is a file
54 | if let filename = part.filename {
55 | headers += "; filename=\"\(filename)\""
56 | }
57 |
58 | headers += Constants.crlf
59 |
60 | // Include Content-Type header if present
61 | headers += "Content-Type: \(part.mimeType.value)\(Constants.crlf)\(Constants.crlf)"
62 |
63 | // Add headers as Data
64 | data.append(Data(headers.utf8))
65 |
66 | // Add the actual data payload
67 | data.append(part.data)
68 |
69 | // If not the last part, add an encapsulated boundary
70 | if index < parts.count - 1 {
71 | data.append(BoundaryGenerator.boundaryData(forBoundaryType: .encapsulated, boundary: boundary))
72 | }
73 | }
74 |
75 | // 3️⃣ Append final boundary
76 | data.append(BoundaryGenerator.boundaryData(forBoundaryType: .final, boundary: boundary))
77 |
78 | return data
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Request/RequestBuilder.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public protocol RequestBuilder {
4 | func setHttpMethod(_ method: HTTPMethod) -> RequestBuilder
5 | func setBaseUrl(_ baseUrl: String) -> RequestBuilder
6 | func setParameters(_ parameters: [HTTPParameter]) -> RequestBuilder
7 | func setHeaders(_ headers: [HTTPHeader]) -> RequestBuilder
8 | func setBody(_ body: HTTPBody) -> RequestBuilder
9 | func setTimeoutInterval(_ timeoutInterval: TimeInterval) -> RequestBuilder
10 | func setCachePolicy(_ cachePolicy: URLRequest.CachePolicy) -> RequestBuilder
11 | func build() -> Request?
12 | }
13 |
14 | public class RequestBuilderImpl: RequestBuilder {
15 | private var httpMethod: HTTPMethod?
16 | private var baseUrlString: String?
17 | private var parameters: [HTTPParameter]?
18 | private var headers: [HTTPHeader]?
19 | private var body: HTTPBody? = nil
20 | private var timeoutInterval: TimeInterval?
21 | private var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
22 |
23 | private let headerEncoder: HTTPHeaderEncoder
24 | private let paramEncoder: HTTPParameterEncoder
25 |
26 | public init(headerEncoder: HTTPHeaderEncoder = HTTPHeaderEncoderImpl(),
27 | paramEncoder: HTTPParameterEncoder = HTTPParameterEncoderImpl()) {
28 | self.headerEncoder = headerEncoder
29 | self.paramEncoder = paramEncoder
30 | }
31 |
32 | public func setHttpMethod(_ method: HTTPMethod) -> RequestBuilder {
33 | self.httpMethod = method
34 | return self
35 | }
36 |
37 | public func setBaseUrl(_ baseUrl: String) -> RequestBuilder {
38 | self.baseUrlString = baseUrl
39 | return self
40 | }
41 |
42 | public func setParameters(_ parameters: [HTTPParameter]) -> RequestBuilder {
43 | self.parameters = parameters
44 | return self
45 | }
46 |
47 | public func setHeaders(_ headers: [HTTPHeader]) -> RequestBuilder {
48 | self.headers = headers
49 | return self
50 | }
51 |
52 | public func setBody(_ body: HTTPBody) -> RequestBuilder {
53 | self.body = body
54 | return self
55 | }
56 |
57 | public func setTimeoutInterval(_ timeoutInterval: TimeInterval) -> RequestBuilder {
58 | self.timeoutInterval = timeoutInterval
59 | return self
60 | }
61 |
62 | public func setCachePolicy(_ cachePolicy: URLRequest.CachePolicy) -> RequestBuilder {
63 | self.cachePolicy = cachePolicy
64 | return self
65 | }
66 |
67 | public func build() -> Request? {
68 | guard let httpMethod, let baseUrlString else { return nil }
69 | return EZRequest(httpMethod: httpMethod, baseUrlString: baseUrlString, parameters: parameters, headers: headers, body: body, timeoutInterval: timeoutInterval ?? 60, cachePolicy: cachePolicy)
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Request/RequestFactoryTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test RequestFactory")
6 | final class RequestFactoryTests {
7 |
8 | @Test("test BuildURLRequest with valid parameters")
9 | func testBuildURLRequestWithValidParameters() {
10 | let builder = RequestFactoryImpl()
11 | let urlString = "https://example.com/api"
12 | let httpMethod = HTTPMethod.POST
13 | let parameters: [HTTPParameter] = [
14 | HTTPParameter(key: "key1", value: "value1"),
15 | HTTPParameter(key: "key2", value: "value2")
16 | ]
17 | let headers: [HTTPHeader] = [
18 | HTTPHeader.contentType(.json),
19 | HTTPHeader.authorization(.bearer("token"))
20 | ]
21 | let body = Data(jsonString: "{\"name\": \"John\"}")
22 | let timeoutInterval: TimeInterval = 30
23 |
24 | let request = builder.build(httpMethod: httpMethod,
25 | baseUrlString: urlString,
26 | parameters: parameters,
27 | headers: headers,
28 | body: body,
29 | timeoutInterval: timeoutInterval)
30 |
31 | #expect(request != nil)
32 | #expect(request.baseUrlString == "https://example.com/api")
33 | #expect(request.httpMethod == httpMethod)
34 | #expect(request.parameters == parameters)
35 | #expect(request.body?.toData() == body)
36 | #expect(request.timeoutInterval == timeoutInterval)
37 | #expect(request.headers == headers)
38 | #expect(request.cachePolicy == .useProtocolCachePolicy)
39 | }
40 |
41 | @Test("test BuildURLRequest with no parameters and headers")
42 | func testBuildURLRequestWithNoParametersAndHeaders() {
43 | let builder = RequestFactoryImpl()
44 | let urlString = "https://example.com/api"
45 | let httpMethod = HTTPMethod.PUT
46 |
47 | let request = builder.build(httpMethod: httpMethod,
48 | baseUrlString: urlString,
49 | parameters: nil,
50 | headers: nil,
51 | body: nil,
52 | timeoutInterval: 60)
53 |
54 | #expect(request != nil)
55 | #expect(request.baseUrlString == "https://example.com/api")
56 | #expect(request.httpMethod == httpMethod)
57 | #expect(request.parameters == nil)
58 | #expect(request.body == nil)
59 | #expect(request.timeoutInterval == 60)
60 | #expect(request.headers == nil)
61 | #expect(request.cachePolicy == .useProtocolCachePolicy)
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/WebSocket/Mocks/MockURLSessionWebSocketTask.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockURLSessionWebSocketTask: WebSocketTaskProtocol {
5 |
6 | init(resumeClosure: @escaping (() -> Void) = {},
7 | sendThrowsError: Bool = false,
8 | receiveMessage: String = "",
9 | receiveThrowsError: Bool = false
10 | ) {
11 | self.resumeClosure = resumeClosure
12 | self.sendThrowsError = sendThrowsError
13 | self.receiveMessage = receiveMessage
14 | self.receiveThrowsError = receiveThrowsError
15 | }
16 |
17 | // MARK: resume()
18 |
19 | var resumeClosure: () -> Void
20 | var didCallResume = false
21 | func resume() {
22 | didCallResume = true
23 | resumeClosure()
24 | }
25 |
26 | // MARK: cancel()
27 |
28 | var didCallCancel = false
29 | var didCancelWithCloseCode: URLSessionWebSocketTask.CloseCode?
30 | var didCancelWithReason: Data?
31 | func cancel(with closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
32 | didCallCancel = true
33 | didCancelWithCloseCode = closeCode
34 | didCancelWithReason = reason
35 | }
36 |
37 | // MARK: send()
38 |
39 | var sendThrowsError: Bool
40 | func send(_ message: URLSessionWebSocketTask.Message, completionHandler: @escaping @Sendable ((any Error)?) -> Void) {
41 | if sendThrowsError {
42 | completionHandler(MockURLSessionWebSocketTaskError.failedToSendMessage)
43 | }
44 | }
45 |
46 | func send(_ message: URLSessionWebSocketTask.Message) async throws {
47 | if sendThrowsError {
48 | throw MockURLSessionWebSocketTaskError.failedToSendMessage
49 | }
50 | }
51 |
52 | // MARK: receive()
53 |
54 | var receiveMessage: String
55 | var receiveThrowsError: Bool
56 | func receive(completionHandler: @escaping @Sendable (Result) -> Void) {
57 | if receiveThrowsError {
58 | completionHandler(.failure(MockURLSessionWebSocketTaskError.failedToReceiveMessage))
59 | } else {
60 | completionHandler(.success(.string(receiveMessage)))
61 | }
62 | }
63 |
64 | func receive() async throws -> URLSessionWebSocketTask.Message {
65 | if receiveThrowsError {
66 | throw MockURLSessionWebSocketTaskError.failedToReceiveMessage
67 | }
68 | return .string(receiveMessage)
69 | }
70 |
71 | // MARK: sendPing
72 |
73 | var shouldFailPing: Bool = false
74 | var pingFailureCount: Int = 0
75 | func sendPing(pongReceiveHandler: @escaping @Sendable ((any Error)?) -> Void) {
76 | if shouldFailPing {
77 | pingFailureCount += 1
78 | pongReceiveHandler(MockURLSessionWebSocketTaskError.pingError)
79 | } else {
80 | pongReceiveHandler(nil)
81 | }
82 | }
83 | }
84 |
85 | enum MockURLSessionWebSocketTaskError: Error {
86 | case pingError
87 | case failedToSendMessage
88 | case failedToReceiveMessage
89 | }
90 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/HTTP/Headers/HTTPHeaderTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPHeader")
5 | final class HTTPHeaderTests {
6 |
7 | @Test("test HTTPHeader .key and .value", arguments: arguments)
8 | func testHTTPHeaderKeyAndValue(header: HTTPHeader, key: String, value: String) {
9 | #expect(header.key == key)
10 | #expect(header.value == value)
11 | }
12 |
13 | private static let arguments: [(header: HTTPHeader, key: String, value: String)] = [
14 | (header: HTTPHeader.accept(.json), key: "Accept", value: "application/json"),
15 | (header: HTTPHeader.accept(.xml), key: "Accept", value: "application/xml"),
16 | (header: HTTPHeader.accept(.formUrlEncoded), key: "Accept", value: "application/x-www-form-urlencoded"),
17 | (header: HTTPHeader.accept(.custom("custom_value")), key: "Accept", value: "custom_value"),
18 | (header: HTTPHeader.acceptCharset("utf-8"), key: "Accept-Charset", value: "utf-8"),
19 | (header: HTTPHeader.acceptEncoding("gzip, deflate"), key: "Accept-Encoding", value: "gzip, deflate"),
20 | (header: HTTPHeader.acceptLanguage("en-US"), key: "Accept-Language", value: "en-US"),
21 | (header: HTTPHeader.authorization(.bearer("abcde")), key: "Authorization", value: "Bearer abcde"),
22 | (header: HTTPHeader.cacheControl("no-cache"), key: "Cache-Control", value: "no-cache"),
23 | (header: HTTPHeader.contentLength("1024"), key: "Content-Length", value: "1024"),
24 | (header: HTTPHeader.contentType(.json), key: "Content-Type", value: "application/json"),
25 | (header: HTTPHeader.contentType(.xml), key: "Content-Type", value: "application/xml"),
26 | (header: HTTPHeader.contentType(.formUrlEncoded), key: "Content-Type", value: "application/x-www-form-urlencoded"),
27 | (header: HTTPHeader.contentType(.custom("custom_value")), key: "Content-Type", value: "custom_value"),
28 | (header: HTTPHeader.cookie("session_id=abcdef123456"), key: "Cookie", value: "session_id=abcdef123456"),
29 | (header: HTTPHeader.host("example.com"), key: "Host", value: "example.com"),
30 | (header: HTTPHeader.ifModifiedSince(sampleDate), key: "If-Modified-Since", value: sampleDate),
31 | (header: HTTPHeader.ifNoneMatch("W/\"123456789\""), key: "If-None-Match", value: "W/\"123456789\""),
32 | (header: HTTPHeader.origin("https://example.com"), key: "Origin", value: "https://example.com"),
33 | (header: HTTPHeader.referer("https://example.com/previous-page"), key: "Referer", value: "https://example.com/previous-page"),
34 | (header: HTTPHeader.userAgent(sampleUserAgent), key: "User-Agent", value: sampleUserAgent),
35 | (header: HTTPHeader.custom(key: "X-Custom-Header", value: "custom-value"), key: "X-Custom-Header", value: "custom-value")
36 | ]
37 |
38 | private static let sampleDate = "Tue, 21 Jul 2024 00:00:00 GMT"
39 | private static let sampleUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
40 | }
41 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Interceptors/SessoionDelegate+Extensions+Tests/SessionDelegate+URLSessionDownloadDelegate+Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test SessionDelegateURLSessionDownloadDelegate")
6 | final class SessionDelegateURLSessionDownloadDelegateTests {
7 |
8 | @Test("test SessionDelegate DidFinishDownloadingTo")
9 | func testSessionDelegateDidFinishDownloadingTo() {
10 | let downloadTaskInterceptor = MockDownloadTaskInterceptor()
11 | let delegate = SessionDelegate()
12 | delegate.downloadTaskInterceptor = downloadTaskInterceptor
13 |
14 | let mockURL = URL(fileURLWithPath: "/tmp/mockFile")
15 | delegate.urlSession(.shared, downloadTask: mockUrlSessionDownloadTask, didFinishDownloadingTo: mockURL)
16 |
17 | #expect(downloadTaskInterceptor.didFinishDownloading)
18 | }
19 |
20 | @Test("test SessionDelegat eDidWriteData")
21 | func testSessionDelegateDidWriteData() {
22 | let downloadTaskInterceptor = MockDownloadTaskInterceptor()
23 | let delegate = SessionDelegate()
24 | delegate.downloadTaskInterceptor = downloadTaskInterceptor
25 |
26 | delegate.urlSession(.shared, downloadTask: mockUrlSessionDownloadTask, didWriteData: 100, totalBytesWritten: 200, totalBytesExpectedToWrite: 1000)
27 |
28 | #expect(downloadTaskInterceptor.didWriteData)
29 | }
30 |
31 | @Test("test SessionDelegate DidResumeAtOffset")
32 | func testSessionDelegateDidResumeAtOffset() {
33 | let downloadTaskInterceptor = MockDownloadTaskInterceptor()
34 | let delegate = SessionDelegate()
35 | delegate.downloadTaskInterceptor = downloadTaskInterceptor
36 |
37 | delegate.urlSession(.shared, downloadTask: mockUrlSessionDownloadTask, didResumeAtOffset: 500, expectedTotalBytes: 1000)
38 |
39 | #expect(downloadTaskInterceptor.didResumeAtOffset)
40 | }
41 |
42 | }
43 | // MARK: mock class
44 |
45 | private class MockDownloadTaskInterceptor: DownloadTaskInterceptor {
46 | var progress: (Double) -> Void = { _ in }
47 |
48 | var didFinishDownloading = false
49 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) {
50 | didFinishDownloading = true
51 | }
52 |
53 | var didWriteData = false
54 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didWriteData bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64) {
55 | didWriteData = true
56 | }
57 |
58 | var didResumeAtOffset = false
59 | func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didResumeAtOffset fileOffset: Int64, expectedTotalBytes: Int64) {
60 | didResumeAtOffset = true
61 | }
62 | }
63 |
64 | // MARK: mock variables
65 |
66 | private var mockUrlSessionDownloadTask: URLSessionDownloadTask {
67 | URLSession.shared.downloadTask(with: URLRequest(url: URL(string: "https://www.example.com")!))
68 | }
69 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Downloader/Mocks/MockFileDownloaderURLSession.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockFileDownloaderURLSession: URLSessionTaskProtocol {
5 | var url: URL?
6 | var data: Data?
7 | var urlResponse: URLResponse?
8 | var error: Error?
9 | var completion: ((Data?, URLResponse?, Error?) -> Void)?
10 | var sessionDelegate: SessionDelegate? = nil
11 |
12 | var progressToExecute: [DownloadProgress] = []
13 |
14 | init(data: Data? = nil, url: URL? = nil, urlResponse: URLResponse? = nil, error: Error? = nil) {
15 | self.data = data
16 | self.url = url
17 | self.urlResponse = urlResponse
18 | self.error = error
19 | }
20 |
21 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, Error?) -> Void) -> URLSessionDownloadTask {
22 |
23 | simulateDownloadProgress(for: .init())
24 |
25 | return MockURLSessionDownloadTask {
26 | completionHandler(URL(fileURLWithPath: "/tmp/test.pdf"), self.urlResponse, self.error)
27 | }
28 | }
29 |
30 | }
31 |
32 | // MARK: Helpers
33 |
34 | extension MockFileDownloaderURLSession {
35 | enum DownloadProgress {
36 | case inProgress(percent: Int64)
37 | case complete
38 | }
39 |
40 | private func simulateDownloadProgress(for task: URLSessionDownloadTask) {
41 |
42 | for progressToExecute in self.progressToExecute {
43 | switch progressToExecute {
44 | case .inProgress(let percent):
45 | // Simulate x% progress
46 | sessionDelegate?.urlSession(
47 | .shared,
48 | downloadTask: task,
49 | didWriteData: 0,
50 | totalBytesWritten: percent,
51 | totalBytesExpectedToWrite: 100
52 | )
53 |
54 | case .complete:
55 | // Simulate completion
56 | sessionDelegate?.urlSession(
57 | .shared,
58 | downloadTask: task,
59 | didFinishDownloadingTo: URL(fileURLWithPath: "/tmp/test.pdf")
60 | )
61 | }
62 | }
63 | }
64 | }
65 |
66 | // MARK: unused methods
67 |
68 | extension MockFileDownloaderURLSession {
69 | func dataTask(with request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionDataTask {
70 | fatalError("Should not be using in this mock")
71 | }
72 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
73 | fatalError("Should not be using in this mock")
74 | }
75 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
76 | fatalError("Should not be using in this mock")
77 | }
78 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
79 | fatalError("Should not be using in this mock")
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Uploader/DataUploader/Mocks/MockDataUploaderURLSession.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockDataUploaderURLSession: URLSessionTaskProtocol {
5 | var data: Data?
6 | var urlResponse: URLResponse?
7 | var error: Error?
8 | var completionHandler: ((Data?, URLResponse?, (any Error)?) -> Void)?
9 |
10 | var sessionDelegate: SessionDelegate? = nil
11 | var progressToExecute: [UploadProgress] = []
12 |
13 | init(data: Data?,
14 | urlResponse: URLResponse? = nil,
15 | error: Error? = nil
16 | ) {
17 | self.data = data
18 | self.urlResponse = urlResponse
19 | self.error = error
20 | }
21 |
22 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
23 | self.completionHandler = completionHandler
24 |
25 | simulateDownloadProgress(for: .init())
26 |
27 | return MockURLSessionUploadTask {
28 | completionHandler(self.data, self.urlResponse, self.error)
29 | }
30 | }
31 | }
32 |
33 | // MARK: Helpers
34 |
35 | extension MockDataUploaderURLSession {
36 | enum UploadProgress {
37 | case inProgress(percent: Int64)
38 | case complete
39 | }
40 |
41 | private func simulateDownloadProgress(for task: URLSessionDownloadTask) {
42 |
43 | for progressToExecute in self.progressToExecute {
44 | switch progressToExecute {
45 | case .inProgress(let percent):
46 | // Simulate x% progress
47 | sessionDelegate?.urlSession(
48 | .shared,
49 | task: task,
50 | didSendBodyData: 0,
51 | totalBytesSent: percent,
52 | totalBytesExpectedToSend: 100
53 | )
54 |
55 | case .complete:
56 | // Simulate completion
57 | sessionDelegate?.urlSession(
58 | .shared,
59 | task: task,
60 | didSendBodyData: 0,
61 | totalBytesSent: 100,
62 | totalBytesExpectedToSend: 100
63 | )
64 | }
65 | }
66 | }
67 | }
68 |
69 | // MARK: unused methods
70 |
71 | extension MockDataUploaderURLSession {
72 | func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask {
73 | fatalError("Should not be using in this mock")
74 | }
75 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, (any Error)?) -> Void) -> URLSessionDownloadTask {
76 | fatalError("Should not be using in this mock")
77 | }
78 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
79 | fatalError("Should not be using in this mock")
80 | }
81 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
82 | fatalError("Should not be using in this mock")
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Uploader/FileUploader/Mocks/MockFileUploaderURLSession.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import EZNetworking
3 |
4 | class MockFileUploaderURLSession: URLSessionTaskProtocol {
5 | var data: Data?
6 | var urlResponse: URLResponse?
7 | var error: Error?
8 | var completionHandler: ((Data?, URLResponse?, (any Error)?) -> Void)?
9 |
10 | var sessionDelegate: SessionDelegate? = nil
11 | var progressToExecute: [UploadProgress] = []
12 |
13 | init(data: Data?,
14 | urlResponse: URLResponse? = nil,
15 | error: Error? = nil
16 | ) {
17 | self.data = data
18 | self.urlResponse = urlResponse
19 | self.error = error
20 | }
21 |
22 | func uploadTask(with request: URLRequest, fromFile fileURL: URL, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
23 | self.completionHandler = completionHandler
24 |
25 | simulateDownloadProgress(for: .init())
26 |
27 | return MockURLSessionUploadTask {
28 | completionHandler(self.data, self.urlResponse, self.error)
29 | }
30 | }
31 | }
32 |
33 | // MARK: Helpers
34 |
35 | extension MockFileUploaderURLSession {
36 | enum UploadProgress {
37 | case inProgress(percent: Int64)
38 | case complete
39 | }
40 |
41 | private func simulateDownloadProgress(for task: URLSessionDownloadTask) {
42 |
43 | for progressToExecute in self.progressToExecute {
44 | switch progressToExecute {
45 | case .inProgress(let percent):
46 | // Simulate x% progress
47 | sessionDelegate?.urlSession(
48 | .shared,
49 | task: task,
50 | didSendBodyData: 0,
51 | totalBytesSent: percent,
52 | totalBytesExpectedToSend: 100
53 | )
54 |
55 | case .complete:
56 | // Simulate completion
57 | sessionDelegate?.urlSession(
58 | .shared,
59 | task: task,
60 | didSendBodyData: 0,
61 | totalBytesSent: 100,
62 | totalBytesExpectedToSend: 100
63 | )
64 | }
65 | }
66 | }
67 | }
68 |
69 | // MARK: unused methods
70 |
71 | extension MockFileUploaderURLSession {
72 | func dataTask(with request: URLRequest, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionDataTask {
73 | fatalError("Should not be using in this mock")
74 | }
75 | func downloadTask(with url: URL, completionHandler: @escaping @Sendable (URL?, URLResponse?, (any Error)?) -> Void) -> URLSessionDownloadTask {
76 | fatalError("Should not be using in this mock")
77 | }
78 | func uploadTask(with request: URLRequest, from bodyData: Data?, completionHandler: @escaping @Sendable (Data?, URLResponse?, (any Error)?) -> Void) -> URLSessionUploadTask {
79 | fatalError("Should not be using in this mock")
80 | }
81 | func webSocketTaskInspectable(with request: URLRequest) -> WebSocketTaskProtocol {
82 | fatalError("Should not be using in this mock")
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Interceptors/SessoionDelegate+Extensions+Tests/SessionDelegate+URLSessionDelegate+Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test SessionDelegateURLSessionDelegate")
6 | final class SessionDelegateURLSessionDelegateTests {
7 |
8 | @Test("test SessionDelegate DidReceiveChallenge")
9 | func testSessionDelegateDidReceiveChallenge() async {
10 | let authenticationInterceptor = MockAuthenticationInterceptor()
11 | let delegate = SessionDelegate()
12 | delegate.authenticationInterceptor = authenticationInterceptor
13 |
14 | let (disposition, credential) = await delegate.urlSession(.shared, didReceive: URLAuthenticationChallenge())
15 | #expect(disposition == .performDefaultHandling)
16 | #expect(credential == nil)
17 | #expect(authenticationInterceptor.didReceiveChallenge)
18 | }
19 |
20 | @Test("test SessionDelegate DidReceiveChallenge with no interceptor")
21 | func testSessionDelegateDidReceiveChallengeWithNoInterceptor() async {
22 | let delegate = SessionDelegate()
23 |
24 | let (disposition, credential) = await delegate.urlSession(.shared, didReceive: URLAuthenticationChallenge())
25 | #expect(disposition == .performDefaultHandling)
26 | #expect(credential == nil)
27 | }
28 |
29 | @Test("test SessionDelegate DidCreateTask")
30 | func testSessionDelegateDidCreateTask() {
31 | let taskLifecycleInterceptor = MockTaskLifecycleInterceptor()
32 | let delegate = SessionDelegate()
33 | delegate.taskLifecycleInterceptor = taskLifecycleInterceptor
34 |
35 | delegate.urlSession(.shared, didCreateTask: mockUrlSessionDataTask)
36 | #expect(taskLifecycleInterceptor.didCreateTask)
37 | }
38 | }
39 |
40 | // MARK: mock classes
41 |
42 | private class MockAuthenticationInterceptor: AuthenticationInterceptor {
43 | var didReceiveChallengeWithTask = false
44 | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
45 | didReceiveChallengeWithTask = true
46 | return (.performDefaultHandling, nil)
47 | }
48 |
49 | var didReceiveChallenge = false
50 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
51 | didReceiveChallenge = true
52 | return (.performDefaultHandling, nil)
53 | }
54 | }
55 |
56 | private class MockTaskLifecycleInterceptor: TaskLifecycleInterceptor {
57 | var didCompleteWithError = false
58 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) {
59 | didCompleteWithError = true
60 | }
61 |
62 | var taskIsWaitingForConnectivity = false
63 | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
64 | taskIsWaitingForConnectivity = true
65 | }
66 |
67 | var didCreateTask = false
68 | func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
69 | didCreateTask = true
70 | }
71 | }
72 |
73 | // MARK: mock variables
74 |
75 | private var mockUrlSessionDataTask: URLSessionDataTask {
76 | URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://www.example.com")!))
77 | }
78 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Interceptors/SessoionDelegate+Extensions+Tests/SessionDelegate+URLSessionStreamDelegate+Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test SessionDelegateURLSessionStreamDelegate")
6 | final class SessionDelegateURLSessionStreamDelegateTests {
7 |
8 | @Test("test SessionDelegate ReadClosedForStreamTask")
9 | func testSessionDelegateReadClosedForStreamTask() {
10 | let streamTaskInterceptor = MockStreamTaskInterceptor()
11 | let delegate = SessionDelegate()
12 | delegate.streamTaskInterceptor = streamTaskInterceptor
13 |
14 | delegate.urlSession(.shared, readClosedFor: mockUrlSessionStreamTask)
15 |
16 | #expect(streamTaskInterceptor.readClosed)
17 | }
18 |
19 | @Test("test SessionDelegate WriteClosedForStreamTask")
20 | func testSessionDelegateWriteClosedForStreamTask() {
21 | let streamTaskInterceptor = MockStreamTaskInterceptor()
22 | let delegate = SessionDelegate()
23 | delegate.streamTaskInterceptor = streamTaskInterceptor
24 |
25 | delegate.urlSession(.shared, writeClosedFor: mockUrlSessionStreamTask)
26 |
27 | #expect(streamTaskInterceptor.writeClosed)
28 | }
29 |
30 | @Test("test SessionDelegate BetterRouteDiscoveredForStreamTask")
31 | func testSessionDelegateBetterRouteDiscoveredForStreamTask() {
32 | let streamTaskInterceptor = MockStreamTaskInterceptor()
33 | let delegate = SessionDelegate()
34 | delegate.streamTaskInterceptor = streamTaskInterceptor
35 |
36 | delegate.urlSession(.shared, betterRouteDiscoveredFor: mockUrlSessionStreamTask)
37 |
38 | #expect(streamTaskInterceptor.betterRouteDiscovered)
39 | }
40 |
41 | @Test("test SessionDelegate StreamTaskDidBecomeStreams")
42 | func testSessionDelegateStreamTaskDidBecomeStreams() {
43 | let streamTaskInterceptor = MockStreamTaskInterceptor()
44 | let delegate = SessionDelegate()
45 | delegate.streamTaskInterceptor = streamTaskInterceptor
46 |
47 | let inputStream = InputStream(data: Data())
48 | let outputStream = OutputStream(toMemory: ())
49 | delegate.urlSession(.shared, streamTask: mockUrlSessionStreamTask, didBecome: inputStream, outputStream: outputStream)
50 |
51 | #expect(streamTaskInterceptor.didBecomeStreams)
52 | }
53 | }
54 |
55 | // MARK: mock classes
56 |
57 | private class MockStreamTaskInterceptor: StreamTaskInterceptor {
58 | var readClosed = false
59 | func urlSession(_ session: URLSession, readClosedFor streamTask: URLSessionStreamTask) {
60 | readClosed = true
61 | }
62 |
63 | var writeClosed = false
64 | func urlSession(_ session: URLSession, writeClosedFor streamTask: URLSessionStreamTask) {
65 | writeClosed = true
66 | }
67 |
68 | var betterRouteDiscovered = false
69 | func urlSession(_ session: URLSession, betterRouteDiscoveredFor streamTask: URLSessionStreamTask) {
70 | betterRouteDiscovered = true
71 | }
72 |
73 | var didBecomeStreams = false
74 | func urlSession(_ session: URLSession, streamTask: URLSessionStreamTask, didBecome inputStream: InputStream, outputStream: OutputStream) {
75 | didBecomeStreams = true
76 | }
77 | }
78 |
79 | // MARK: mock variables
80 |
81 | private var mockUrlSessionStreamTask: URLSessionStreamTask {
82 | URLSession.shared.streamTask(withHostName: "", port: 0)
83 | }
84 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/WebSocket/Helpers/DefaultWebSocketTaskInterceptorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test DefaultWebSocketTaskInterceptor")
6 | final class DefaultWebSocketTaskInterceptorTests {
7 |
8 | @Test("calls onEvent when didOpenWithProtocol is invoked")
9 | func testDidOpenCallsOnEvent() {
10 | let url = URL(string: "wss://example.com/socket")!
11 | let session = URLSession(configuration: .default)
12 | let task = session.webSocketTask(with: url)
13 |
14 | var received: WebSocketTaskEvent? = nil
15 | let interceptor = DefaultWebSocketTaskInterceptor { event in
16 | received = event
17 | }
18 |
19 | interceptor.urlSession(session, webSocketTask: task, didOpenWithProtocol: "chat")
20 |
21 | guard case let .didOpenWithProtocol(proto) = received else {
22 | #expect(Bool(false))
23 | return
24 | }
25 | #expect(proto == "chat")
26 | }
27 |
28 | @Test("calls onEvent when didCloseWith is invoked with code and reason")
29 | func testDidCloseCallsOnEvent() {
30 | let url = URL(string: "wss://example.com/socket")!
31 | let session = URLSession(configuration: .default)
32 | let task = session.webSocketTask(with: url)
33 |
34 | var received: WebSocketTaskEvent? = nil
35 | let interceptor = DefaultWebSocketTaskInterceptor { event in
36 | received = event
37 | }
38 |
39 | let reason = "bye".data(using: .utf8)
40 | interceptor.urlSession(session, webSocketTask: task, didCloseWith: .normalClosure, reason: reason)
41 |
42 | guard case let .didClose(code, data) = received else {
43 | #expect(Bool(false))
44 | return
45 | }
46 | #expect(code == .normalClosure)
47 | #expect(data == reason)
48 | }
49 |
50 | @Test("default init does not crash and onEvent is mutable")
51 | func testDefaultInitAndOnEventMutation() {
52 | let url = URL(string: "wss://example.com/socket")!
53 | let session = URLSession(configuration: .default)
54 | let task = session.webSocketTask(with: url)
55 |
56 | let interceptor = DefaultWebSocketTaskInterceptor()
57 |
58 | var called = false
59 | interceptor.onEvent = { event in
60 | called = true
61 | }
62 |
63 | interceptor.urlSession(session, webSocketTask: task, didOpenWithProtocol: nil)
64 | #expect(called == true)
65 | }
66 |
67 | @Test("calls onEvent when task:didCompleteWithError is invoked")
68 | func testDidCompleteWithErrorCallsOnEvent() {
69 | let url = URL(string: "wss://example.com/socket")!
70 | let session = URLSession(configuration: .default)
71 | let task = session.webSocketTask(with: url)
72 |
73 | var received: WebSocketTaskEvent? = nil
74 | let interceptor = DefaultWebSocketTaskInterceptor { event in
75 | received = event
76 | }
77 |
78 | let error = URLError(.timedOut)
79 | interceptor.urlSession(session, task: task, didCompleteWithError: error)
80 |
81 | guard case let .didOpenWithError(err) = received else {
82 | #expect(Bool(false))
83 | return
84 | }
85 | let receivedNSError = err as NSError
86 | let expectedNSError = error as NSError
87 | #expect(receivedNSError.domain == expectedNSError.domain)
88 | #expect(receivedNSError.code == expectedNSError.code)
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Headers/MimeType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum MimeType: Equatable {
4 | // MARK: - Application Types
5 | case json
6 | case xml
7 | case formUrlEncoded
8 | case multipartFormData(boundary: String)
9 | case pdf
10 | case zip
11 | case octetStream
12 | case javascript
13 | case wasm
14 |
15 | // MARK: - Text Types
16 | case plain
17 | case html
18 | case css
19 | case csv
20 | case rtf
21 | case xmlText
22 |
23 | // MARK: - Image Types
24 | case jpeg
25 | case png
26 | case gif
27 | case webp
28 | case svg
29 | case bmp
30 | case ico
31 | case tiff
32 |
33 | // MARK: - Video Types
34 | case mp4
35 | case avi
36 | case mov
37 | case wmv
38 | case flv
39 | case webm
40 | case mkv
41 | case quicktime
42 |
43 | // MARK: - Audio Types
44 | case mp3
45 | case wav
46 | case ogg
47 | case aac
48 | case flac
49 | case m4a
50 | case wma
51 |
52 | // MARK: - Font Types
53 | case ttf
54 | case otf
55 | case woff
56 | case woff2
57 | case eot
58 |
59 | // MARK: - Custom
60 | case custom(String)
61 |
62 | var value: String {
63 | switch self {
64 | // Application Types
65 | case .json: return "application/json"
66 | case .xml: return "application/xml"
67 | case .formUrlEncoded: return "application/x-www-form-urlencoded"
68 | case .multipartFormData(let boundary): return "multipart/form-data; boundary=\(boundary)"
69 | case .pdf: return "application/pdf"
70 | case .zip: return "application/zip"
71 | case .octetStream: return "application/octet-stream"
72 | case .javascript: return "application/javascript"
73 | case .wasm: return "application/wasm"
74 |
75 | // Text Types
76 | case .plain: return "text/plain"
77 | case .html: return "text/html"
78 | case .css: return "text/css"
79 | case .csv: return "text/csv"
80 | case .rtf: return "text/rtf"
81 | case .xmlText: return "text/xml"
82 |
83 | // Image Types
84 | case .jpeg: return "image/jpeg"
85 | case .png: return "image/png"
86 | case .gif: return "image/gif"
87 | case .webp: return "image/webp"
88 | case .svg: return "image/svg+xml"
89 | case .bmp: return "image/bmp"
90 | case .ico: return "image/x-icon"
91 | case .tiff: return "image/tiff"
92 |
93 | // Video Types
94 | case .mp4: return "video/mp4"
95 | case .avi: return "video/x-msvideo"
96 | case .mov: return "video/quicktime"
97 | case .wmv: return "video/x-ms-wmv"
98 | case .flv: return "video/x-flv"
99 | case .webm: return "video/webm"
100 | case .mkv: return "video/x-matroska"
101 | case .quicktime: return "video/quicktime"
102 |
103 | // Audio Types
104 | case .mp3: return "audio/mpeg"
105 | case .wav: return "audio/wav"
106 | case .ogg: return "audio/ogg"
107 | case .aac: return "audio/aac"
108 | case .flac: return "audio/flac"
109 | case .m4a: return "audio/mp4"
110 | case .wma: return "audio/x-ms-wma"
111 |
112 | // Font Types
113 | case .ttf: return "font/ttf"
114 | case .otf: return "font/otf"
115 | case .woff: return "font/woff"
116 | case .woff2: return "font/woff2"
117 | case .eot: return "application/vnd.ms-fontobject"
118 |
119 | // Custom
120 | case .custom(let value): return value
121 | }
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Interceptors/SessoionDelegate+Extensions+Tests/SessionDelegate+URLSessionWebSocketDelegate+Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test SessionDelegateURLSessionWebSocketDelegate")
6 | final class SessionDelegateURLSessionWebSocketDelegateTests {
7 |
8 | @Test("test SessionDelegate WebSocket DidOpenWithProtocol")
9 | func testSessionDelegateWebSocketDidOpenWithProtocol() {
10 | let webSocketInterceptor = SpyWebSocketTaskInterceptor()
11 | let delegate = SessionDelegate()
12 | delegate.webSocketTaskInterceptor = webSocketInterceptor
13 |
14 | let webSocketTask = mockURLSessionWebSocketTask
15 | let protocolString = "test-protocol"
16 | delegate.urlSession(.shared, webSocketTask: webSocketTask, didOpenWithProtocol: protocolString)
17 |
18 | #expect(webSocketInterceptor.didOpenWithProtocol)
19 | #expect(webSocketInterceptor.receivedProtocol == protocolString)
20 | }
21 |
22 | @Test("test SessionDelegate WebSocket DidCloseWithCodeAndReason")
23 | func testSessionDelegateWebSocketDidCloseWithCodeAndReason() {
24 | let webSocketInterceptor = SpyWebSocketTaskInterceptor()
25 | let delegate = SessionDelegate()
26 | delegate.webSocketTaskInterceptor = webSocketInterceptor
27 |
28 | let closeCode: URLSessionWebSocketTask.CloseCode = .goingAway
29 | let reasonData = "Closed by server".data(using: .utf8)
30 | delegate.urlSession(.shared, webSocketTask: mockURLSessionWebSocketTask, didCloseWith: closeCode, reason: reasonData)
31 |
32 | #expect(webSocketInterceptor.didCloseWithCodeAndReason)
33 | #expect(webSocketInterceptor.receivedCloseCode == closeCode)
34 | #expect(webSocketInterceptor.receivedReason == reasonData)
35 | }
36 |
37 | @Test("test SessionDelegate WebSocket didCompleteWithError")
38 | func testSessionDelegateWebSocketDidCompleteWithError() {
39 | let webSocketInterceptor = SpyWebSocketTaskInterceptor()
40 | let delegate = SessionDelegate()
41 | delegate.webSocketTaskInterceptor = webSocketInterceptor
42 |
43 |
44 | let error = NSError(domain: "test", code: 0)
45 | delegate.urlSession(.shared, task: .init(), didCompleteWithError: error)
46 |
47 | #expect(webSocketInterceptor.didCompleteWithError)
48 | #expect(webSocketInterceptor.receivedError as? NSError == error)
49 | }
50 | }
51 |
52 | // MARK: mock class
53 |
54 | private class SpyWebSocketTaskInterceptor: WebSocketTaskInterceptor {
55 | var onEvent: ((WebSocketTaskEvent) -> Void)? = { _ in }
56 |
57 | var didOpenWithProtocol = false
58 | var receivedProtocol: String?
59 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) {
60 | didOpenWithProtocol = true
61 | receivedProtocol = `protocol`
62 | }
63 |
64 | var didCompleteWithError = false
65 | var receivedError: Error?
66 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: any Error) {
67 | didCompleteWithError = true
68 | receivedError = error
69 | }
70 |
71 | var didCloseWithCodeAndReason = false
72 | var receivedCloseCode: URLSessionWebSocketTask.CloseCode?
73 | var receivedReason: Data?
74 | func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) {
75 | didCloseWithCodeAndReason = true
76 | receivedCloseCode = closeCode
77 | receivedReason = reason
78 | }
79 | }
80 |
81 | // MARK: mock variables
82 |
83 | private var mockURLSessionWebSocketTask: URLSessionWebSocketTask {
84 | URLSession.shared.webSocketTask(with: URL(string: "wss://www.example.com")!)
85 | }
86 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Request/RequestTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test Request")
6 | final class RequestTests {
7 |
8 | @Test("test Request .httpMethod")
9 | func testRequestHttpMethod() {
10 | #expect(MockRequest().httpMethod == .GET)
11 | }
12 |
13 | @Test("test Request .baseUrlString")
14 | func testRequestBaseUrlString() {
15 | #expect(MockRequest().baseUrlString == "https://www.example.com")
16 | }
17 |
18 | @Test("test Request .parameters")
19 | func testRequestParameters() throws {
20 | let sut = MockRequest()
21 | let parameters = try #require(sut.parameters)
22 | #expect(parameters.count == 3)
23 |
24 | let firstParam = try #require(parameters[0])
25 | #expect(firstParam.key == "key_1")
26 | #expect(firstParam.key == "key_1")
27 |
28 | let secondParam = try #require(parameters[1])
29 | #expect(secondParam.key == "key_2")
30 | #expect(secondParam.key == "key_2")
31 |
32 | let thirdParam = try #require(parameters[2])
33 | #expect(thirdParam.key == "key_3")
34 | #expect(thirdParam.key == "key_3")
35 | }
36 |
37 | @Test("test Request .headers")
38 | func testRequestHeaders() throws {
39 | let sut = MockRequest()
40 | let headers = try #require(sut.headers)
41 | #expect(headers.count == 3)
42 |
43 | let firstHeader = try #require(headers[0])
44 | #expect(firstHeader.key == "Accept")
45 | #expect(firstHeader.value == "application/json")
46 |
47 | let secondHeader = try #require(headers[1])
48 | #expect(secondHeader.key == "Content-Type")
49 | #expect(secondHeader.value == "application/json")
50 |
51 | let thirdHeader = try #require(headers[2])
52 | #expect(thirdHeader.key == "Authorization")
53 | #expect(thirdHeader.value == "Bearer api_key")
54 | }
55 |
56 | @Test("test Request .timeoutInterval")
57 | func testRequestTimeoutInterval() {
58 | #expect(MockRequest().timeoutInterval == 60)
59 | }
60 |
61 | @Test("test Request .urlRequest")
62 | func testRequestBuildMethod() throws {
63 | let request = MockRequest()
64 | let sut = try #require(request.urlRequest)
65 |
66 | #expect(sut.url?.absoluteString == "https://www.example.com?key_1=value_1&key_2=value_2&key_3=value_3")
67 | #expect(sut.httpMethod == "GET")
68 | #expect(sut.httpBody == "{\"name\": \"John\"}".data(using: .utf8))
69 | #expect(sut.timeoutInterval == 60)
70 | #expect(sut.value(forHTTPHeaderField: "Content-Type") == "application/json")
71 | #expect(sut.value(forHTTPHeaderField: "Authorization") == "Bearer api_key")
72 | }
73 |
74 | @Test("test Request .cachePolicy")
75 | func testRequestCachePolicy() throws {
76 | let request = MockRequest(cachePolicy: .returnCacheDataElseLoad)
77 | let sut = try #require(request.urlRequest)
78 | #expect(sut.cachePolicy == .returnCacheDataElseLoad)
79 | }
80 |
81 | }
82 |
83 | private struct MockRequest: Request {
84 | var httpMethod: HTTPMethod { .GET }
85 |
86 | var baseUrlString: String { "https://www.example.com" }
87 |
88 | var parameters: [HTTPParameter]? {
89 | [
90 | .init(key: "key_1", value: "value_1"),
91 | .init(key: "key_2", value: "value_2"),
92 | .init(key: "key_3", value: "value_3")
93 | ]
94 | }
95 |
96 | var headers: [HTTPHeader]? {
97 | [
98 | .accept(.json),
99 | .contentType(.json),
100 | .authorization(.bearer("api_key"))
101 | ]
102 | }
103 |
104 | var body: EZNetworking.HTTPBody? {
105 | Data(jsonString: "{\"name\": \"John\"}")
106 | }
107 |
108 | var cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
109 | }
110 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Performers/RequestPerformer.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public struct RequestPerformer: RequestPerformable {
5 | private let urlSession: URLSessionTaskProtocol
6 | private let validator: ResponseValidator
7 | private let requestDecoder: RequestDecodable
8 |
9 | public init(
10 | urlSession: URLSessionTaskProtocol = URLSession.shared,
11 | validator: ResponseValidator = ResponseValidatorImpl(),
12 | requestDecoder: RequestDecodable = RequestDecoder(),
13 | sessionDelegate: SessionDelegate? = nil
14 | ) {
15 | if let urlSession = urlSession as? URLSession {
16 | // If the session already has a delegate, use it (if it's a SessionDelegate)
17 | if let _ = urlSession.delegate as? SessionDelegate {
18 | self.urlSession = urlSession
19 | } else {
20 | // If no delegate or not a SessionDelegate, create one
21 | let newDelegate = sessionDelegate ?? SessionDelegate()
22 | let newSession = URLSession(
23 | configuration: urlSession.configuration,
24 | delegate: newDelegate,
25 | delegateQueue: urlSession.delegateQueue
26 | )
27 | self.urlSession = newSession
28 | }
29 | } else {
30 | // For mocks or custom protocol types
31 | self.urlSession = urlSession
32 | }
33 | self.validator = validator
34 | self.requestDecoder = requestDecoder
35 | }
36 |
37 | // MARK: Async Await
38 | public func perform(request: Request, decodeTo decodableObject: T.Type) async throws -> T {
39 | try await withCheckedThrowingContinuation { continuation in
40 | performDataTask(request: request, decodeTo: decodableObject, completion: { result in
41 | switch result {
42 | case .success(let success):
43 | continuation.resume(returning: success)
44 | case .failure(let failure):
45 | continuation.resume(throwing: failure)
46 | }
47 | })
48 | }
49 | }
50 |
51 | // MARK: Completion Handler
52 | @discardableResult
53 | public func performTask(request: Request, decodeTo decodableObject: T.Type, completion: @escaping ((Result) -> Void)) -> URLSessionDataTask? {
54 | return performDataTask(request: request, decodeTo: decodableObject, completion: completion)
55 | }
56 |
57 | // MARK: Publisher
58 | public func performPublisher(request: Request, decodeTo decodableObject: T.Type) -> AnyPublisher {
59 | Future { promise in
60 | performDataTask(request: request, decodeTo: decodableObject) { result in
61 | promise(result)
62 | }
63 | }
64 | .eraseToAnyPublisher()
65 | }
66 |
67 | // MARK: Core
68 |
69 | @discardableResult
70 | private func performDataTask(request: Request, decodeTo decodableObject: T.Type, completion: @escaping ((Result) -> Void)) -> URLSessionDataTask? {
71 |
72 | guard let urlRequest = request.urlRequest else {
73 | completion(.failure(.internalError(.noRequest)))
74 | return nil
75 | }
76 | let task = urlSession.dataTask(with: urlRequest) { data, urlResponse, error in
77 | do {
78 | try validator.validateNoError(error)
79 | try validator.validateStatus(from: urlResponse)
80 | let validData = try validator.validateData(data)
81 |
82 | let result = try requestDecoder.decode(decodableObject, from: validData)
83 | completion(.success(result))
84 | } catch {
85 | completion(.failure(mapError(error)))
86 | }
87 | }
88 | task.resume()
89 | return task
90 | }
91 |
92 | private func mapError(_ error: Error) -> NetworkingError {
93 | if let networkError = error as? NetworkingError { return networkError }
94 | if let urlError = error as? URLError { return .urlError(urlError) }
95 | return .internalError(.unknown)
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/HTTP/Headers/AuthorizationType.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum AuthorizationType: Equatable {
4 | // MARK: - Standard Authorization Types
5 |
6 | /// Bearer token authentication (OAuth 2.0, JWT)
7 | case bearer(String)
8 |
9 | /// Basic authentication (username:password base64 encoded)
10 | case basic(String)
11 |
12 | /// Digest authentication
13 | case digest(String)
14 |
15 | /// API Key authentication
16 | case apiKey(String)
17 |
18 | /// OAuth 1.0 authentication
19 | case oauth1(String)
20 |
21 | /// OAuth 2.0 with different token types
22 | case oauth2(String, tokenType: String = "Bearer")
23 |
24 | /// AWS Signature Version 4
25 | case aws4(String)
26 |
27 | /// Hawk authentication
28 | case hawk(String)
29 |
30 | /// Custom authorization header value
31 | case custom(String)
32 |
33 | // MARK: - Computed Properties
34 |
35 | var value: String {
36 | switch self {
37 | // Standard Authorization Types
38 | case .bearer(let token):
39 | return "Bearer \(token)"
40 |
41 | case .basic(let credentials):
42 | return "Basic \(credentials)"
43 |
44 | case .digest(let credentials):
45 | return "Digest \(credentials)"
46 |
47 | case .apiKey(let key):
48 | return "ApiKey \(key)"
49 |
50 | case .oauth1(let credentials):
51 | return "OAuth \(credentials)"
52 |
53 | case .oauth2(let token, let tokenType):
54 | return "\(tokenType) \(token)"
55 |
56 | case .aws4(let signature):
57 | return "AWS4-HMAC-SHA256 \(signature)"
58 |
59 | case .hawk(let credentials):
60 | return "Hawk \(credentials)"
61 |
62 | case .custom(let value):
63 | return value
64 | }
65 | }
66 |
67 | // MARK: - Convenience Initializers
68 |
69 | /// Creates a Basic authorization with username and password
70 | public static func basic(username: String, password: String) -> AuthorizationType {
71 | let credentials = "\(username):\(password)"
72 | let base64Credentials = Data(credentials.utf8).base64EncodedString()
73 | return .basic(base64Credentials)
74 | }
75 |
76 | /// Creates an API Key authorization with a specific header name
77 | public static func apiKeyWithHeader(_ key: String, headerName: String = "X-API-Key") -> AuthorizationType {
78 | return .custom("\(headerName) \(key)")
79 | }
80 |
81 | /// Creates a custom authorization with a specific scheme
82 | public static func custom(scheme: String, credentials: String) -> AuthorizationType {
83 | return .custom("\(scheme) \(credentials)")
84 | }
85 | }
86 |
87 | // MARK: - AuthorizationType Extensions
88 |
89 | extension AuthorizationType {
90 | /// Returns the authorization scheme (e.g., "Bearer", "Basic", "Digest")
91 | public var scheme: String {
92 | switch self {
93 | case .bearer:
94 | return "Bearer"
95 | case .basic:
96 | return "Basic"
97 | case .digest:
98 | return "Digest"
99 | case .apiKey:
100 | return "ApiKey"
101 | case .oauth1:
102 | return "OAuth"
103 | case .oauth2(_, let tokenType):
104 | return tokenType
105 | case .aws4:
106 | return "AWS4-HMAC-SHA256"
107 | case .hawk:
108 | return "Hawk"
109 | case .custom(let value):
110 | // Extract scheme from custom value (everything before first space)
111 | if let spaceIndex = value.firstIndex(of: " ") {
112 | return String(value[.. CachedURLResponse? {
81 | didCallWillCacheResponse = true
82 | return proposedResponse
83 | }
84 | }
85 |
86 | private class MockDataTaskInterceptor: DataTaskInterceptor {
87 | var didRecieveData = false
88 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
89 | didRecieveData = true
90 | }
91 |
92 | var didBecomeDownloadTask = false
93 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome downloadTask: URLSessionDownloadTask) {
94 | didBecomeDownloadTask = true
95 | }
96 |
97 | var didBecomeStreamTask = false
98 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didBecome streamTask: URLSessionStreamTask) {
99 | didBecomeStreamTask = true
100 | }
101 |
102 | var didReceiveResponse = false
103 | func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive response: URLResponse) async -> URLSession.ResponseDisposition {
104 | didReceiveResponse = true
105 | return .allow
106 | }
107 | }
108 |
109 | // MARK: mock variables
110 |
111 | private var mockUrlSessionDataTask: URLSessionDataTask {
112 | URLSession.shared.dataTask(with: URLRequest(url: URL(string: "https://www.example.com")!))
113 | }
114 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Validator/ResponseValidatorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test URLResponseValidator")
6 | final class URLResponseValidatorTests {
7 |
8 | let sut = ResponseValidatorImpl()
9 |
10 | private struct SomeUnknownError: Error {}
11 |
12 | // MARK: - test validateNoError()
13 |
14 | @Test("test validateNoError givenNilError NoThrow")
15 | func test_validateNoError_givenNilError_NoThrow() throws {
16 | #expect(throws: Never.self) { try sut.validateNoError(nil) }
17 | }
18 |
19 | @Test("test validateNoError givenURLError Throws")
20 | func test_validateNoError_givenURLError_Throws() throws {
21 | #expect(throws: NetworkingError.urlError(URLError(.notConnectedToInternet)).self) {
22 | try sut.validateNoError(URLError(.notConnectedToInternet))
23 | }
24 | }
25 |
26 | @Test("test validateNoError givenClientError Throws")
27 | func test_validateNoError_givenClientError_Throws() throws {
28 | #expect(throws: NetworkingError.internalError(.requestFailed(SomeUnknownError())).self) {
29 | try sut.validateNoError(SomeUnknownError())
30 | }
31 | }
32 |
33 | // MARK: - test validateData()
34 |
35 | @Test("test validateData givenData NoThrow")
36 | func test_validateData_givenData_NoThrow() throws {
37 | #expect(throws: Never.self) { try sut.validateData(MockData.mockPersonJsonData) }
38 | }
39 |
40 | @Test("test validateData givenNilData Throws")
41 | func test_validateData_givenNilData_Throws() throws {
42 | #expect(throws: NetworkingError.internalError(.noData).self) {
43 | try sut.validateData(nil)
44 | }
45 | }
46 |
47 | // MARK: - test validateUrl()
48 |
49 | @Test("test validateUrl givenData NoThrow")
50 | func test_validateUrl_givenData_NoThrow() throws {
51 | #expect(throws: Never.self) { try sut.validateUrl(URL(string: "https://www.example.com")!) }
52 | }
53 |
54 | @Test("test validateUrl givenNilData Throws")
55 | func test_validateUrl_givenNilData_Throws() throws {
56 | #expect(throws: NetworkingError.internalError(.noURL).self) {
57 | try sut.validateUrl(nil)
58 | }
59 | }
60 |
61 | // MARK: - test validateStatus()
62 |
63 | @Test("test validateStatus givenNilResponse Throws")
64 | func test_validateStatus_givenNilResponse_Throws() throws {
65 | #expect(throws: NetworkingError.internalError(.noResponse).self) {
66 | try sut.validateStatus(from: nil)
67 | }
68 | }
69 |
70 | @Test("test validateStatus givenURLResponse Throws")
71 | func test_validateStatus_givenURLResponse_Throws() throws {
72 | #expect(throws: NetworkingError.internalError(.noHTTPURLResponse).self) {
73 | try sut.validateStatus(from: URLResponse())
74 | }
75 | }
76 |
77 | // MARK: 1xx status code
78 |
79 | @Test("test validateStatus givenHTTPURLResponseStatusCode100 Throws")
80 | func test_validateStatus_givenHTTPURLResponseStatusCode100_Throws() throws {
81 | #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 100)).self) {
82 | try sut.validateStatus(from: createHttpUrlResponse(statusCode: 100))
83 | }
84 | }
85 |
86 | // MARK: 2xx status code
87 |
88 | @Test("test validateStatus givenHTTPURLResponseStatusCode200 NoThrow")
89 | func test_validateStatus_givenHTTPURLResponseStatusCode200_NoThrow() throws {
90 | #expect(throws: Never.self) { try sut.validateStatus(from: createHttpUrlResponse(statusCode: 200)) }
91 | }
92 |
93 | // MARK: 3xx status code
94 |
95 | @Test("test validateStatus givenHTTPURLResponseStatusCode300 Throws")
96 | func test_validateStatus_givenHTTPURLResponseStatusCode300_Throws() throws {
97 | #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 300)).self) {
98 | try sut.validateStatus(from: createHttpUrlResponse(statusCode: 300))
99 | }
100 | }
101 |
102 | // MARK: 4xx status code
103 |
104 | @Test("test validateStatus givenHTTPURLResponseStatusCode400 Throws")
105 | func test_validateStatus_givenHTTPURLResponseStatusCode400_Throws() throws {
106 | #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 400)).self) {
107 | try sut.validateStatus(from: createHttpUrlResponse(statusCode: 400))
108 | }
109 | }
110 |
111 | // MARK: 5xx status code
112 |
113 | @Test("test validateStatus givenHTTPURLResponseStatusCode500 Throws")
114 | func test_validateStatus_givenHTTPURLResponseStatusCode500_Throws() throws {
115 | #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 500)).self) {
116 | try sut.validateStatus(from: createHttpUrlResponse(statusCode: 500))
117 | }
118 | }
119 | }
120 |
121 | // MARK: - Test Helpers
122 |
123 | extension URLResponseValidatorTests {
124 | func createHttpUrlResponse(statusCode: Int) -> HTTPURLResponse {
125 | return HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)!
126 | }
127 |
128 | var url: URL {
129 | return URL(string: "https://example.com")!
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/DataUploader/DataUploader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 |
4 | public class DataUploader: DataUploadable {
5 | private let urlSession: URLSessionTaskProtocol
6 | private let validator: ResponseValidator
7 | private var sessionDelegate: SessionDelegate
8 |
9 | private let fallbackUploadTaskInterceptor: DefaultUploadTaskInterceptor = DefaultUploadTaskInterceptor()
10 |
11 | // MARK: init
12 |
13 | public init(
14 | urlSession: URLSessionTaskProtocol = URLSession.shared,
15 | validator: ResponseValidator = ResponseValidatorImpl(),
16 | sessionDelegate: SessionDelegate? = nil
17 | ) {
18 | if let urlSession = urlSession as? URLSession {
19 | if let existingDelegate = urlSession.delegate as? SessionDelegate {
20 | self.sessionDelegate = existingDelegate
21 | self.urlSession = urlSession
22 | } else {
23 | let newDelegate = sessionDelegate ?? SessionDelegate()
24 | let newSession = URLSession(
25 | configuration: urlSession.configuration,
26 | delegate: newDelegate,
27 | delegateQueue: urlSession.delegateQueue
28 | )
29 | self.sessionDelegate = newDelegate
30 | self.urlSession = newSession
31 | }
32 | } else {
33 | self.sessionDelegate = sessionDelegate ?? SessionDelegate()
34 | self.urlSession = urlSession
35 | }
36 | self.validator = validator
37 | }
38 |
39 | // MARK: Async Await
40 |
41 | public func uploadData(_ data: Data, with request: Request, progress: UploadProgressHandler?) async throws -> Data {
42 | try await withCheckedThrowingContinuation { continuation in
43 | self._uploadDataTask(data, with: request, progress: progress) { result in
44 | switch result {
45 | case .success(let data):
46 | continuation.resume(returning: data)
47 | case .failure(let error):
48 | continuation.resume(throwing: error)
49 | }
50 | }
51 | }
52 | }
53 |
54 | // MARK: Completion Handler
55 |
56 | @discardableResult
57 | public func uploadDataTask(_ data: Data, with request: Request, progress: UploadProgressHandler?, completion: @escaping (UploadCompletionHandler)) -> URLSessionUploadTask? {
58 | return _uploadDataTask(data, with: request, progress: progress, completion: completion)
59 | }
60 |
61 | // MARK: Publisher
62 |
63 | public func uploadDataPublisher(_ data: Data, with request: Request, progress: UploadProgressHandler?) -> AnyPublisher {
64 | Future { promise in
65 | _ = self._uploadDataTask(data, with: request, progress: progress) { result in
66 | promise(result)
67 | }
68 | }
69 | .eraseToAnyPublisher()
70 | }
71 |
72 | // MARK: Async Stream
73 |
74 | public func uploadDataStream(_ data: Data, with request: Request) -> AsyncStream {
75 | AsyncStream { continuation in
76 | let progressHandler: UploadProgressHandler = { progress in
77 | continuation.yield(.progress(progress))
78 | }
79 | let task = self._uploadDataTask(data, with: request, progress: progressHandler) { result in
80 | switch result {
81 | case .success(let data):
82 | continuation.yield(.success(data))
83 | case .failure(let error):
84 | continuation.yield(.failure(error))
85 | }
86 | continuation.finish()
87 | }
88 | continuation.onTermination = { @Sendable _ in
89 | task?.cancel()
90 | }
91 | }
92 | }
93 |
94 | // MARK: Core
95 |
96 | @discardableResult
97 | private func _uploadDataTask(_ data: Data, with request: Request, progress: UploadProgressHandler?, completion: @escaping (UploadCompletionHandler)) -> URLSessionUploadTask? {
98 | let request = request
99 | configureProgressTracking(progress: progress)
100 |
101 | guard let urlRequest = request.urlRequest else {
102 | completion(.failure(.internalError(.noRequest)))
103 | return nil
104 | }
105 |
106 | let task = urlSession.uploadTask(with: urlRequest, from: data) { [weak self] data, response, error in
107 | guard let self else {
108 | completion(.failure(.internalError(.lostReferenceOfSelf)))
109 | return
110 | }
111 | do {
112 | try self.validator.validateNoError(error)
113 | try self.validator.validateStatus(from: response)
114 | let validData = try self.validator.validateData(data)
115 | completion(.success(validData))
116 | } catch {
117 | completion(.failure(self.mapError(error)))
118 | }
119 | }
120 | task.resume()
121 | return task
122 | }
123 |
124 | private func mapError(_ error: Error) -> NetworkingError {
125 | if let networkError = error as? NetworkingError { return networkError }
126 | if let urlError = error as? URLError { return .urlError(urlError) }
127 | return .internalError(.unknown)
128 | }
129 |
130 | private func configureProgressTracking(progress: UploadProgressHandler?) {
131 | guard let progress else { return }
132 | if sessionDelegate.uploadTaskInterceptor != nil {
133 | sessionDelegate.uploadTaskInterceptor?.progress = progress
134 | } else {
135 | fallbackUploadTaskInterceptor.progress = progress
136 | sessionDelegate.uploadTaskInterceptor = fallbackUploadTaskInterceptor
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Uploader/FileUploader/FileUploader.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 | import Combine
3 |
4 | public class FileUploader: FileUploadable {
5 | private let urlSession: URLSessionTaskProtocol
6 | private let validator: ResponseValidator
7 | private var sessionDelegate: SessionDelegate
8 |
9 | private let fallbackUploadTaskInterceptor: DefaultUploadTaskInterceptor = DefaultUploadTaskInterceptor()
10 |
11 | // MARK: init
12 |
13 | public init(
14 | urlSession: URLSessionTaskProtocol = URLSession.shared,
15 | validator: ResponseValidator = ResponseValidatorImpl(),
16 | sessionDelegate: SessionDelegate? = nil
17 | ) {
18 | if let urlSession = urlSession as? URLSession {
19 | if let existingDelegate = urlSession.delegate as? SessionDelegate {
20 | self.sessionDelegate = existingDelegate
21 | self.urlSession = urlSession
22 | } else {
23 | let newDelegate = sessionDelegate ?? SessionDelegate()
24 | let newSession = URLSession(
25 | configuration: urlSession.configuration,
26 | delegate: newDelegate,
27 | delegateQueue: urlSession.delegateQueue
28 | )
29 | self.sessionDelegate = newDelegate
30 | self.urlSession = newSession
31 | }
32 | } else {
33 | self.sessionDelegate = sessionDelegate ?? SessionDelegate()
34 | self.urlSession = urlSession
35 | }
36 | self.validator = validator
37 | }
38 |
39 | // MARK: Async Await
40 |
41 | public func uploadFile(_ fileURL: URL, with request: any Request, progress: UploadProgressHandler?) async throws -> Data {
42 | try await withCheckedThrowingContinuation { continuation in
43 | self._uploadFileTask(fileURL, with: request, progress: progress) { result in
44 | switch result {
45 | case .success(let data):
46 | continuation.resume(returning: data)
47 | case .failure(let error):
48 | continuation.resume(throwing: error)
49 | }
50 | }
51 | }
52 |
53 | }
54 |
55 | // MARK: Completion Handler
56 |
57 | @discardableResult
58 | public func uploadFileTask(_ fileURL: URL, with request: any Request, progress: UploadProgressHandler?, completion: @escaping UploadCompletionHandler) -> URLSessionUploadTask? {
59 | return _uploadFileTask(fileURL, with: request, progress: progress, completion: completion)
60 | }
61 |
62 | // MARK: Publisher
63 |
64 | public func uploadFilePublisher(_ fileURL: URL, with request: any Request, progress: UploadProgressHandler?) -> AnyPublisher {
65 | Future { promise in
66 | _ = self._uploadFileTask(fileURL, with: request, progress: progress) { result in
67 | promise(result)
68 | }
69 | }
70 | .eraseToAnyPublisher()
71 | }
72 |
73 | // MARK: AsyncStream
74 |
75 | public func uploadFileStream(_ fileURL: URL, with request: any Request) -> AsyncStream {
76 | AsyncStream { continuation in
77 | let progressHandler: UploadProgressHandler = { progress in
78 | continuation.yield(.progress(progress))
79 | }
80 | let task = self._uploadFileTask(fileURL, with: request, progress: progressHandler) { result in
81 | switch result {
82 | case .success(let data):
83 | continuation.yield(.success(data))
84 | case .failure(let error):
85 | continuation.yield(.failure(error))
86 | }
87 | continuation.finish()
88 | }
89 | continuation.onTermination = { @Sendable _ in
90 | task?.cancel()
91 | }
92 | }
93 | }
94 |
95 | // MARK: - Core
96 |
97 | private func _uploadFileTask(_ fileURL: URL, with request: Request, progress: UploadProgressHandler?, completion: @escaping (UploadCompletionHandler)) -> URLSessionUploadTask? {
98 | let request = request
99 | configureProgressTracking(progress: progress)
100 |
101 | guard let urlRequest = request.urlRequest else {
102 | completion(.failure(.internalError(.noRequest)))
103 | return nil
104 | }
105 |
106 | let task = urlSession.uploadTask(with: urlRequest, fromFile: fileURL) { [weak self] data, response, error in
107 | guard let self else {
108 | completion(.failure(.internalError(.lostReferenceOfSelf)))
109 | return
110 | }
111 | do {
112 | try self.validator.validateNoError(error)
113 | try self.validator.validateStatus(from: response)
114 | let validData = try self.validator.validateData(data)
115 | completion(.success(validData))
116 | } catch {
117 | completion(.failure(self.mapError(error)))
118 | }
119 | }
120 | task.resume()
121 | return task
122 | }
123 |
124 | private func mapError(_ error: Error) -> NetworkingError {
125 | if let networkError = error as? NetworkingError { return networkError }
126 | if let urlError = error as? URLError { return .urlError(urlError) }
127 | return .internalError(.unknown)
128 | }
129 |
130 | private func configureProgressTracking(progress: UploadProgressHandler?) {
131 | guard let progress else { return }
132 | if sessionDelegate.uploadTaskInterceptor != nil {
133 | sessionDelegate.uploadTaskInterceptor?.progress = progress
134 | } else {
135 | fallbackUploadTaskInterceptor.progress = progress
136 | sessionDelegate.uploadTaskInterceptor = fallbackUploadTaskInterceptor
137 | }
138 | }
139 | }
140 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Error/WebSocketError/WebSocketError.swift:
--------------------------------------------------------------------------------
1 | import Foundation
2 |
3 | public enum WebSocketError: Error {
4 | // Connection errors
5 | case notConnected
6 | case stillConnecting
7 | case alreadyConnected
8 | case connectionFailed(underlying: Error)
9 | case connectionTimeout
10 | case invalidURL
11 | case unsupportedProtocol(String)
12 |
13 | // Communication errors
14 | case sendFailed(underlying: Error)
15 | case receiveFailed(underlying: Error)
16 | case invalidMessageFormat
17 | case messageEncodingFailed
18 | case messageDecodingFailed
19 |
20 | // Ping/pong errors
21 | case pingFailed(underlying: Error)
22 | case pongTimeout
23 | case keepAliveFailure(consecutiveFailures: Int)
24 |
25 | // Disconnection errors
26 | case unexpectedDisconnection(code: URLSessionWebSocketTask.CloseCode, reason: String?)
27 | case forcedDisconnection
28 |
29 | // Task errors
30 | case taskNotInitialized
31 | case taskCancelled
32 |
33 | // Stream errors
34 | case streamAlreadyCreated
35 | case streamNotAvailable
36 | }
37 |
38 | // MARK: - LocalizedError conformance for better error messages
39 |
40 | extension WebSocketError: LocalizedError {
41 | public var errorDescription: String? {
42 | switch self {
43 | case .notConnected:
44 | return "WebSocket is not connected"
45 | case .stillConnecting:
46 | return "WebSocket is still connecting"
47 | case .alreadyConnected:
48 | return "WebSocket is already connected"
49 | case .connectionFailed(let error):
50 | return "WebSocket connection failed: \(error.localizedDescription)"
51 | case .connectionTimeout:
52 | return "WebSocket connection timed out"
53 | case .invalidURL:
54 | return "Invalid WebSocket URL"
55 | case .unsupportedProtocol(let protocolString):
56 | return "Unsupported WebSocket protocol: \(protocolString)"
57 |
58 | case .sendFailed(let error):
59 | return "Failed to send WebSocket message: \(error.localizedDescription)"
60 | case .receiveFailed(let error):
61 | return "Failed to receive WebSocket message: \(error.localizedDescription)"
62 | case .invalidMessageFormat:
63 | return "Invalid WebSocket message format"
64 | case .messageEncodingFailed:
65 | return "Failed to encode message for WebSocket"
66 | case .messageDecodingFailed:
67 | return "Failed to decode WebSocket message"
68 |
69 | case .pingFailed(let error):
70 | return "WebSocket ping failed: \(error.localizedDescription)"
71 | case .pongTimeout:
72 | return "WebSocket pong response timed out"
73 | case .keepAliveFailure(let count):
74 | return "WebSocket keep-alive failed after \(count) consecutive attempts"
75 |
76 | case .unexpectedDisconnection(let code, let reason):
77 | let reasonText = reason ?? "No reason provided"
78 | return "WebSocket disconnected unexpectedly with code \(code.rawValue): \(reasonText)"
79 | case .forcedDisconnection:
80 | return "WebSocket was forcefully disconnected"
81 |
82 | case .taskNotInitialized:
83 | return "WebSocket task is not initialized"
84 | case .taskCancelled:
85 | return "WebSocket task was cancelled"
86 |
87 | case .streamAlreadyCreated:
88 | return "WebSocket message stream has already been created"
89 | case .streamNotAvailable:
90 | return "WebSocket message stream is not available"
91 | }
92 | }
93 | }
94 |
95 | // MARK: - CustomStringConvertible for debugging
96 |
97 | extension WebSocketError: CustomStringConvertible {
98 | public var description: String {
99 | errorDescription ?? "Unknown WebSocket error"
100 | }
101 | }
102 |
103 | // MARK: - Equatable (useful for testing)
104 |
105 | extension WebSocketError: Equatable {
106 | public static func == (lhs: WebSocketError, rhs: WebSocketError) -> Bool {
107 | switch (lhs, rhs) {
108 | case (.notConnected, .notConnected),
109 | (.stillConnecting, .stillConnecting),
110 | (.alreadyConnected, .alreadyConnected),
111 | (.connectionTimeout, .connectionTimeout),
112 | (.invalidURL, .invalidURL),
113 | (.invalidMessageFormat, .invalidMessageFormat),
114 | (.messageEncodingFailed, .messageEncodingFailed),
115 | (.messageDecodingFailed, .messageDecodingFailed),
116 | (.pongTimeout, .pongTimeout),
117 | (.forcedDisconnection, .forcedDisconnection),
118 | (.taskNotInitialized, .taskNotInitialized),
119 | (.taskCancelled, .taskCancelled),
120 | (.streamAlreadyCreated, .streamAlreadyCreated),
121 | (.streamNotAvailable, .streamNotAvailable):
122 | return true
123 |
124 | case (.unsupportedProtocol(let lhsProto), .unsupportedProtocol(let rhsProto)):
125 | return lhsProto == rhsProto
126 |
127 | case (.keepAliveFailure(let lhsCount), .keepAliveFailure(let rhsCount)):
128 | return lhsCount == rhsCount
129 |
130 | case (.unexpectedDisconnection(let lhsCode, let lhsReason),
131 | .unexpectedDisconnection(let rhsCode, let rhsReason)):
132 | return lhsCode == rhsCode && lhsReason == rhsReason
133 |
134 | // For errors with underlying errors, compare type names
135 | case (.connectionFailed(let lhsError), .connectionFailed(let rhsError)),
136 | (.sendFailed(let lhsError), .sendFailed(let rhsError)),
137 | (.receiveFailed(let lhsError), .receiveFailed(let rhsError)),
138 | (.pingFailed(let lhsError), .pingFailed(let rhsError)):
139 | return (lhsError as NSError) == (rhsError as NSError)
140 |
141 | default:
142 | return false
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/Sources/EZNetworking/Util/Downloader/FileDownloader.swift:
--------------------------------------------------------------------------------
1 | import Combine
2 | import Foundation
3 |
4 | public class FileDownloader: FileDownloadable {
5 | private let urlSession: URLSessionTaskProtocol
6 | private let validator: ResponseValidator
7 | private let requestDecoder: RequestDecodable
8 | private var sessionDelegate: SessionDelegate
9 |
10 | private let fallbackDownloadTaskInterceptor: DownloadTaskInterceptor = DefaultDownloadTaskInterceptor()
11 |
12 | // MARK: init
13 |
14 | public init(
15 | urlSession: URLSessionTaskProtocol = URLSession.shared,
16 | validator: ResponseValidator = ResponseValidatorImpl(),
17 | requestDecoder: RequestDecodable = RequestDecoder(),
18 | sessionDelegate: SessionDelegate? = nil // Now optional!
19 | ) {
20 | if let urlSession = urlSession as? URLSession {
21 | // If the session already has a delegate, use it (if it's a SessionDelegate)
22 | if let existingDelegate = urlSession.delegate as? SessionDelegate {
23 | self.sessionDelegate = existingDelegate
24 | self.urlSession = urlSession
25 | } else {
26 | // If no delegate or not a SessionDelegate, create one
27 | let newDelegate = sessionDelegate ?? SessionDelegate()
28 | let newSession = URLSession(
29 | configuration: urlSession.configuration,
30 | delegate: newDelegate,
31 | delegateQueue: urlSession.delegateQueue
32 | )
33 | self.sessionDelegate = newDelegate
34 | self.urlSession = newSession
35 | }
36 | } else {
37 | // For mocks or custom protocol types
38 | self.sessionDelegate = sessionDelegate ?? SessionDelegate()
39 | self.urlSession = urlSession
40 | }
41 | self.validator = validator
42 | self.requestDecoder = requestDecoder
43 | }
44 |
45 | // MARK: Async Await
46 |
47 | public func downloadFile(from serverUrl: URL, progress: DownloadProgressHandler? = nil) async throws -> URL {
48 | try await withCheckedThrowingContinuation { continuation in
49 | performDownloadTask(url: serverUrl, progress: progress) { result in
50 | switch result {
51 | case .success(let success):
52 | continuation.resume(returning: success)
53 | case .failure(let error):
54 | continuation.resume(throwing: error)
55 | }
56 | }
57 | }
58 | }
59 |
60 | // MARK: Completion Handler
61 |
62 | @discardableResult
63 | public func downloadFileTask(from serverUrl: URL, progress: DownloadProgressHandler?, completion: @escaping(DownloadCompletionHandler)) -> URLSessionDownloadTask {
64 | return performDownloadTask(url: serverUrl, progress: progress, completion: completion)
65 | }
66 |
67 | // MARK: Publisher
68 |
69 | public func downloadFilePublisher(from serverUrl: URL, progress: DownloadProgressHandler?) -> AnyPublisher {
70 | Future { promise in
71 | self.performDownloadTask(url: serverUrl, progress: progress) { result in
72 | promise(result)
73 | }
74 | }
75 | .eraseToAnyPublisher()
76 | }
77 |
78 | // MARK: AsyncStream
79 |
80 | public func downloadFileStream(from serverUrl: URL) -> AsyncStream {
81 | AsyncStream { continuation in
82 | // Progress handler yields progress updates to the stream.
83 | let progressHandler: DownloadProgressHandler = { progress in
84 | continuation.yield(.progress(progress))
85 | }
86 | // Start the download task, yielding completion to the stream.
87 | let task = self.performDownloadTask(url: serverUrl, progress: progressHandler) { result in
88 | switch result {
89 | case .success(let url):
90 | continuation.yield(.success(url))
91 | case .failure(let error):
92 | continuation.yield(.failure(error))
93 | }
94 | continuation.finish()
95 | }
96 | // Cancel the task if the stream is terminated.
97 | continuation.onTermination = { @Sendable _ in
98 | task.cancel()
99 | }
100 | }
101 | }
102 |
103 | // MARK: - Core
104 |
105 | @discardableResult
106 | private func performDownloadTask(url: URL, progress: DownloadProgressHandler?, completion: @escaping(DownloadCompletionHandler)) -> URLSessionDownloadTask {
107 | configureProgressTracking(progress: progress)
108 |
109 | let task = urlSession.downloadTask(with: url) { [weak self] localURL, response, error in
110 | guard let self else {
111 | completion(.failure(.internalError(.lostReferenceOfSelf)))
112 | return
113 | }
114 | do {
115 | try self.validator.validateNoError(error)
116 | try self.validator.validateStatus(from: response)
117 | let localURL = try self.validator.validateUrl(localURL)
118 |
119 | completion(.success(localURL))
120 | } catch {
121 | completion(.failure(mapError(error)))
122 | }
123 | }
124 | task.resume()
125 | return task
126 | }
127 |
128 | private func mapError(_ error: Error) -> NetworkingError {
129 | if let networkError = error as? NetworkingError { return networkError }
130 | if let urlError = error as? URLError { return .urlError(urlError) }
131 | return .internalError(.unknown)
132 | }
133 |
134 | private func configureProgressTracking(progress: DownloadProgressHandler?) {
135 | guard let progress else { return }
136 |
137 | if sessionDelegate.downloadTaskInterceptor != nil {
138 | // Update existing interceptor's progress handler
139 | sessionDelegate.downloadTaskInterceptor?.progress = progress
140 | } else {
141 | // Set up fallback interceptor with progress handler
142 | fallbackDownloadTaskInterceptor.progress = progress
143 | sessionDelegate.downloadTaskInterceptor = fallbackDownloadTaskInterceptor
144 | }
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Error/WebSocketError/WebSocketErrorTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test WebSocketErrorTests")
6 | final class WebSocketErrorTests {
7 | @Test("test WebSocketError basic cases are Equatable", arguments: zip(WebSocketErrorList, WebSocketErrorList))
8 | func testBasicCasesAreEquatable(inputA: WebSocketError, inputB: WebSocketError) {
9 | #expect(inputA == inputB)
10 | }
11 |
12 | @Test("test different basic cases are not equal")
13 | func testDifferentBasicCasesAreNotEqual() {
14 | #expect(WebSocketError.notConnected != WebSocketError.alreadyConnected)
15 | #expect(WebSocketError.invalidURL != WebSocketError.invalidMessageFormat)
16 | }
17 |
18 | @Test("test unsupportedProtocol equality and inequality")
19 | func testUnsupportedProtocolEquality() {
20 | let a = WebSocketError.unsupportedProtocol("protoA")
21 | let b = WebSocketError.unsupportedProtocol("protoA")
22 | let c = WebSocketError.unsupportedProtocol("protoB")
23 | #expect(a == b)
24 | #expect(a != c)
25 | }
26 |
27 | @Test("test keepAliveFailure equality by count")
28 | func testKeepAliveFailureEquality() {
29 | #expect(WebSocketError.keepAliveFailure(consecutiveFailures: 3) == .keepAliveFailure(consecutiveFailures: 3))
30 | #expect(WebSocketError.keepAliveFailure(consecutiveFailures: 2) != .keepAliveFailure(consecutiveFailures: 4))
31 | }
32 |
33 | @Test("test unexpectedDisconnection equality and description")
34 | func testUnexpectedDisconnectionEqualityAndDescription() {
35 | let lhs = WebSocketError.unexpectedDisconnection(code: .normalClosure, reason: "bye")
36 | let rhs = WebSocketError.unexpectedDisconnection(code: .normalClosure, reason: "bye")
37 | let other = WebSocketError.unexpectedDisconnection(code: .goingAway, reason: "bye")
38 |
39 | #expect(lhs == rhs)
40 | #expect(lhs != other)
41 |
42 | // description should contain code raw value and reason
43 | guard case let .unexpectedDisconnection(code, reason) = lhs else {
44 | #expect(Bool(false))
45 | return
46 | }
47 | let expected = "WebSocket disconnected unexpectedly with code \(code.rawValue): \(reason ?? "No provided")"
48 | #expect(lhs.errorDescription == expected)
49 | #expect(lhs.description == expected)
50 | }
51 |
52 | @Test("test unexpectedDisconnection equality and description with no reason provided")
53 | func testUnexpectedDisconnectionEqualityAndDescriptionWithNoReasonProvided() {
54 | let err = WebSocketError.unexpectedDisconnection(code: .normalClosure, reason: nil)
55 |
56 | guard case let .unexpectedDisconnection(code, _) = err else {
57 | #expect(Bool(false))
58 | return
59 | }
60 |
61 | let expected = "WebSocket disconnected unexpectedly with code \(code.rawValue): No reason provided"
62 | #expect(err.errorDescription == expected)
63 | #expect(err.description == expected)
64 | }
65 |
66 | @Test("test underlying error equality compares by type for various cases")
67 | func testUnderlyingErrorEqualityByType() {
68 | let urlErr1 = URLError(.timedOut)
69 | let urlErr2 = URLError(.timedOut)
70 |
71 | #expect(WebSocketError.connectionFailed(underlying: urlErr1) == .connectionFailed(underlying: urlErr2))
72 | #expect(WebSocketError.sendFailed(underlying: urlErr1) == .sendFailed(underlying: urlErr2))
73 | #expect(WebSocketError.receiveFailed(underlying: urlErr1) == .receiveFailed(underlying: urlErr2))
74 | #expect(WebSocketError.pingFailed(underlying: urlErr1) == .pingFailed(underlying: urlErr2))
75 | }
76 |
77 | @Test("test underlying error equality compares fails if underlying error difers")
78 | func testUnderlyingErrorEqualityByTypeFailsIfErrorDiffers() {
79 | let urlErr = URLError(.timedOut)
80 | let nsErr = NSError(domain: "test", code: 1, userInfo: nil)
81 |
82 | #expect(WebSocketError.connectionFailed(underlying: urlErr) != .connectionFailed(underlying: nsErr))
83 | }
84 |
85 | @Test("test localized descriptions for a selection of cases")
86 | func testLocalizedDescriptions() {
87 | #expect(WebSocketError.notConnected.errorDescription == "WebSocket is not connected")
88 | #expect(WebSocketError.invalidURL.errorDescription == "Invalid WebSocket URL")
89 | #expect(WebSocketError.invalidMessageFormat.errorDescription == "Invalid WebSocket message format")
90 | #expect(WebSocketError.messageEncodingFailed.errorDescription == "Failed to encode message for WebSocket")
91 | #expect(WebSocketError.messageDecodingFailed.errorDescription == "Failed to decode WebSocket message")
92 | }
93 |
94 | @Test("LocalizedError - dynamic descriptions")
95 | func testLocalizedErrorDynamicDescriptions() {
96 | let err = NSError(domain: "Test", code: -1)
97 | let msg = err.localizedDescription
98 | let protocolString = "bad-protocol"
99 | let count = 3
100 |
101 | #expect(WebSocketError.connectionFailed(underlying: err).errorDescription == "WebSocket connection failed: \(msg)")
102 | #expect(WebSocketError.sendFailed(underlying: err).errorDescription == "Failed to send WebSocket message: \(msg)")
103 | #expect(WebSocketError.receiveFailed(underlying: err).errorDescription == "Failed to receive WebSocket message: \(msg)")
104 | #expect(WebSocketError.pingFailed(underlying: err).errorDescription == "WebSocket ping failed: \(msg)")
105 |
106 | #expect(WebSocketError.unsupportedProtocol(protocolString).errorDescription == "Unsupported WebSocket protocol: \(protocolString)")
107 | #expect(WebSocketError.keepAliveFailure(consecutiveFailures: count).errorDescription == "WebSocket keep-alive failed after \(count) consecutive attempts")
108 | }
109 |
110 | private static let WebSocketErrorList: [WebSocketError] = [
111 | .notConnected,
112 | .stillConnecting,
113 | .alreadyConnected,
114 | .connectionTimeout,
115 | .invalidURL,
116 | .invalidMessageFormat,
117 | .messageEncodingFailed,
118 | .messageDecodingFailed,
119 | .pongTimeout,
120 | .forcedDisconnection,
121 | .taskNotInitialized,
122 | .taskCancelled,
123 | .streamAlreadyCreated,
124 | .streamNotAvailable,
125 | ]
126 | }
127 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/HTTP/Methods/HTTPMethodTests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Testing
3 |
4 | @Suite("Test HTTPMethod")
5 | final class HTTPMethodTests {
6 |
7 | // MARK: - Individual Method Tests
8 |
9 | @Test("test HTTPMethod.GET raw value")
10 | func testHTTPMethodGetRawValue() {
11 | #expect(HTTPMethod.GET.rawValue == "GET")
12 | }
13 |
14 | @Test("test HTTPMethod.POST raw value")
15 | func testHTTPMethodPOSTRawValue() {
16 | #expect(HTTPMethod.POST.rawValue == "POST")
17 | }
18 |
19 | @Test("test HTTPMethod.PUT raw value")
20 | func testHTTPMethodPUTRawValue() {
21 | #expect(HTTPMethod.PUT.rawValue == "PUT")
22 | }
23 |
24 | @Test("test HTTPMethod.DELETE raw value")
25 | func testHTTPMethodDELETERawValue() {
26 | #expect(HTTPMethod.DELETE.rawValue == "DELETE")
27 | }
28 |
29 | @Test("test HTTPMethod.PATCH raw value")
30 | func testHTTPMethodPATCHRawValue() {
31 | #expect(HTTPMethod.PATCH.rawValue == "PATCH")
32 | }
33 |
34 | @Test("test HTTPMethod.HEAD raw value")
35 | func testHTTPMethodHEADRawValue() {
36 | #expect(HTTPMethod.HEAD.rawValue == "HEAD")
37 | }
38 |
39 | @Test("test HTTPMethod.OPTIONS raw value")
40 | func testHTTPMethodOPTIONSRawValue() {
41 | #expect(HTTPMethod.OPTIONS.rawValue == "OPTIONS")
42 | }
43 |
44 | @Test("test HTTPMethod.TRACE raw value")
45 | func testHTTPMethodTRACERawValue() {
46 | #expect(HTTPMethod.TRACE.rawValue == "TRACE")
47 | }
48 |
49 | @Test("test HTTPMethod.CONNECT raw value")
50 | func testHTTPMethodCONNECTRawValue() {
51 | #expect(HTTPMethod.CONNECT.rawValue == "CONNECT")
52 | }
53 |
54 | // MARK: - Comprehensive Tests
55 |
56 | @Test("test all HTTP methods have correct raw values")
57 | func testAllHTTPMethodsHaveCorrectRawValues() {
58 | let expectedValues: [HTTPMethod: String] = [
59 | .GET: "GET",
60 | .POST: "POST",
61 | .PUT: "PUT",
62 | .DELETE: "DELETE",
63 | .PATCH: "PATCH",
64 | .HEAD: "HEAD",
65 | .OPTIONS: "OPTIONS",
66 | .TRACE: "TRACE",
67 | .CONNECT: "CONNECT"
68 | ]
69 |
70 | for (method, expectedValue) in expectedValues {
71 | #expect(method.rawValue == expectedValue, "HTTPMethod.\(method) should have raw value '\(expectedValue)'")
72 | }
73 | }
74 |
75 | @Test("test all HTTP methods are uppercase")
76 | func testAllHTTPMethodsAreUppercase() {
77 | for method in HTTPMethod.allCases {
78 | #expect(method.rawValue == method.rawValue.uppercased(), "HTTPMethod.\(method) should be uppercase")
79 | }
80 | }
81 |
82 | @Test("test all HTTP methods are non-empty")
83 | func testAllHTTPMethodsAreNonEmpty() {
84 | for method in HTTPMethod.allCases {
85 | #expect(!method.rawValue.isEmpty, "HTTPMethod.\(method) should have non-empty raw value")
86 | }
87 | }
88 |
89 | @Test("test HTTP methods are unique")
90 | func testHTTPMethodsAreUnique() {
91 | let rawValues = HTTPMethod.allCases.map { $0.rawValue }
92 | let uniqueValues = Set(rawValues)
93 |
94 | #expect(rawValues.count == uniqueValues.count, "All HTTP methods should have unique raw values")
95 | }
96 |
97 | // MARK: - CaseIterable Tests
98 |
99 | @Test("test CaseIterable conformance")
100 | func testCaseIterableConformance() {
101 | let allMethods = HTTPMethod.allCases
102 | #expect(allMethods.count == 9, "Should have 9 HTTP methods")
103 |
104 | // Test that all expected methods are present
105 | let expectedMethods: Set = [.GET, .POST, .PUT, .DELETE, .PATCH, .HEAD, .OPTIONS, .TRACE, .CONNECT]
106 | let actualMethods = Set(allMethods)
107 |
108 | #expect(actualMethods == expectedMethods, "All expected HTTP methods should be present")
109 | }
110 |
111 | // MARK: - HTTP Method Categories Tests
112 |
113 | @Test("test safe HTTP methods")
114 | func testSafeHTTPMethods() {
115 | let safeMethods: [HTTPMethod] = [.GET, .HEAD, .OPTIONS, .TRACE]
116 |
117 | for method in safeMethods {
118 | #expect(isSafeMethod(method), "\(method) should be considered a safe HTTP method")
119 | }
120 | }
121 |
122 | @Test("test idempotent HTTP methods")
123 | func testIdempotentHTTPMethods() {
124 | let idempotentMethods: [HTTPMethod] = [.GET, .PUT, .DELETE, .HEAD, .OPTIONS, .TRACE]
125 |
126 | for method in idempotentMethods {
127 | #expect(isIdempotentMethod(method), "\(method) should be considered an idempotent HTTP method")
128 | }
129 | }
130 |
131 | @Test("test methods that allow request body")
132 | func testMethodsThatAllowRequestBody() {
133 | let bodyAllowedMethods: [HTTPMethod] = [.POST, .PUT, .PATCH, .DELETE]
134 |
135 | for method in bodyAllowedMethods {
136 | #expect(allowsRequestBody(method), "\(method) should allow request body")
137 | }
138 | }
139 |
140 | @Test("test methods that typically don't allow request body")
141 | func testMethodsThatDontAllowRequestBody() {
142 | let noBodyMethods: [HTTPMethod] = [.GET, .HEAD, .OPTIONS, .TRACE, .CONNECT]
143 |
144 | for method in noBodyMethods {
145 | #expect(!allowsRequestBody(method), "\(method) should typically not allow request body")
146 | }
147 | }
148 |
149 | // MARK: - Helper Methods
150 |
151 | private func isSafeMethod(_ method: HTTPMethod) -> Bool {
152 | switch method {
153 | case .GET, .HEAD, .OPTIONS, .TRACE:
154 | return true
155 | case .POST, .PUT, .DELETE, .PATCH, .CONNECT:
156 | return false
157 | }
158 | }
159 |
160 | private func isIdempotentMethod(_ method: HTTPMethod) -> Bool {
161 | switch method {
162 | case .GET, .PUT, .DELETE, .HEAD, .OPTIONS, .TRACE:
163 | return true
164 | case .POST, .PATCH, .CONNECT:
165 | return false
166 | }
167 | }
168 |
169 | private func allowsRequestBody(_ method: HTTPMethod) -> Bool {
170 | switch method {
171 | case .POST, .PUT, .PATCH, .DELETE:
172 | return true
173 | case .GET, .HEAD, .OPTIONS, .TRACE, .CONNECT:
174 | return false
175 | }
176 | }
177 | }
178 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Interceptors/SessoionDelegate+Extensions+Tests/SessionDelegate+URLSessionTaskDelegate+Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test SessionDelegateURLSessionTaskDelegate")
6 | final class SessionDelegateURLSessionTaskDelegateTests {
7 |
8 | @Test("test SessionDelegate DidReceiveChallenge")
9 | func testSessionDelegateDidReceiveChallenge() async {
10 | let authenticationInterceptor = MockAuthenticationInterceptor()
11 | let delegate = SessionDelegate()
12 | delegate.authenticationInterceptor = authenticationInterceptor
13 |
14 | let (disposition, credential) = await delegate.urlSession(.shared, task: mockURLSessionTask, didReceive: URLAuthenticationChallenge())
15 | #expect(disposition == .performDefaultHandling)
16 | #expect(credential == nil)
17 | #expect(authenticationInterceptor.didReceiveChallengeWithTask)
18 | }
19 |
20 | @Test("test SessionDelegate DidReceiveChallenge with no interceptor")
21 | func testSessionDelegateDidReceiveChallengeWithNoInterceptor() async {
22 | let delegate = SessionDelegate()
23 |
24 | let (disposition, credential) = await delegate.urlSession(.shared, task: mockURLSessionTask, didReceive: URLAuthenticationChallenge())
25 | #expect(disposition == .performDefaultHandling)
26 | #expect(credential == nil)
27 | }
28 |
29 | @Test("test SessionDelegate WillPerformHTTPRedirection")
30 | func testSessionDelegateWillPerformHTTPRedirection() async {
31 | let redirectInterceptor = MockRedirectInterceptor()
32 | let delegate = SessionDelegate()
33 | delegate.redirectInterceptor = redirectInterceptor
34 |
35 | let response = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 301, httpVersion: nil, headerFields: nil)!
36 | let request = URLRequest(url: URL(string: "https://redirected.com")!)
37 |
38 | let newRequest = await delegate.urlSession(.shared, task: mockURLSessionTask, willPerformHTTPRedirection: response, newRequest: request)
39 | #expect(newRequest == request)
40 | #expect(redirectInterceptor.didRedirect)
41 | }
42 |
43 | @Test("test SessionDelegate WillPerformHTTPRedirection with no interceptor")
44 | func testSessionDelegateWillPerformHTTPRedirectionWithNoInterceptor() async {
45 | let delegate = SessionDelegate()
46 |
47 | let response = HTTPURLResponse(url: URL(string: "https://example.com")!, statusCode: 301, httpVersion: nil, headerFields: nil)!
48 | let request = URLRequest(url: URL(string: "https://redirected.com")!)
49 |
50 | let newRequest = await delegate.urlSession(.shared, task: mockURLSessionTask, willPerformHTTPRedirection: response, newRequest: request)
51 | #expect(newRequest == request)
52 | }
53 |
54 | @Test("test SessionDelegate DidFinishCollectingMetrics")
55 | func testSessionDelegateDidFinishCollectingMetrics() {
56 | let metricsInterceptor = MockMetricsInterceptor()
57 | let delegate = SessionDelegate()
58 | delegate.metricsInterceptor = metricsInterceptor
59 |
60 | delegate.urlSession(.shared, task: mockURLSessionTask, didFinishCollecting: mockURLSessionTaskMetrics)
61 |
62 | #expect(metricsInterceptor.didCollectMetrics)
63 | }
64 |
65 | @Test("test SessionDelegate DidCompleteWithError")
66 | func testSessionDelegateDidCompleteWithError() {
67 | let taskLifecycleInterceptor = MockTaskLifecycleInterceptor()
68 | let delegate = SessionDelegate()
69 | delegate.taskLifecycleInterceptor = taskLifecycleInterceptor
70 |
71 | let error = NSError(domain: "TestError", code: 1, userInfo: nil)
72 | delegate.urlSession(.shared, task: mockURLSessionTask, didCompleteWithError: error)
73 |
74 | #expect(taskLifecycleInterceptor.didCompleteWithError)
75 | }
76 |
77 | @Test("test SessionDelegateT askIsWaitingForConnectivity")
78 | func testSessionDelegateTaskIsWaitingForConnectivity() {
79 | let taskLifecycleInterceptor = MockTaskLifecycleInterceptor()
80 | let delegate = SessionDelegate()
81 | delegate.taskLifecycleInterceptor = taskLifecycleInterceptor
82 |
83 | delegate.urlSession(.shared, taskIsWaitingForConnectivity: mockURLSessionTask)
84 |
85 | #expect(taskLifecycleInterceptor.taskIsWaitingForConnectivity)
86 | }
87 |
88 | }
89 |
90 | // MARK: Mock classes
91 |
92 | private class MockAuthenticationInterceptor: AuthenticationInterceptor {
93 | var didReceiveChallengeWithTask = false
94 | func urlSession(_ session: URLSession, task: URLSessionTask, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
95 | didReceiveChallengeWithTask = true
96 | return (.performDefaultHandling, nil)
97 | }
98 |
99 | var didReceiveChallenge = false
100 | func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge) async -> (URLSession.AuthChallengeDisposition, URLCredential?) {
101 | didReceiveChallenge = true
102 | return (.performDefaultHandling, nil)
103 | }
104 | }
105 |
106 | private class MockRedirectInterceptor: RedirectInterceptor {
107 | var didRedirect = false
108 | func urlSession(_ session: URLSession, task: URLSessionTask, willPerformHTTPRedirection response: HTTPURLResponse, newRequest request: URLRequest) async -> URLRequest? {
109 | didRedirect = true
110 | return request
111 | }
112 | }
113 |
114 | private class MockMetricsInterceptor: MetricsInterceptor {
115 | var didCollectMetrics = false
116 | func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
117 | didCollectMetrics = true
118 | }
119 | }
120 |
121 | private class MockTaskLifecycleInterceptor: TaskLifecycleInterceptor {
122 | var didCompleteWithError = false
123 | var taskIsWaitingForConnectivity = false
124 |
125 | func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
126 | didCompleteWithError = true
127 | }
128 |
129 | func urlSession(_ session: URLSession, taskIsWaitingForConnectivity task: URLSessionTask) {
130 | taskIsWaitingForConnectivity = true
131 | }
132 |
133 | func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {}
134 | }
135 |
136 | // MARK: Mock variable
137 |
138 | private var mockURLSessionTask: URLSessionTask {
139 | URLSessionTask()
140 | }
141 |
142 | private var mockURLSessionTaskMetrics: URLSessionTaskMetrics {
143 | URLSessionTaskMetrics()
144 | }
145 |
--------------------------------------------------------------------------------
/Tests/EZNetworkingTests/Util/Performers/RequestPerformable_asyncAwait_Tests.swift:
--------------------------------------------------------------------------------
1 | @testable import EZNetworking
2 | import Foundation
3 | import Testing
4 |
5 | @Suite("Test RequestPerformable async/await methods")
6 | final class RequestPerformable_asyncAwait_Tests {
7 |
8 | // MARK: - SUCCESS RESPONSE
9 |
10 | @Test("test perform(request:_, decodeTo:_) with all valid inputs does not throw error")
11 | func perform_withValidInputs_doesNotThrowError() async throws {
12 | let sut = createRequestPerformer()
13 | await #expect(throws: Never.self) {
14 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
15 | }
16 | }
17 |
18 | @Test("test perform(request:_, decodeTo:_) with all valid inputs decodes data")
19 | func perform_withValidInputs_doesDecodeData() async throws {
20 | let sut = createRequestPerformer()
21 | let person = try await sut.perform(request: MockRequest(), decodeTo: Person.self)
22 | #expect(person.name == "John")
23 | #expect(person.age == 30)
24 | }
25 |
26 | @Test("test perform(request:_) with all valid inputs does not throw error")
27 | func perform_withoutDecoding_withValidInputs_doesNotThrowError() async throws {
28 | let sut = createRequestPerformer()
29 | await #expect(throws: Never.self) {
30 | try await sut.perform(request: MockRequest(), decodeTo: EmptyResponse.self)
31 | }
32 | }
33 |
34 | // MARK: - ERROR RESPONSE
35 |
36 |
37 |
38 | // MARK: http status code error tests
39 |
40 | @Test("test perform(request:_) fails when status code is 3xx")
41 | func perform_throwsErrorWhen_statusCodeIs300() async throws {
42 | let sut = createRequestPerformer(
43 | urlSession: createMockURLSession(statusCode: 300)
44 | )
45 | await #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 300))) {
46 | try await sut.perform(request: MockRequest(), decodeTo: EmptyResponse.self)
47 | }
48 | }
49 |
50 | @Test("test perform(request:_) fails when status code is 4xx")
51 | func perform_throwsErrorWhen_statusCodeIs400() async throws {
52 | let sut = createRequestPerformer(
53 | urlSession: createMockURLSession(statusCode: 400)
54 | )
55 | await #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 400))) {
56 | try await sut.perform(request: MockRequest(), decodeTo: EmptyResponse.self)
57 | }
58 | }
59 |
60 | @Test("test perform(request:_) fails when status code is 5xx")
61 | func perform_throwsErrorWhen_statusCodeIs500() async throws {
62 | let sut = createRequestPerformer(
63 | urlSession: createMockURLSession(statusCode: 500)
64 | )
65 | await #expect(throws: NetworkingError.httpError(HTTPError(statusCode: 500))) {
66 | try await sut.perform(request: MockRequest(), decodeTo: EmptyResponse.self)
67 | }
68 | }
69 |
70 | // MARK: URLSession has error
71 |
72 | @Test("test perform(request:_) fails when URLSession throws HTTPClientError")
73 | func perform_throwsErrorWhen_urlSessionThrowsHTTPClientError() async throws {
74 | let sut = createRequestPerformer(
75 | urlSession: createMockURLSession(error: HTTPError(statusCode: 400))
76 | )
77 | await #expect(throws: NetworkingError.internalError(.requestFailed(HTTPError(statusCode: 400)))) {
78 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
79 | }
80 | }
81 |
82 | @Test("test perform(request:_) fails when URLSession throws HTTPServerError")
83 | func perform_throwsErrorWhen_urlSessionThrowsHTTPServerError() async throws {
84 | let sut = createRequestPerformer(
85 | urlSession: createMockURLSession(error: HTTPError(statusCode: 500))
86 | )
87 | await #expect(throws: NetworkingError.internalError(.requestFailed(HTTPError(statusCode: 500)))) {
88 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
89 | }
90 | }
91 |
92 | @Test("test perform(request:_) fails when URLSession throws URLError")
93 | func perform_throwsErrorWhen_urlSessionThrowsURLError() async throws {
94 | let sut = createRequestPerformer(
95 | urlSession: createMockURLSession(error: URLError(.networkConnectionLost))
96 | )
97 | await #expect(throws: NetworkingError.urlError(URLError(.networkConnectionLost))) {
98 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
99 | }
100 | }
101 |
102 | @Test("test perform(request:_) fails when URLSession throws unknown error")
103 | func perform_throwsErrorWhen_urlSessionThrowsUnknownError() async throws {
104 | enum UnknownError: Error {
105 | case unknownError
106 | }
107 | let sut = createRequestPerformer(
108 | urlSession: createMockURLSession(error: UnknownError.unknownError)
109 | )
110 | await #expect(throws: NetworkingError.internalError(.requestFailed(UnknownError.unknownError))) {
111 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
112 | }
113 | }
114 |
115 | // MARK: data deocding errors
116 |
117 | @Test("test perform(request:_, decodeTo:_) fails when data is nil")
118 | func performAndDecode_throwsErrorWhen_dataIsNil() async throws {
119 | let sut = createRequestPerformer(
120 | urlSession: createMockURLSession(data: nil)
121 | )
122 | await #expect(throws: NetworkingError.internalError(.noData)) {
123 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
124 | }
125 | }
126 |
127 | @Test("test perform(request:_, decodeTo:_) fails data does not match decodeTo type")
128 | func performAndDecode_throwsErrorWhen_dataDoesNotMatchDecodeToType() async throws {
129 | let sut = createRequestPerformer(
130 | urlSession: createMockURLSession(data: MockData.invalidMockPersonJsonData)
131 | )
132 | await #expect(throws: NetworkingError.internalError(.couldNotParse)) {
133 | try await sut.perform(request: MockRequest(), decodeTo: Person.self)
134 | }
135 | }
136 | }
137 |
138 | // MARK: - helpers
139 |
140 | private func createRequestPerformer(
141 | urlSession: URLSessionTaskProtocol = createMockURLSession(),
142 | validator: ResponseValidator = ResponseValidatorImpl(),
143 | requestDecoder: RequestDecodable = RequestDecoder()
144 | ) -> RequestPerformer {
145 | return RequestPerformer(urlSession: urlSession, validator: validator, requestDecoder: requestDecoder)
146 | }
147 |
148 | private func createMockURLSession(
149 | data: Data? = MockData.mockPersonJsonData,
150 | statusCode: Int = 200,
151 | error: Error? = nil
152 | ) -> MockRequestPerformerURLSession {
153 | return MockRequestPerformerURLSession(
154 | data: data,
155 | urlResponse: buildResponse(statusCode: statusCode),
156 | error: error
157 | )
158 | }
159 |
160 | private func buildResponse(statusCode: Int) -> HTTPURLResponse {
161 | HTTPURLResponse(url: URL(string: "https://example.com")!,
162 | statusCode: statusCode,
163 | httpVersion: nil,
164 | headerFields: nil)!
165 | }
166 |
167 | private struct MockRequest: Request {
168 | var httpMethod: HTTPMethod { .GET }
169 | var baseUrlString: String { "https://www.example.com" }
170 | var parameters: [HTTPParameter]? { nil }
171 | var headers: [HTTPHeader]? { nil }
172 | var body: HTTPBody? { nil }
173 | }
174 |
175 | private struct MockRequestWithNilBuild: Request {
176 | var httpMethod: HTTPMethod { .GET }
177 | var baseUrlString: String { "https://www.example.com" }
178 | var parameters: [HTTPParameter]? { nil }
179 | var headers: [HTTPHeader]? { nil }
180 | var body: HTTPBody? { nil }
181 | var urlRequest: URLRequest? { nil }
182 | }
183 |
--------------------------------------------------------------------------------