From 4213128ba8b6409f9a3f94f45f9f76adb31c5959 Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Fri, 2 Aug 2024 14:18:21 +0300 Subject: [PATCH 01/18] Async Await support for data tasks. --- .../Networkable+DataTaskAsync.swift | 114 ++++++++++++++++++ Tests/MBNetworkingTests/NetworkingTests.swift | 13 ++ 2 files changed, 127 insertions(+) create mode 100644 Sources/MBNetworking/Networkable+DataTaskAsync.swift diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift new file mode 100644 index 0000000..6ee4984 --- /dev/null +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -0,0 +1,114 @@ +// +// Networkable+DataTaskAsync.swift +// Network +// +// Created by Rasid Ramazanov on 25.11.2019. +// Copyright © 2019 LeanScale. All rights reserved. +// + +import Foundation +import MBErrorKit + +/// Networkable extension related to data tasks. +extension Networkable { + /// Fetch data with specified parameters and return back with the completion. + /// - Parameters: + /// - type: Type of the result. + /// - Returns: Response as `Result` + @available(iOS 13.0.0, *) + public func fetch(_ type: V.Type) async -> Result { + // StubURLProtocol enabled and adding a small delay. + if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { +// RunLoop.current.run(until: Date().addingTimeInterval(0.05)) + return await fetch(request) + } else { + return await fetch(request) + } + } + + @available(iOS 13.0.0, *) private func fetch( + _ urlRequest: URLRequest + ) async -> Result { + let (response, data, error) = await requestData(urlRequest) + + if let error = error, + isNetworkConnectionError((error as NSError).code) { + let error = MBErrorKit.NetworkingError.networkConnectionError(error) + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) + printErrorLog(error) + return .failure(error) + + } else if let error = error { + let networkingError: NetworkingError + if (error as NSError).code == NSURLErrorCancelled { + networkingError = .dataTaskCancelled + } else { + networkingError = MBErrorKit.NetworkingError.underlyingError(error, response, data) + } + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) + printErrorLog(networkingError) + return .failure(networkingError) + + } else if let httpResponse = response as? HTTPURLResponse, + isSuccess(httpResponse.statusCode) { + let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) + printErrorLog(error) + return .failure(error) + + } else if let response = response, data == nil || data?.count == 0 { + let error = MBErrorKit.NetworkingError.dataTaskError(response, data) + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) + printErrorLog(error) + return .failure(error) + + } else if let data = data, data.count > 0 { + do { + // If requested decodable type is Data, received data will be returned. + if V.Type.self == Data.Type.self { + return .success(data as! V) + } + let decodableData = try JSONDecoder().decode(V.self, from: data) + return .success(decodableData) + } catch let sError { + let error = MBErrorKit.NetworkingError.decodingError(sError, response, data) + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(serializationError: error) + self.printErrorLog(error) + return .failure(error) + } + + } else { + let error = MBErrorKit.NetworkingError.unkownError(error, data) + MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) + printErrorLog(error) + return .failure(error) + } + } + + @available(iOS 13.0.0, *) + private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { + let taskId = UUID().uuidString + do { + let (data, response) = try await Session.shared.session.data(for: urlRequest) + if let task = Session.shared.tasksInProgress[taskId] { + Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) + finalizeTask(withId: taskId, task: task) + } + printResponse(data) + return (response, data, nil) + } catch { + if let task = Session.shared.tasksInProgress[taskId] { + Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) + finalizeTask(withId: taskId, task: task) + } + return (nil, nil, error) + } + } + + func finalizeTask(withId taskId: String, task: URLSessionDataTask) { + Session.shared.tasksInProgress.removeValue(forKey: taskId) + + Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) + Session.shared.tasksInProgress.updateValue(task, forKey: taskId) + } +} diff --git a/Tests/MBNetworkingTests/NetworkingTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift index eb420b6..70b627a 100644 --- a/Tests/MBNetworkingTests/NetworkingTests.swift +++ b/Tests/MBNetworkingTests/NetworkingTests.swift @@ -30,6 +30,19 @@ import XCTest } XCTAssertNotNil(image) } + + func testDataDownloadAsync() async { + StubURLProtocol + .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) + var image: UIImage? + let result = await Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) + if case let .success(data) = result { + image = UIImage(data: data) + } + XCTAssertNotNil(image) + } } #endif From ee32c5b01f8a53ecb2abf6583ab3cb788859b2b2 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Thu, 8 Aug 2024 17:25:12 +0300 Subject: [PATCH 02/18] Fixed crash and unit test. Refactoring async methods --- .../Networkable+DataTaskAsync.swift | 36 +++++++++--------- .../StubURLProtocol/StubURLProtocol.swift | 6 +++ .../NetworkablePerformanceTests.swift | 37 +++++++++---------- Tests/MBNetworkingTests/NetworkingTests.swift | 16 ++++++-- 4 files changed, 53 insertions(+), 42 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 6ee4984..26dde86 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -10,25 +10,24 @@ import Foundation import MBErrorKit /// Networkable extension related to data tasks. +/// +@available(iOS 13.0.0, *) extension Networkable { /// Fetch data with specified parameters and return back with the completion. /// - Parameters: /// - type: Type of the result. /// - Returns: Response as `Result` - @available(iOS 13.0.0, *) - public func fetch(_ type: V.Type) async -> Result { + public func fetch(_ type: V.Type) async throws -> V { // StubURLProtocol enabled and adding a small delay. if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { // RunLoop.current.run(until: Date().addingTimeInterval(0.05)) - return await fetch(request) + return try await fetch(request) } else { - return await fetch(request) + return try await fetch(request) } } - @available(iOS 13.0.0, *) private func fetch( - _ urlRequest: URLRequest - ) async -> Result { + private func fetch(_ urlRequest: URLRequest) async throws -> V { let (response, data, error) = await requestData(urlRequest) if let error = error, @@ -36,7 +35,7 @@ extension Networkable { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) - return .failure(error) + throw error } else if let error = error { let networkingError: NetworkingError @@ -47,65 +46,64 @@ extension Networkable { } MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) printErrorLog(networkingError) - return .failure(networkingError) + throw networkingError } else if let httpResponse = response as? HTTPURLResponse, isSuccess(httpResponse.statusCode) { let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) - return .failure(error) + throw error } else if let response = response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) - return .failure(error) + throw error } else if let data = data, data.count > 0 { do { // If requested decodable type is Data, received data will be returned. if V.Type.self == Data.Type.self { - return .success(data as! V) + return data as! V } let decodableData = try JSONDecoder().decode(V.self, from: data) - return .success(decodableData) + return decodableData } catch let sError { let error = MBErrorKit.NetworkingError.decodingError(sError, response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(serializationError: error) self.printErrorLog(error) - return .failure(error) + throw error } } else { let error = MBErrorKit.NetworkingError.unkownError(error, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) - return .failure(error) + throw error } } - @available(iOS 13.0.0, *) private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { let taskId = UUID().uuidString do { let (data, response) = try await Session.shared.session.data(for: urlRequest) if let task = Session.shared.tasksInProgress[taskId] { Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) - finalizeTask(withId: taskId, task: task) + Self.finalizeTask(withId: taskId, task: task) } printResponse(data) return (response, data, nil) } catch { if let task = Session.shared.tasksInProgress[taskId] { Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) - finalizeTask(withId: taskId, task: task) + Self.finalizeTask(withId: taskId, task: task) } return (nil, nil, error) } } - func finalizeTask(withId taskId: String, task: URLSessionDataTask) { + private static func finalizeTask(withId taskId: String, task: URLSessionDataTask) { Session.shared.tasksInProgress.removeValue(forKey: taskId) Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) diff --git a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift index 9e360d0..f5ea2b7 100644 --- a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift +++ b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift @@ -60,6 +60,11 @@ extension StubURLProtocol { switch result { case let .success(data): self.client?.urlProtocol(self, didLoad: data) + + if let url = request.url, + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { + self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) + } case let .failure(error): self.client?.urlProtocol(self, didFailWithError: error) case let .failureStatusCode(statusCode): @@ -68,6 +73,7 @@ extension StubURLProtocol { self.client?.urlProtocol(self, cachedResponseIsValid: CachedURLResponse(response: response, data: Data())) } } + self.client?.urlProtocolDidFinishLoading(self) } } diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift index 63ed605..a5f0ac6 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift @@ -21,45 +21,44 @@ NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) } - func testWhenMultipleDownloadCommandCalled() { + func testWhenMultipleDownloadCommandCalled() async throws { let expectation = XCTestExpectation(description: "wait for image") for i in 0 ..< 10000 { - downloadImage(index: i) + try await downloadImage(index: i) } XCTWaiter().wait(for: [expectation], timeout: 100) + Timer.scheduledTimer(withTimeInterval: 100, repeats: false) { _ in expectation.fulfill() } } - private func downloadImage(index: Int) { - imageView.downloadImageFrom(index: index) + private func downloadImage(index: Int) async throws { + try await imageView.downloadImageFrom(index: index) } } extension UIImageView { - func downloadImageFrom(index: Int) { + func downloadImageFrom(index: Int) async throws { if let savedImage = FileIOManager.readFile("\(index)"), let image = UIImage(data: savedImage) { self.image = image } - getProfilePhoto { result in - switch result { - case let .success(data): - self.image = UIImage(named: "ky_avatar") - let image = UIImage(data: data) - self.image = image - FileIOManager.writeFile("\(index)", content: data) - case let .failure(error): - return - } + + let data = try? await getProfilePhoto() + + if let data { + self.image = UIImage(named: "ky_avatar") + let image = UIImage(data: data) + self.image = image + FileIOManager.writeFile("\(index)", content: data) + } else { + return } } - private func getProfilePhoto( - completion: @escaping (Result) -> Void - ) { - API.getProfilePhoto.fetch(Data.self, completion: completion) + private func getProfilePhoto() async throws -> Data { + try await API.getProfilePhoto.fetch(Data.self) } } diff --git a/Tests/MBNetworkingTests/NetworkingTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift index 70b627a..6a5291a 100644 --- a/Tests/MBNetworkingTests/NetworkingTests.swift +++ b/Tests/MBNetworkingTests/NetworkingTests.swift @@ -25,21 +25,29 @@ import XCTest url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(Data.self) { result in if case let .success(data) = result { + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(data, actualData) + } + image = UIImage(data: data) } } XCTAssertNotNil(image) } - func testDataDownloadAsync() async { + func testDataDownloadAsync() async throws { StubURLProtocol .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) var image: UIImage? - let result = await Download.data( + let result = try await Download.data( url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(Data.self) - if case let .success(data) = result { - image = UIImage(data: data) + if case let result = result { + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(result, actualData) + } + + image = UIImage(data: result) } XCTAssertNotNil(image) } From 02bfd792f9c6c5273997fa66ff371d8162697ee4 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Thu, 8 Aug 2024 17:57:50 +0300 Subject: [PATCH 03/18] Refactor async unit tests methods --- .../NetworkablePerformanceTests.swift | 13 ++++++++----- Tests/MBNetworkingTests/NetworkingTests.swift | 14 ++++++++++---- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift index a5f0ac6..ceb1311 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift @@ -23,14 +23,17 @@ func testWhenMultipleDownloadCommandCalled() async throws { let expectation = XCTestExpectation(description: "wait for image") + for i in 0 ..< 10000 { - try await downloadImage(index: i) + do { + try await downloadImage(index: i) + expectation.fulfill() + } catch { + XCTFail("Download failed: \(error)") + } } - XCTWaiter().wait(for: [expectation], timeout: 100) - Timer.scheduledTimer(withTimeInterval: 100, repeats: false) { _ in - expectation.fulfill() - } + await fulfillment(of: [expectation], timeout: 100) } private func downloadImage(index: Int) async throws { diff --git a/Tests/MBNetworkingTests/NetworkingTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift index 6a5291a..2030d91 100644 --- a/Tests/MBNetworkingTests/NetworkingTests.swift +++ b/Tests/MBNetworkingTests/NetworkingTests.swift @@ -39,16 +39,22 @@ import XCTest StubURLProtocol .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) var image: UIImage? - let result = try await Download.data( - url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") - ).fetch(Data.self) - if case let result = result { + + do { + let result = try await Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) + if case let .success(actualData) = StubURLProtocol.result { XCTAssertEqual(result, actualData) } image = UIImage(data: result) + } catch let error { + print(error.localizedDescription) + XCTFail() } + XCTAssertNotNil(image) } } From 7ab0b46f0a1ffef38d6307b4417e3faa63f83b34 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Thu, 8 Aug 2024 18:00:08 +0300 Subject: [PATCH 04/18] Optimize for loop duration --- Tests/MBNetworkingTests/NetworkablePerformanceTests.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift index ceb1311..579d0d2 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift @@ -24,7 +24,7 @@ func testWhenMultipleDownloadCommandCalled() async throws { let expectation = XCTestExpectation(description: "wait for image") - for i in 0 ..< 10000 { + for i in 0 ..< 100 { do { try await downloadImage(index: i) expectation.fulfill() @@ -33,7 +33,7 @@ } } - await fulfillment(of: [expectation], timeout: 100) + await fulfillment(of: [expectation], timeout: 10) } private func downloadImage(index: Int) async throws { From 1281fe7722bb68cf75dd0e76dced13c1643ca7f1 Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Fri, 9 Aug 2024 10:51:56 +0300 Subject: [PATCH 05/18] Add todo for task queue bug? (maybe) --- Tests/MBNetworkingTests/NetworkableTasksTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/MBNetworkingTests/NetworkableTasksTests.swift b/Tests/MBNetworkingTests/NetworkableTasksTests.swift index a7584d7..c3e7747 100644 --- a/Tests/MBNetworkingTests/NetworkableTasksTests.swift +++ b/Tests/MBNetworkingTests/NetworkableTasksTests.swift @@ -36,6 +36,7 @@ class NetworkableTasksTests: XCTestCase { } func testSessionQueueHasRemovedDataTask_WhenTaskIsFinished() { + // TODO: test whether it's a bug in task queue or not? let expectation = XCTestExpectation(description: "waiting for image") makeACall(expectation) XCTAssertEqual(Session.shared.tasksInProgress.count, 1) From 706fc697d0155405f11ade22f7a962661d65958e Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Fri, 9 Aug 2024 10:55:28 +0300 Subject: [PATCH 06/18] Add todo async support for url session pinning delegate. --- Sources/MBNetworking/Networkable+Pinning.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/MBNetworking/Networkable+Pinning.swift b/Sources/MBNetworking/Networkable+Pinning.swift index 1e82e85..6f7bb0e 100644 --- a/Sources/MBNetworking/Networkable+Pinning.swift +++ b/Sources/MBNetworking/Networkable+Pinning.swift @@ -9,7 +9,9 @@ import Foundation import Security -internal class URLSessionPinningDelegate: NSObject, URLSessionDelegate { +// TODO: for iOS 13 and above create new URLSessionPinningDelegate +// and rename below class to URLSessionPinningLegacyDelegate +class URLSessionPinningDelegate: NSObject, URLSessionDelegate { var certificatePaths: [String] = [] func urlSession( @@ -67,7 +69,7 @@ extension URLSessionPinningDelegate: URLSessionTaskDelegate { } } -internal class UntrustedURLSessionDelegate: NSObject, URLSessionDelegate { +class UntrustedURLSessionDelegate: NSObject, URLSessionDelegate { func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, From 551efcf71a6a6103b67fe48700b6203aed2178c2 Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Fri, 9 Aug 2024 10:57:46 +0300 Subject: [PATCH 07/18] Update NetworkingTests.swift --- Tests/MBNetworkingTests/NetworkingTests.swift | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Tests/MBNetworkingTests/NetworkingTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift index 2030d91..b282890 100644 --- a/Tests/MBNetworkingTests/NetworkingTests.swift +++ b/Tests/MBNetworkingTests/NetworkingTests.swift @@ -10,6 +10,7 @@ import XCTest @testable import MBNetworking @testable import MobKitCore +// TODO: add NetworkingLegacyTests and separate NetworkingTests for async support. #if canImport(UIKit) class NetworkingTests: XCTestCase { override func setUp() { @@ -28,7 +29,7 @@ import XCTest if case let .success(actualData) = StubURLProtocol.result { XCTAssertEqual(data, actualData) } - + image = UIImage(data: data) } } @@ -44,17 +45,17 @@ import XCTest let result = try await Download.data( url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(Data.self) - + if case let .success(actualData) = StubURLProtocol.result { XCTAssertEqual(result, actualData) } - + image = UIImage(data: result) - } catch let error { + } catch { print(error.localizedDescription) XCTFail() } - + XCTAssertNotNil(image) } } From 6eb9f48887ccfa6bdf96debecaf4d11c1209d69f Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Mon, 12 Aug 2024 10:35:58 +0300 Subject: [PATCH 08/18] Separated URLSessingPinningDelegate into async and legacy and refactoring unit tests. --- .../MBNetworking/Networkable+DataTask.swift | 34 ++--- .../Networkable+DataTaskAsync.swift | 26 ++-- .../MBNetworking/Networkable+Pinning.swift | 83 ----------- .../Networkable+PinningDelegate.swift | 113 ++++++++++++++ .../Networkable+UntrustedDelegate.swift | 38 +++++ Sources/MBNetworking/Session.swift | 6 +- .../NetworkablePerformanceAsyncTests.swift | 136 +++++++++++++++++ .../NetworkablePerformanceLegacyTests.swift | 65 ++++++++ .../NetworkablePerformanceTests.swift | 140 ------------------ .../NetworkingAsyncTests.swift | 38 +++++ .../NetworkingLegacyTests.swift | 53 +++++++ Tests/MBNetworkingTests/NetworkingTests.swift | 73 --------- .../StubURLProtocolTests.swift | 14 +- 13 files changed, 489 insertions(+), 330 deletions(-) delete mode 100644 Sources/MBNetworking/Networkable+Pinning.swift create mode 100644 Sources/MBNetworking/Networkable+PinningDelegate.swift create mode 100644 Sources/MBNetworking/Networkable+UntrustedDelegate.swift create mode 100644 Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift create mode 100644 Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift delete mode 100644 Tests/MBNetworkingTests/NetworkablePerformanceTests.swift create mode 100644 Tests/MBNetworkingTests/NetworkingAsyncTests.swift create mode 100644 Tests/MBNetworkingTests/NetworkingLegacyTests.swift delete mode 100644 Tests/MBNetworkingTests/NetworkingTests.swift diff --git a/Sources/MBNetworking/Networkable+DataTask.swift b/Sources/MBNetworking/Networkable+DataTask.swift index b281e47..0dcf6db 100644 --- a/Sources/MBNetworking/Networkable+DataTask.swift +++ b/Sources/MBNetworking/Networkable+DataTask.swift @@ -21,26 +21,26 @@ extension Networkable { ) { // StubURLProtocol enabled and adding a small delay. if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { - fetch(request, completion: completion) RunLoop.current.run(until: Date().addingTimeInterval(0.05)) - } else { - fetch(request, completion: completion) } + + fetch(request, completion: completion) + } - + private func fetch( _ urlRequest: URLRequest, completion: @escaping ((Result) -> Void) ) { requestData(urlRequest) { response, data, error in - + if let error = error, self.isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - + } else if let error = error { let networkingError: NetworkingError if (error as NSError).code == NSURLErrorCancelled { @@ -51,21 +51,21 @@ extension Networkable { MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) self.printErrorLog(networkingError) completion(.failure(networkingError)) - + } else if let httpResponse = response as? HTTPURLResponse, self.isSuccess(httpResponse.statusCode) { let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - + } else if let response = response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - - } else if let data = data, data.count > 0 { + + } else if let data = data, !data.isEmpty { do { // If requested decodable type is Data, received data will be returned. if V.Type.self == Data.Type.self { @@ -80,7 +80,7 @@ extension Networkable { self.printErrorLog(error) completion(.failure(error)) } - + } else { let error = MBErrorKit.NetworkingError.unkownError(error, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) @@ -89,18 +89,18 @@ extension Networkable { } } } - + func isNetworkConnectionError(_ errorCode: Int) -> Bool { errorCode == NSURLErrorNetworkConnectionLost - || errorCode == NSURLErrorNotConnectedToInternet - || errorCode == NSURLErrorCannotConnectToHost - || errorCode == 53 + || errorCode == NSURLErrorNotConnectedToInternet + || errorCode == NSURLErrorCannotConnectToHost + || errorCode == 53 } - + func isSuccess(_ errorCode: Int) -> Bool { !(200 ... 399).contains(errorCode) } - + private func requestData(_ urlRequest: URLRequest, completion: @escaping ((URLResponse?, Data?, Error?) -> Void)) { let taskId = UUID().uuidString let task = Session.shared.session diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 26dde86..0326b81 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -21,22 +21,21 @@ extension Networkable { // StubURLProtocol enabled and adding a small delay. if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { // RunLoop.current.run(until: Date().addingTimeInterval(0.05)) - return try await fetch(request) - } else { - return try await fetch(request) } + + return try await fetch(request) } - + private func fetch(_ urlRequest: URLRequest) async throws -> V { let (response, data, error) = await requestData(urlRequest) - + if let error = error, isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let error = error { let networkingError: NetworkingError if (error as NSError).code == NSURLErrorCancelled { @@ -47,20 +46,20 @@ extension Networkable { MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) printErrorLog(networkingError) throw networkingError - + } else if let httpResponse = response as? HTTPURLResponse, isSuccess(httpResponse.statusCode) { let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let response = response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let data = data, data.count > 0 { do { // If requested decodable type is Data, received data will be returned. @@ -75,7 +74,7 @@ extension Networkable { self.printErrorLog(error) throw error } - + } else { let error = MBErrorKit.NetworkingError.unkownError(error, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) @@ -83,11 +82,12 @@ extension Networkable { throw error } } - + private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { let taskId = UUID().uuidString do { let (data, response) = try await Session.shared.session.data(for: urlRequest) + if let task = Session.shared.tasksInProgress[taskId] { Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) Self.finalizeTask(withId: taskId, task: task) @@ -102,10 +102,10 @@ extension Networkable { return (nil, nil, error) } } - + private static func finalizeTask(withId taskId: String, task: URLSessionDataTask) { Session.shared.tasksInProgress.removeValue(forKey: taskId) - + Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) Session.shared.tasksInProgress.updateValue(task, forKey: taskId) } diff --git a/Sources/MBNetworking/Networkable+Pinning.swift b/Sources/MBNetworking/Networkable+Pinning.swift deleted file mode 100644 index 6f7bb0e..0000000 --- a/Sources/MBNetworking/Networkable+Pinning.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// Networkable+Pinning.swift -// Networking -// -// Created by Rasid Ramazanov on 29.01.2020. -// Copyright © 2020 Mobven. All rights reserved. -// - -import Foundation -import Security - -// TODO: for iOS 13 and above create new URLSessionPinningDelegate -// and rename below class to URLSessionPinningLegacyDelegate -class URLSessionPinningDelegate: NSObject, URLSessionDelegate { - var certificatePaths: [String] = [] - - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - guard let serverCertificate = getServerCertificate(forChallenge: challenge) else { - completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) - return - } - - let serverPublicKeys = serverCertificate.trust.certificates.publicKeys - for certificatePath in certificatePaths { - if let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: certificatePath)) as CFData?, - let localCertificate = SecCertificateCreateWithData(nil, localCertificateData), - let localPublicKey = localCertificate.publicKey { - if serverPublicKeys.contains(localPublicKey) { - completionHandler( - URLSession.AuthChallengeDisposition.useCredential, URLCredential(trust: serverCertificate.trust) - ) - return - } - } - } - if certificatePaths.count == 0 { - // No SSL pinning. Performing default handling. - completionHandler(URLSession.AuthChallengeDisposition.performDefaultHandling, nil) - } else { - // SSL pinning could not succeed with given certificates. Cancelling authentication. - completionHandler(URLSession.AuthChallengeDisposition.cancelAuthenticationChallenge, nil) - } - } - - private func getServerCertificate( - forChallenge challenge: URLAuthenticationChallenge - ) -> (data: CFData, trust: SecTrust)? { - var secresult = SecTrustResultType.invalid - guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, - let serverTrust = challenge.protectionSpace.serverTrust, - errSecSuccess == SecTrustEvaluate(serverTrust, &secresult), - let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) - else { - return nil - } - - let serverCertificateData = SecCertificateCopyData(serverCertificate) - return (serverCertificateData, serverTrust) - } -} - -extension URLSessionPinningDelegate: URLSessionTaskDelegate { - func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { - Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didFinishCollecting: metrics) - } -} - -class UntrustedURLSessionDelegate: NSObject, URLSessionDelegate { - func urlSession( - _ session: URLSession, - didReceive challenge: URLAuthenticationChallenge, - completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void - ) { - completionHandler( - URLSession.AuthChallengeDisposition.useCredential, - URLCredential(trust: challenge.protectionSpace.serverTrust!) - ) - } -} diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift new file mode 100644 index 0000000..f5ae261 --- /dev/null +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -0,0 +1,113 @@ +// +// Networkable+PinningDelegate.swift +// Networking +// +// Created by Rasid Ramazanov on 29.01.2020. +// Copyright © 2020 Mobven. All rights reserved. +// + +import Foundation +import Security + +protocol URLSessionPinningDelegateProtocol: URLSessionDelegate { + var certificatePaths: [String] { get set } +} + +func createURLSessionPinningDelegate() -> URLSessionPinningDelegateProtocol { + if #available(iOS 13.0, *) { + return URLSessionPinningDelegateAsync() + } else { + return URLSessionPinningDelegateLegacy() + } +} + +extension URLSessionPinningDelegateProtocol { + func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) { + Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didFinishCollecting: metrics) + } +} + +class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtocol { + var certificatePaths: [String] = [] + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + guard let serverCertificate = URLSessionPinningHelper.getServerCertificate(forChallenge: challenge) else { + completionHandler(.cancelAuthenticationChallenge, nil) + return + } + + let disposition = URLSessionPinningHelper.handleChallengeCommon(certificatePaths: certificatePaths, + serverCertificate: serverCertificate) + if disposition == .useCredential { + completionHandler(disposition, URLCredential(trust: serverCertificate.trust)) + } else { + completionHandler(disposition, nil) + } + } +} + + +@available(iOS 13.0, *) +class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtocol { + var certificatePaths: [String] = [] + + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + guard let serverCertificate = URLSessionPinningHelper.getServerCertificate(forChallenge: challenge) else { + return (.cancelAuthenticationChallenge, nil) + } + + let disposition = URLSessionPinningHelper.handleChallengeCommon(certificatePaths: certificatePaths, serverCertificate: serverCertificate) + if disposition == .useCredential { + return (disposition, URLCredential(trust: serverCertificate.trust)) + } else { + return (disposition, nil) + } + } +} + +private class URLSessionPinningHelper { + static func getServerCertificate( + forChallenge challenge: URLAuthenticationChallenge + ) -> (data: CFData, trust: SecTrust)? { + var secresult = SecTrustResultType.invalid + guard challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, + let serverTrust = challenge.protectionSpace.serverTrust, + errSecSuccess == SecTrustEvaluate(serverTrust, &secresult), + let serverCertificate = SecTrustGetCertificateAtIndex(serverTrust, 0) + else { + return nil + } + + let serverCertificateData = SecCertificateCopyData(serverCertificate) + return (serverCertificateData, serverTrust) + } + + static func handleChallengeCommon( + certificatePaths: [String], + serverCertificate: (data: CFData, trust: SecTrust) + ) -> URLSession.AuthChallengeDisposition { + let serverPublicKeys = serverCertificate.trust.certificates.publicKeys + for certificatePath in certificatePaths { + if let localCertificateData = try? Data(contentsOf: URL(fileURLWithPath: certificatePath)) as CFData?, + let localCertificate = SecCertificateCreateWithData(nil, localCertificateData), + let localPublicKey = localCertificate.publicKey { + if serverPublicKeys.contains(localPublicKey) { + return .useCredential + } + } + } + + if certificatePaths.isEmpty { + return .performDefaultHandling + } else { + return .cancelAuthenticationChallenge + } + } +} diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift new file mode 100644 index 0000000..daa10d2 --- /dev/null +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -0,0 +1,38 @@ +// +// Networkable+UntrustedDelegate.swift +// Networking +// +// Created by Umut Can ARDUÇ on 9.08.2024. +// Copyright © 2024 Mobven. All rights reserved. +// + +import Foundation + +func createUntrustedURLSessionDelegate() -> URLSessionDelegate { + if #available(iOS 13.0, *) { + return UntrustedURLSessionDelegateAsync() + } else { + return UntrustedURLSessionDelegateLegacy() + } +} + +@available(iOS 13.0, *) +class UntrustedURLSessionDelegateAsync: NSObject, URLSessionDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge + ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { + return (.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) + } +} + + +class UntrustedURLSessionDelegateLegacy: NSObject, URLSessionDelegate { + func urlSession( + _ session: URLSession, + didReceive challenge: URLAuthenticationChallenge, + completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void + ) { + completionHandler(.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) + } +} diff --git a/Sources/MBNetworking/Session.swift b/Sources/MBNetworking/Session.swift index ff0d96d..5f438e4 100644 --- a/Sources/MBNetworking/Session.swift +++ b/Sources/MBNetworking/Session.swift @@ -34,7 +34,7 @@ final class Session { /// SSL certificate paths of `URLSessionDelegate`. var certificatePaths: [String] = [] { didSet { - (delegate as? URLSessionPinningDelegate)?.certificatePaths = certificatePaths + (delegate as? URLSessionPinningDelegateProtocol)?.certificatePaths = certificatePaths } } @@ -56,7 +56,7 @@ final class Session { /// Configures networking to trust session authentication challenge, even if the certificate is not trusted. func setServerTrustedURLAuthenticationChallenge() { - delegate = UntrustedURLSessionDelegate() + delegate = createUntrustedURLSessionDelegate() session = URLSession( configuration: configuration, delegate: delegate, @@ -65,7 +65,7 @@ final class Session { } required init() { - delegate = URLSessionPinningDelegate() + delegate = createURLSessionPinningDelegate() session = URLSession( configuration: configuration, delegate: delegate, diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift new file mode 100644 index 0000000..92b6fd5 --- /dev/null +++ b/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift @@ -0,0 +1,136 @@ +// +// NetworkablePerformanceAsyncTests.swift +// Networking +// +// Created by Umut Can ARDUÇ on 9.08.2024. +// Copyright © 2024 Mobven. All rights reserved. +// + +#if canImport(UIKit) +import Foundation +import UIKit +import XCTest +@testable import MBErrorKit +@testable import MBNetworking +@testable import MobKitCore + +class NetworkablePerformanceAsyncTests: XCTestCase { + var imageView: UIImageView = .init() + + override func setUp() { + MobKit.isDeveloperModeOn = true + NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) + } + + func testWhenMultipleDownloadCommandCalled() async throws { + let expectation = XCTestExpectation(description: "wait for image") + + for i in 0 ..< 100 { + try await downloadImage(index: i) + expectation.fulfill() + + } + + await fulfillment(of: [expectation], timeout: 10) + } + + private func downloadImage(index: Int) async throws { + try await imageView.downloadImageFrom(index: index) + } +} + +extension UIImageView { + func downloadImageFrom(index: Int) async throws { + if let savedImage = FileIOManager.readFile("\(index)"), + let image = UIImage(data: savedImage) { + self.image = image + return + } + + let data = try await getProfilePhoto() + + image = UIImage(named: "ky_avatar") + let image = UIImage(data: data) + self.image = image + FileIOManager.writeFile("\(index)", content: data) + } + + private func getProfilePhoto() async throws -> Data { + try await API.getProfilePhoto.fetch(Data.self) + } +} + +enum API: Networkable { + case getProfilePhoto + + var request: URLRequest { + URLRequest(url: URL(forceString: "https://picsum.photos/200/300")) + } +} + +enum UIImageManager { + static func convertImageToBase64String(img: UIImage) -> String { + img.pngData()?.base64EncodedString() ?? "" + } + + static func convertBase64StringToImage(data: Data) -> UIImage? { + UIImage(data: data) + } +} + +enum FileIOManager { + private static let localDirectory = "kutup_pp" + + @discardableResult static func writeFile(_ fileName: String, content: Data) -> Bool { + guard let directory = getFileDirectory() else { + return false + } + guard createDirectoryIfNeeded(directory) else { + return false + } + let fileURL = directory.appendingPathComponent(fileName) + do { + try content.write(to: fileURL, options: .atomic) + return true + } catch { + return false + } + } + + private static func createDirectoryIfNeeded(_ directory: URL) -> Bool { + guard !FileManager.default.fileExists(atPath: directory.absoluteString) else { + // Directory exists, no need to recreate it. + return true + } + do { + try FileManager.default.createDirectory( + at: directory, withIntermediateDirectories: true, attributes: nil + ) + return true + } catch { + print(error.localizedDescription) + return false + } + } + + static func readFile(_ fileName: String) -> Data? { + guard let directory = getFileDirectory() else { + return nil + } + let fileURL = directory.appendingPathComponent(fileName) + do { + return try Data(contentsOf: fileURL) + } catch { + return nil + } + } + + private static func getFileDirectory() -> URL? { + guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + return nil + } + return directory.appendingPathComponent(localDirectory, isDirectory: true) + } +} +#endif + diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift new file mode 100644 index 0000000..32c48d9 --- /dev/null +++ b/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift @@ -0,0 +1,65 @@ +// +// NetworkablePerformanceTests.swift +// MBNetworkingTests +// +// Created by Rashid Ramazanov on 2/23/22. +// + +#if canImport(UIKit) +import Foundation +import UIKit +import XCTest +@testable import MBErrorKit +@testable import MBNetworking +@testable import MobKitCore + +class NetworkablePerformanceLegacyTests: XCTestCase { + var imageView: UIImageView = .init() + + override func setUp() { + MobKit.isDeveloperModeOn = true + NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) + } + + func testWhenMultipleDownloadCommandCalled() { + let expectation = XCTestExpectation(description: "wait for image") + for i in 0 ..< 1000 { + downloadImage(index: i) + } + XCTWaiter().wait(for: [expectation], timeout: 10) + Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in + expectation.fulfill() + } + } + + private func downloadImage(index: Int) { + imageView.downloadImageFrom(index: index) + } +} + +extension UIImageView { + func downloadImageFrom(index: Int) { + if let savedImage = FileIOManager.readFile("\(index)"), + let image = UIImage(data: savedImage) { + self.image = image + } + getProfilePhoto { result in + switch result { + case let .success(data): + self.image = UIImage(named: "ky_avatar") + let image = UIImage(data: data) + self.image = image + FileIOManager.writeFile("\(index)", content: data) + case .failure(_): + return + } + } + } + + private func getProfilePhoto( + completion: @escaping (Result) -> Void + ) { + API.getProfilePhoto.fetch(Data.self, completion: completion) + } +} +#endif diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift deleted file mode 100644 index 579d0d2..0000000 --- a/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// NetworkablePerformanceTests.swift -// MBNetworkingTests -// -// Created by Rashid Ramazanov on 2/23/22. -// - -#if canImport(UIKit) - import Foundation - import UIKit - import XCTest - @testable import MBErrorKit - @testable import MBNetworking - @testable import MobKitCore - - class NetworkablePerformanceTests: XCTestCase { - var imageView: UIImageView = .init() - - override func setUp() { - MobKit.isDeveloperModeOn = true - NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) - } - - func testWhenMultipleDownloadCommandCalled() async throws { - let expectation = XCTestExpectation(description: "wait for image") - - for i in 0 ..< 100 { - do { - try await downloadImage(index: i) - expectation.fulfill() - } catch { - XCTFail("Download failed: \(error)") - } - } - - await fulfillment(of: [expectation], timeout: 10) - } - - private func downloadImage(index: Int) async throws { - try await imageView.downloadImageFrom(index: index) - } - } - - extension UIImageView { - func downloadImageFrom(index: Int) async throws { - if let savedImage = FileIOManager.readFile("\(index)"), - let image = UIImage(data: savedImage) { - self.image = image - } - - let data = try? await getProfilePhoto() - - if let data { - self.image = UIImage(named: "ky_avatar") - let image = UIImage(data: data) - self.image = image - FileIOManager.writeFile("\(index)", content: data) - } else { - return - } - } - - private func getProfilePhoto() async throws -> Data { - try await API.getProfilePhoto.fetch(Data.self) - } - } - - enum API: Networkable { - case getProfilePhoto - - var request: URLRequest { - URLRequest(url: URL(forceString: "https://picsum.photos/200/300")) - } - } - - enum UIImageManager { - static func convertImageToBase64String(img: UIImage) -> String { - img.pngData()?.base64EncodedString() ?? "" - } - - static func convertBase64StringToImage(data: Data) -> UIImage? { - UIImage(data: data) - } - } - - enum FileIOManager { - private static let localDirectory = "kutup_pp" - - @discardableResult static func writeFile(_ fileName: String, content: Data) -> Bool { - guard let directory = getFileDirectory() else { - return false - } - guard createDirectoryIfNeeded(directory) else { - return false - } - let fileURL = directory.appendingPathComponent(fileName) - do { - try content.write(to: fileURL, options: .atomic) - return true - } catch { - return false - } - } - - private static func createDirectoryIfNeeded(_ directory: URL) -> Bool { - guard !FileManager.default.fileExists(atPath: directory.absoluteString) else { - // Directory exists, no need to recreate it. - return true - } - do { - try FileManager.default.createDirectory( - at: directory, withIntermediateDirectories: true, attributes: nil - ) - return true - } catch { - print(error.localizedDescription) - return false - } - } - - static func readFile(_ fileName: String) -> Data? { - guard let directory = getFileDirectory() else { - return nil - } - let fileURL = directory.appendingPathComponent(fileName) - do { - return try Data(contentsOf: fileURL) - } catch { - return nil - } - } - - private static func getFileDirectory() -> URL? { - guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - return nil - } - return directory.appendingPathComponent(localDirectory, isDirectory: true) - } - } -#endif diff --git a/Tests/MBNetworkingTests/NetworkingAsyncTests.swift b/Tests/MBNetworkingTests/NetworkingAsyncTests.swift new file mode 100644 index 0000000..d222c8f --- /dev/null +++ b/Tests/MBNetworkingTests/NetworkingAsyncTests.swift @@ -0,0 +1,38 @@ +// +// NetworkingAsyncTests.swift +// MBNetworking +// +// Created by Umut Can ARDUÇ on 9.08.2024. +// Copyright © 2021 Mobven. All rights reserved. +// + +import XCTest +@testable import MBNetworking +@testable import MobKitCore + +#if canImport(UIKit) +class NetworkingAsyncTests: XCTestCase { + override func setUp() { + MobKit.isDeveloperModeOn = true + StubURLProtocol.delay = .zero + } + + func testDataDownloadAsync() async throws { + StubURLProtocol + .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) + var image: UIImage? + + let result = try await Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) + + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(result, actualData) + } + + image = UIImage(data: result) + + XCTAssertNotNil(image) + } +} +#endif diff --git a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift new file mode 100644 index 0000000..0bd4dd1 --- /dev/null +++ b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift @@ -0,0 +1,53 @@ +// +// NetworkingTests.swift +// NetworkingTests +// +// Created by Rasid Ramazanov on 17.02.2020. +// Copyright © 2020 Mobven. All rights reserved. +// + +import XCTest +@testable import MBNetworking +@testable import MobKitCore + +#if canImport(UIKit) +class NetworkingLegacyTests: XCTestCase { + override func setUp() { + MobKit.isDeveloperModeOn = true + StubURLProtocol.delay = .zero + } + + func testDataDownload() { + let expectation = expectation(description: "Data download should succeed and produce a valid image") + StubURLProtocol.result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) + var image: UIImage? + + Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) { result in + if case let .success(data) = result { + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(data, actualData) + } + + image = UIImage(data: data) + } + expectation.fulfill() + } + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertNotNil(image) + } +} +#endif + +enum Download: Networkable { + case data(url: URL) + + var request: URLRequest { + switch self { + case let .data(url): + return getRequest(url: url, queryItems: [:]) + } + } +} diff --git a/Tests/MBNetworkingTests/NetworkingTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift deleted file mode 100644 index b282890..0000000 --- a/Tests/MBNetworkingTests/NetworkingTests.swift +++ /dev/null @@ -1,73 +0,0 @@ -// -// NetworkingTests.swift -// NetworkingTests -// -// Created by Rasid Ramazanov on 17.02.2020. -// Copyright © 2020 Mobven. All rights reserved. -// - -import XCTest -@testable import MBNetworking -@testable import MobKitCore - -// TODO: add NetworkingLegacyTests and separate NetworkingTests for async support. -#if canImport(UIKit) - class NetworkingTests: XCTestCase { - override func setUp() { - MobKit.isDeveloperModeOn = true - StubURLProtocol.delay = .zero - } - - func testDataDownload() { - StubURLProtocol - .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) - var image: UIImage? - Download.data( - url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") - ).fetch(Data.self) { result in - if case let .success(data) = result { - if case let .success(actualData) = StubURLProtocol.result { - XCTAssertEqual(data, actualData) - } - - image = UIImage(data: data) - } - } - XCTAssertNotNil(image) - } - - func testDataDownloadAsync() async throws { - StubURLProtocol - .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) - var image: UIImage? - - do { - let result = try await Download.data( - url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") - ).fetch(Data.self) - - if case let .success(actualData) = StubURLProtocol.result { - XCTAssertEqual(result, actualData) - } - - image = UIImage(data: result) - } catch { - print(error.localizedDescription) - XCTFail() - } - - XCTAssertNotNil(image) - } - } -#endif - -enum Download: Networkable { - case data(url: URL) - - var request: URLRequest { - switch self { - case let .data(url): - return getRequest(url: url, queryItems: [:]) - } - } -} diff --git a/Tests/MBNetworkingTests/StubURLProtocolTests.swift b/Tests/MBNetworkingTests/StubURLProtocolTests.swift index ace58d2..ff007b8 100644 --- a/Tests/MBNetworkingTests/StubURLProtocolTests.swift +++ b/Tests/MBNetworkingTests/StubURLProtocolTests.swift @@ -49,20 +49,26 @@ class StubURLProtocolTests: XCTestCase { } expectation.fulfill() } - wait(for: [expectation], timeout: StubURLProtocol.delay) + waitForExpectations(timeout: 5, handler: nil) XCTAssertEqual(string, "some\n") } func test_When_StubProtocolFetchesJSON() { StubURLProtocol.result = .getData(from: Bundle.module.url(forResource: "results", withExtension: "json")) var response: DecodableTrue? + let expectation = expectation(description: "wait for delay") + Download.data( url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(DecodableTrue.self) { result in if case let .success(resp) = result { response = resp } + + expectation.fulfill() } + waitForExpectations(timeout: 5, handler: nil) + // The image in the link is 200x150 size. XCTAssertNotNil(response) XCTAssertEqual(response?.resultCount, 0) @@ -73,13 +79,19 @@ class StubURLProtocolTests: XCTestCase { StubURLProtocol .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) var image: UIImage? + let expectation = expectation(description: "wait for delay") + Download.data( url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(Data.self) { result in if case let .success(data) = result { image = UIImage(data: data) } + + expectation.fulfill() } + waitForExpectations(timeout: 5, handler: nil) + // The image in the link is 200x150 size. XCTAssertNotNil(image) XCTAssertEqual(image?.size.width, 200) From a61ae4c07b7ae634ee72e60a23a9b82e21411c21 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Tue, 13 Aug 2024 09:02:33 +0300 Subject: [PATCH 09/18] Removed unused codes and fixed unit test. --- Sources/MBNetworking/Networkable+DataTaskAsync.swift | 7 +------ Tests/MBNetworkingTests/StubURLProtocolTests.swift | 3 ++- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 0326b81..980de0d 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -18,12 +18,7 @@ extension Networkable { /// - type: Type of the result. /// - Returns: Response as `Result` public func fetch(_ type: V.Type) async throws -> V { - // StubURLProtocol enabled and adding a small delay. - if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { -// RunLoop.current.run(until: Date().addingTimeInterval(0.05)) - } - - return try await fetch(request) + try await fetch(request) } private func fetch(_ urlRequest: URLRequest) async throws -> V { diff --git a/Tests/MBNetworkingTests/StubURLProtocolTests.swift b/Tests/MBNetworkingTests/StubURLProtocolTests.swift index ff007b8..5b97c5c 100644 --- a/Tests/MBNetworkingTests/StubURLProtocolTests.swift +++ b/Tests/MBNetworkingTests/StubURLProtocolTests.swift @@ -109,7 +109,8 @@ class StubURLProtocolTests: XCTestCase { } expectation.fulfill() } - wait(for: [expectation], timeout: 1) + waitForExpectations(timeout: 5, handler: nil) + // The real image in the link is 1400x637 size. XCTAssertNotNil(image) XCTAssertEqual(image?.size.width, 1400) From 7be5feb213d638c28269a9a9344a147e35e61e4c Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Tue, 13 Aug 2024 09:05:24 +0300 Subject: [PATCH 10/18] Swiftformat changes. --- .../Extensions/CharacterEncoding.swift | 2 +- Sources/MBNetworking/Extensions/Data.swift | 2 - .../MBNetworking/Extensions/ProcessInfo.swift | 2 - Sources/MBNetworking/Extensions/URL.swift | 12 +- .../MBNetworking/Networkable+DataTask.swift | 35 ++- .../Networkable+DataTaskAsync.swift | 25 +- .../Networkable+PinningDelegate.swift | 30 +-- .../MBNetworking/Networkable+URLRequest.swift | 148 +++++++----- .../Networkable+UntrustedDelegate.swift | 4 +- Sources/MBNetworking/Networkable.swift | 2 +- Sources/MBNetworking/NetworkableConfigs.swift | 2 +- Sources/MBNetworking/Session.swift | 2 +- .../MBNetworking/StubURLProtocol/Result.swift | 10 +- .../StubURLProtocol/StubURLProtocol.swift | 30 +-- Sources/MBNetworking/UploadFile.swift | 2 - .../NetworkablePerformanceAsyncTests.swift | 216 +++++++++--------- .../NetworkablePerformanceLegacyTests.swift | 90 ++++---- .../NetworkingAsyncTests.swift | 44 ++-- .../NetworkingLegacyTests.swift | 55 ++--- .../ResultTests/ResultTestAPI.swift | 6 +- .../StubURLProtocolTests.swift | 8 +- 21 files changed, 368 insertions(+), 359 deletions(-) diff --git a/Sources/MBNetworking/Extensions/CharacterEncoding.swift b/Sources/MBNetworking/Extensions/CharacterEncoding.swift index 0fbeeba..4fb440b 100644 --- a/Sources/MBNetworking/Extensions/CharacterEncoding.swift +++ b/Sources/MBNetworking/Extensions/CharacterEncoding.swift @@ -23,7 +23,7 @@ extension CharacterSet { let generalDelimitersToEncode = ":#[]@" // does not include "?" or "/" due to RFC 3986 - Section 3.4 let subDelimitersToEncode = "!$&'()*+,;=" let encodableDelimiters = CharacterSet(charactersIn: "\(generalDelimitersToEncode)\(subDelimitersToEncode)") - + return CharacterSet.urlQueryAllowed.subtracting(encodableDelimiters) }() } diff --git a/Sources/MBNetworking/Extensions/Data.swift b/Sources/MBNetworking/Extensions/Data.swift index b62f5e2..52f582c 100644 --- a/Sources/MBNetworking/Extensions/Data.swift +++ b/Sources/MBNetworking/Extensions/Data.swift @@ -9,10 +9,8 @@ import Foundation extension NSMutableData { - func appendString(_ string: String) { guard let data = string.data(using: .utf8) else { return } append(data) } - } diff --git a/Sources/MBNetworking/Extensions/ProcessInfo.swift b/Sources/MBNetworking/Extensions/ProcessInfo.swift index 325ba85..e6a2674 100644 --- a/Sources/MBNetworking/Extensions/ProcessInfo.swift +++ b/Sources/MBNetworking/Extensions/ProcessInfo.swift @@ -9,10 +9,8 @@ import Foundation extension ProcessInfo { - /// Returns true if process is in testing. static var isUnderTest: Bool { ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] != nil } - } diff --git a/Sources/MBNetworking/Extensions/URL.swift b/Sources/MBNetworking/Extensions/URL.swift index 8591c96..68e570e 100644 --- a/Sources/MBNetworking/Extensions/URL.swift +++ b/Sources/MBNetworking/Extensions/URL.swift @@ -8,17 +8,16 @@ import Foundation -extension URL { - +public extension URL { /// Initializes URL with string. Same as URL(string:), but returns URL (not optional.) /// In case of failure in initializing, fatal error will be thrown. - public init(forceString string: String) { + init(forceString string: String) { guard let url = URL(string: string) else { fatalError("Could not init URL '\(string)'") } self = url } - + /// Returns URL by settings URL.queryItems to specified parameters. - public func adding(parameters: [String: String]) -> URL { + func adding(parameters: [String: String]) -> URL { guard parameters.count > 0 else { return self } var queryItems: [URLQueryItem] = [] for parameter in parameters { @@ -26,7 +25,7 @@ extension URL { } return adding(queryItems: queryItems) } - + /// Returns URL by settings URL.queryItems to specified queryItems. private func adding(queryItems: [URLQueryItem]) -> URL { guard var urlComponents = URLComponents(string: absoluteString) else { @@ -38,5 +37,4 @@ extension URL { } return url } - } diff --git a/Sources/MBNetworking/Networkable+DataTask.swift b/Sources/MBNetworking/Networkable+DataTask.swift index 0dcf6db..575c24e 100644 --- a/Sources/MBNetworking/Networkable+DataTask.swift +++ b/Sources/MBNetworking/Networkable+DataTask.swift @@ -1,5 +1,5 @@ // -// Network.swift +// Networkable+DataTask.swift // Network // // Created by Rasid Ramazanov on 25.11.2019. @@ -23,24 +23,23 @@ extension Networkable { if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { RunLoop.current.run(until: Date().addingTimeInterval(0.05)) } - + fetch(request, completion: completion) - } - + private func fetch( _ urlRequest: URLRequest, completion: @escaping ((Result) -> Void) ) { requestData(urlRequest) { response, data, error in - + if let error = error, self.isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - + } else if let error = error { let networkingError: NetworkingError if (error as NSError).code == NSURLErrorCancelled { @@ -51,20 +50,20 @@ extension Networkable { MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) self.printErrorLog(networkingError) completion(.failure(networkingError)) - + } else if let httpResponse = response as? HTTPURLResponse, self.isSuccess(httpResponse.statusCode) { let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - + } else if let response = response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - + } else if let data = data, !data.isEmpty { do { // If requested decodable type is Data, received data will be returned. @@ -80,7 +79,7 @@ extension Networkable { self.printErrorLog(error) completion(.failure(error)) } - + } else { let error = MBErrorKit.NetworkingError.unkownError(error, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) @@ -89,18 +88,18 @@ extension Networkable { } } } - + func isNetworkConnectionError(_ errorCode: Int) -> Bool { errorCode == NSURLErrorNetworkConnectionLost - || errorCode == NSURLErrorNotConnectedToInternet - || errorCode == NSURLErrorCannotConnectToHost - || errorCode == 53 + || errorCode == NSURLErrorNotConnectedToInternet + || errorCode == NSURLErrorCannotConnectToHost + || errorCode == 53 } - + func isSuccess(_ errorCode: Int) -> Bool { !(200 ... 399).contains(errorCode) } - + private func requestData(_ urlRequest: URLRequest, completion: @escaping ((URLResponse?, Data?, Error?) -> Void)) { let taskId = UUID().uuidString let task = Session.shared.session @@ -111,9 +110,9 @@ extension Networkable { } Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) } - + Session.shared.tasksInProgress.removeValue(forKey: taskId) - + self.printResponse(data) DispatchQueue.main.async { completion(response, data, error) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 980de0d..fdaebf7 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -11,8 +11,7 @@ import MBErrorKit /// Networkable extension related to data tasks. /// -@available(iOS 13.0.0, *) -extension Networkable { +@available(iOS 13.0.0, *) extension Networkable { /// Fetch data with specified parameters and return back with the completion. /// - Parameters: /// - type: Type of the result. @@ -20,17 +19,17 @@ extension Networkable { public func fetch(_ type: V.Type) async throws -> V { try await fetch(request) } - + private func fetch(_ urlRequest: URLRequest) async throws -> V { let (response, data, error) = await requestData(urlRequest) - + if let error = error, isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let error = error { let networkingError: NetworkingError if (error as NSError).code == NSURLErrorCancelled { @@ -41,20 +40,20 @@ extension Networkable { MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) printErrorLog(networkingError) throw networkingError - + } else if let httpResponse = response as? HTTPURLResponse, isSuccess(httpResponse.statusCode) { let error = MBErrorKit.NetworkingError.httpError(error, httpResponse, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let response = response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - + } else if let data = data, data.count > 0 { do { // If requested decodable type is Data, received data will be returned. @@ -69,7 +68,7 @@ extension Networkable { self.printErrorLog(error) throw error } - + } else { let error = MBErrorKit.NetworkingError.unkownError(error, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) @@ -77,12 +76,12 @@ extension Networkable { throw error } } - + private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { let taskId = UUID().uuidString do { let (data, response) = try await Session.shared.session.data(for: urlRequest) - + if let task = Session.shared.tasksInProgress[taskId] { Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) Self.finalizeTask(withId: taskId, task: task) @@ -97,10 +96,10 @@ extension Networkable { return (nil, nil, error) } } - + private static func finalizeTask(withId taskId: String, task: URLSessionDataTask) { Session.shared.tasksInProgress.removeValue(forKey: taskId) - + Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) Session.shared.tasksInProgress.updateValue(task, forKey: taskId) } diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index f5ae261..b39d5ba 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -29,7 +29,7 @@ extension URLSessionPinningDelegateProtocol { class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtocol { var certificatePaths: [String] = [] - + func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, @@ -39,9 +39,12 @@ class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtoc completionHandler(.cancelAuthenticationChallenge, nil) return } - - let disposition = URLSessionPinningHelper.handleChallengeCommon(certificatePaths: certificatePaths, - serverCertificate: serverCertificate) + + let disposition = URLSessionPinningHelper.handleChallengeCommon( + certificatePaths: certificatePaths, + + serverCertificate: serverCertificate + ) if disposition == .useCredential { completionHandler(disposition, URLCredential(trust: serverCertificate.trust)) } else { @@ -50,11 +53,9 @@ class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtoc } } - -@available(iOS 13.0, *) -class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtocol { +@available(iOS 13.0, *) class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtocol { var certificatePaths: [String] = [] - + func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge @@ -62,8 +63,11 @@ class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtoco guard let serverCertificate = URLSessionPinningHelper.getServerCertificate(forChallenge: challenge) else { return (.cancelAuthenticationChallenge, nil) } - - let disposition = URLSessionPinningHelper.handleChallengeCommon(certificatePaths: certificatePaths, serverCertificate: serverCertificate) + + let disposition = URLSessionPinningHelper.handleChallengeCommon( + certificatePaths: certificatePaths, + serverCertificate: serverCertificate + ) if disposition == .useCredential { return (disposition, URLCredential(trust: serverCertificate.trust)) } else { @@ -84,11 +88,11 @@ private class URLSessionPinningHelper { else { return nil } - + let serverCertificateData = SecCertificateCopyData(serverCertificate) return (serverCertificateData, serverTrust) } - + static func handleChallengeCommon( certificatePaths: [String], serverCertificate: (data: CFData, trust: SecTrust) @@ -103,7 +107,7 @@ private class URLSessionPinningHelper { } } } - + if certificatePaths.isEmpty { return .performDefaultHandling } else { diff --git a/Sources/MBNetworking/Networkable+URLRequest.swift b/Sources/MBNetworking/Networkable+URLRequest.swift index b90c9b2..c60b95f 100644 --- a/Sources/MBNetworking/Networkable+URLRequest.swift +++ b/Sources/MBNetworking/Networkable+URLRequest.swift @@ -10,11 +10,10 @@ import Foundation import MBErrorKit /// `Networkable` extension related to `URLRequest`'s. -extension Networkable { - +public extension Networkable { /** Returns GET or DELETE `URLRequest` with specified url and query item. - + - parameter url: `URL`. - parameter queryItems: Query items to be appended to the url, eg, pageSize: 10 will be appended to url as &pageSize=10. @@ -22,38 +21,46 @@ extension Networkable { - parameter httpMethod: HTTP method. (GET, DELETE) - returns: `URLRequest` with specified url and query item. */ - public func getRequest(url: URL, - queryItems: [String: String] = [:], - headers: [String: String] = [:], - httpMethod: RequestType = .GET) -> URLRequest { - //TODO: throw exception when an unexpected http method is encountered + func getRequest( + url: URL, + queryItems: [String: String] = [:], + headers: [String: String] = [:], + httpMethod: RequestType = .GET + ) -> URLRequest { + // TODO: throw exception when an unexpected http method is encountered let url = url.adding(parameters: queryItems) - var request = getRequest(with: url, - httpMethod: httpMethod, - headers: headers, - contentType: .json) + var request = getRequest( + with: url, + httpMethod: httpMethod, + headers: headers, + contentType: .json + ) request.timeoutInterval = Session.shared.timeout.request return request } - + /** Returns POST, PUT or DELETE `URLRequest` with specified url and encodable body object. - + - parameter url: `URL`. - parameter encodable: Any object confirming `Encodable` to be used in `URLRequest.httpBody`. - parameter headers: HTTP headers. - parameter httpMethod: HTTP method. (DELETE, POST, PUT) - returns: `URLRequest` with specified url and encodable body object. */ - public func getRequest(url: URL, - encodable data: T, - headers: [String: String] = [:], - httpMethod: RequestType = .POST) -> URLRequest { - //TODO: throw exception when an unexpected http method is encountered - var request = getRequest(with: url, - httpMethod: httpMethod, - headers: headers, - contentType: .json) + func getRequest( + url: URL, + encodable data: T, + headers: [String: String] = [:], + httpMethod: RequestType = .POST + ) -> URLRequest { + // TODO: throw exception when an unexpected http method is encountered + var request = getRequest( + with: url, + httpMethod: httpMethod, + headers: headers, + contentType: .json + ) do { request.httpBody = try JSONEncoder().encode(data) } catch { @@ -65,103 +72,119 @@ extension Networkable { request.timeoutInterval = Session.shared.timeout.request return request } - + /** Returns POST or PUT `URLRequest` with specified url and form item. - + - parameter url: `URL`. - parameter formItems: HashMap to be used in `URLRequest.httpBody`. - parameter headers: HTTP headers. - parameter httpMethod: HTTP method. (POST, PUT) - returns: `URLRequest` with specified url and form item. */ - public func getRequest(url: URL, - formItems: [String: String] = [:], - headers: [String: String] = [:], - httpMethod: RequestType = .POST) -> URLRequest { - //TODO: throw exception when an unexpected http method is encountered - let formData = formItems.map({ + func getRequest( + url: URL, + formItems: [String: String] = [:], + headers: [String: String] = [:], + httpMethod: RequestType = .POST + ) -> URLRequest { + // TODO: throw exception when an unexpected http method is encountered + let formData = formItems.map { "\($0.key)=\($0.value.addingPercentEncoding(withAllowedCharacters: .nwURLQueryAllowed) ?? "")" - }).joined(separator: "&") - var request = getRequest(with: url, - httpMethod: httpMethod, - headers: headers, - contentType: .urlencoded) + }.joined(separator: "&") + var request = getRequest( + with: url, + httpMethod: httpMethod, + headers: headers, + contentType: .urlencoded + ) request.httpBody = formData.data(using: .utf8) request.timeoutInterval = Session.shared.timeout.request return request } - + /** Returns `URLRequest` for file upload - + - parameter url: `URL`. - parameter parameters: Parameters to be added to multipart `URLRequest.httpBody`. - parameter files: Files to be added to multipart `URLRequest.httpBody`. - parameter headers: HTTP headers. - returns: `URLRequest` with specified parameters. */ - public func uploadRequest(url: URL, - parameters: [String: String] = [:], files: [File] = [], - headers: [String: String] = [:]) -> URLRequest { + func uploadRequest( + url: URL, + parameters: [String: String] = [:], + files: [File] = [], + headers: [String: String] = [:] + ) -> URLRequest { let body = NSMutableData() let boundary = "Boundary-\(UUID().uuidString)" let boundaryPrefix = "--\(boundary)\r\n" - + for (key, value) in parameters { body.appendString(boundaryPrefix) body.appendString("Content-Disposition: form-data; name=\"\(key)\"\r\n\r\n") body.appendString("\(value)\r\n") } - + for file in files { body.appendString(boundaryPrefix) - body.appendString("Content-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.fileNameWithExtension)\"\r\n") + body + .appendString( + "Content-Disposition: form-data; name=\"\(file.name)\"; filename=\"\(file.fileNameWithExtension)\"\r\n" + ) body.appendString("Content-Type: \(file.mimeType)\r\n\r\n") body.append(file.data) body.appendString("\r\n") } body.appendString("--".appending(boundary.appending("--"))) - + let url = url - var request = getRequest(with: url, - httpMethod: .POST, - headers: headers, - contentType: .multipartFormData(boundary)) + var request = getRequest( + with: url, + httpMethod: .POST, + headers: headers, + contentType: .multipartFormData(boundary) + ) request.httpBody = body as Data request.timeoutInterval = Session.shared.timeout.request return request } - + /** Returns `URLRequest` with specified url and httpMehthod. - + - parameter url: `URL` of the request. - parameter httpMethod: HTTP method of the reuest, either GET or POST. - parameter headers: HTTP headers. - parameter contentType: Content-Type of the request. - returns: `URLRequest` with specified url and httpMehthod. */ - private func getRequest(with url: URL, httpMethod: RequestType, - headers: [String: String], - contentType: NetworkContentType) -> URLRequest { + private func getRequest( + with url: URL, + httpMethod: RequestType, + headers: [String: String], + contentType: NetworkContentType + ) -> URLRequest { var request = URLRequest(url: url) request.httpMethod = httpMethod.rawValue - + request.allHTTPHeaderFields = getHeaders(headers, contentType: contentType) request.cachePolicy = .reloadIgnoringLocalAndRemoteCacheData - + return request } - + /// Returns default headers for used `URLRequest`s. - private func getHeaders(_ headers: [String: String], - contentType: NetworkContentType) -> [String: String] { + private func getHeaders( + _ headers: [String: String], + contentType: NetworkContentType + ) -> [String: String] { var heads = headers heads["Content-Type"] = contentType.rawValue return heads } - } /// "Content-Type" values for network requests. @@ -170,19 +193,18 @@ public enum NetworkContentType { case json case urlencoded case multipartFormData(String) - + var rawValue: String { switch self { case .json: return "application/json" case .urlencoded: return "application/x-www-form-urlencoded" - case .multipartFormData(let boundary): return "multipart/form-data; boundary=\(boundary)" + case let .multipartFormData(boundary): return "multipart/form-data; boundary=\(boundary)" } } } /// Request types to be passed as `URLRequest.httpMethod`. public enum RequestType: String { - /// HTTP GET request case GET /// HTTP POST request diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift index daa10d2..bae55bb 100644 --- a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -16,8 +16,7 @@ func createUntrustedURLSessionDelegate() -> URLSessionDelegate { } } -@available(iOS 13.0, *) -class UntrustedURLSessionDelegateAsync: NSObject, URLSessionDelegate { +@available(iOS 13.0, *) class UntrustedURLSessionDelegateAsync: NSObject, URLSessionDelegate { func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge @@ -26,7 +25,6 @@ class UntrustedURLSessionDelegateAsync: NSObject, URLSessionDelegate { } } - class UntrustedURLSessionDelegateLegacy: NSObject, URLSessionDelegate { func urlSession( _ session: URLSession, diff --git a/Sources/MBNetworking/Networkable.swift b/Sources/MBNetworking/Networkable.swift index 3d68af4..95d7e82 100644 --- a/Sources/MBNetworking/Networkable.swift +++ b/Sources/MBNetworking/Networkable.swift @@ -1,5 +1,5 @@ // -// NetworkEndpoint.swift +// Networkable.swift // Networking // // Created by Rasid Ramazanov on 25.11.2019. diff --git a/Sources/MBNetworking/NetworkableConfigs.swift b/Sources/MBNetworking/NetworkableConfigs.swift index 9a672bf..ffb967f 100644 --- a/Sources/MBNetworking/NetworkableConfigs.swift +++ b/Sources/MBNetworking/NetworkableConfigs.swift @@ -42,7 +42,7 @@ public enum NetworkableConfigs { public func set(configuration: URLSessionConfiguration) { Session.shared.configuration = configuration } - + /// Sets `NetworkLogMonitoringDelegate` that used for network log monitoring. /// Default value is `nil`. /// - Parameter networkLogMonitoringDelegate: NetworkLogMonitoringDelegate. diff --git a/Sources/MBNetworking/Session.swift b/Sources/MBNetworking/Session.swift index 5f438e4..678086b 100644 --- a/Sources/MBNetworking/Session.swift +++ b/Sources/MBNetworking/Session.swift @@ -49,7 +49,7 @@ final class Session { ) } } - + /// `NetworkLogMonitoringDelegate` that used for network log monitoring. /// Default value is `nil`. var networkLogMonitoringDelegate: NetworkLogMonitoringDelegate? diff --git a/Sources/MBNetworking/StubURLProtocol/Result.swift b/Sources/MBNetworking/StubURLProtocol/Result.swift index 9ed5b0f..0b8b45d 100644 --- a/Sources/MBNetworking/StubURLProtocol/Result.swift +++ b/Sources/MBNetworking/StubURLProtocol/Result.swift @@ -8,10 +8,8 @@ import Foundation -extension StubURLProtocol { - - public enum Result { - +public extension StubURLProtocol { + enum Result { /// Successfull result with specified data /// You can use `StubURLProtocol.Result.getData()` to read mock data from bundle, easily and inline. case success(Data) @@ -21,13 +19,10 @@ extension StubURLProtocol { /// Failure with the specified status code. /// The actual result will of `Networkable.fetch` will be `NetworkingError.httpError`. case failureStatusCode(Int) - } - } public extension StubURLProtocol.Result { - /// Prepares `StubURLProtocol.Result.success(Data)` from specified Bundle path. /// - Parameter url: Bundle URL for the specifed resource. Can be received from `url(forResource:,ofType:)`. /// - Returns: Returns `StubURLProtocol.Result.success(Data)` with data from specified file url. @@ -50,5 +45,4 @@ public extension StubURLProtocol.Result { } return .success(data) } - } diff --git a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift index f5ea2b7..dde79fb 100644 --- a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift +++ b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift @@ -12,7 +12,6 @@ import MBErrorKit /// URLProtocol for simplifying unit tests by acting man-in-the-middle on for the session. /// It's configured to work only with test targets. It won't work if there's no test process in progress. public final class StubURLProtocol: URLProtocol { - /// Result of the request, which is going to happen. public static var result: Result? { didSet { @@ -32,24 +31,22 @@ public final class StubURLProtocol: URLProtocol { static var isEnabled: Bool { return result != nil } - } -extension StubURLProtocol { - - public override class func canInit(with request: URLRequest) -> Bool { +public extension StubURLProtocol { + override class func canInit(with request: URLRequest) -> Bool { return isEnabled } - public override class func canInit(with task: URLSessionTask) -> Bool { + override class func canInit(with task: URLSessionTask) -> Bool { return isEnabled } - public override class func canonicalRequest(for request: URLRequest) -> URLRequest { + override class func canonicalRequest(for request: URLRequest) -> URLRequest { return request } - public override func startLoading() { + override func startLoading() { Timer.scheduledTimer(withTimeInterval: StubURLProtocol.delay, repeats: false) { [weak self] _ in guard let self = self else { return } guard let result = StubURLProtocol.result else { @@ -60,7 +57,7 @@ extension StubURLProtocol { switch result { case let .success(data): self.client?.urlProtocol(self, didLoad: data) - + if let url = request.url, let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) @@ -69,8 +66,16 @@ extension StubURLProtocol { self.client?.urlProtocol(self, didFailWithError: error) case let .failureStatusCode(statusCode): if let url = self.request.url, - let response = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil) { - self.client?.urlProtocol(self, cachedResponseIsValid: CachedURLResponse(response: response, data: Data())) + let response = HTTPURLResponse( + url: url, + statusCode: statusCode, + httpVersion: nil, + headerFields: nil + ) { + self.client?.urlProtocol( + self, + cachedResponseIsValid: CachedURLResponse(response: response, data: Data()) + ) } } @@ -78,8 +83,7 @@ extension StubURLProtocol { } } - public override func stopLoading() { + override func stopLoading() { // Nothing to handle } - } diff --git a/Sources/MBNetworking/UploadFile.swift b/Sources/MBNetworking/UploadFile.swift index 77ea854..3e5d5da 100644 --- a/Sources/MBNetworking/UploadFile.swift +++ b/Sources/MBNetworking/UploadFile.swift @@ -10,7 +10,6 @@ import Foundation /// Multipart body file public struct File { - /// Key of multipart form data. public var name: String /// File name @@ -51,5 +50,4 @@ public struct File { self.mimeType = mimeType self.data = data } - } diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift index 92b6fd5..89e63e6 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceAsyncTests.swift @@ -7,130 +7,128 @@ // #if canImport(UIKit) -import Foundation -import UIKit -import XCTest -@testable import MBErrorKit -@testable import MBNetworking -@testable import MobKitCore - -class NetworkablePerformanceAsyncTests: XCTestCase { - var imageView: UIImageView = .init() - - override func setUp() { - MobKit.isDeveloperModeOn = true - NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) - } - - func testWhenMultipleDownloadCommandCalled() async throws { - let expectation = XCTestExpectation(description: "wait for image") - - for i in 0 ..< 100 { - try await downloadImage(index: i) - expectation.fulfill() - + import Foundation + import UIKit + import XCTest + @testable import MBErrorKit + @testable import MBNetworking + @testable import MobKitCore + + class NetworkablePerformanceAsyncTests: XCTestCase { + var imageView: UIImageView = .init() + + override func setUp() { + MobKit.isDeveloperModeOn = true + NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) } - - await fulfillment(of: [expectation], timeout: 10) - } - - private func downloadImage(index: Int) async throws { - try await imageView.downloadImageFrom(index: index) - } -} -extension UIImageView { - func downloadImageFrom(index: Int) async throws { - if let savedImage = FileIOManager.readFile("\(index)"), - let image = UIImage(data: savedImage) { - self.image = image - return + func testWhenMultipleDownloadCommandCalled() async throws { + let expectation = XCTestExpectation(description: "wait for image") + + for i in 0 ..< 100 { + try await downloadImage(index: i) + expectation.fulfill() + } + + await fulfillment(of: [expectation], timeout: 10) } - - let data = try await getProfilePhoto() - - image = UIImage(named: "ky_avatar") - let image = UIImage(data: data) - self.image = image - FileIOManager.writeFile("\(index)", content: data) - } - - private func getProfilePhoto() async throws -> Data { - try await API.getProfilePhoto.fetch(Data.self) - } -} -enum API: Networkable { - case getProfilePhoto - - var request: URLRequest { - URLRequest(url: URL(forceString: "https://picsum.photos/200/300")) + private func downloadImage(index: Int) async throws { + try await imageView.downloadImageFrom(index: index) + } } -} -enum UIImageManager { - static func convertImageToBase64String(img: UIImage) -> String { - img.pngData()?.base64EncodedString() ?? "" - } - - static func convertBase64StringToImage(data: Data) -> UIImage? { - UIImage(data: data) - } -} - -enum FileIOManager { - private static let localDirectory = "kutup_pp" - - @discardableResult static func writeFile(_ fileName: String, content: Data) -> Bool { - guard let directory = getFileDirectory() else { - return false + extension UIImageView { + func downloadImageFrom(index: Int) async throws { + if let savedImage = FileIOManager.readFile("\(index)"), + let image = UIImage(data: savedImage) { + self.image = image + return + } + + let data = try await getProfilePhoto() + + image = UIImage(named: "ky_avatar") + let image = UIImage(data: data) + self.image = image + FileIOManager.writeFile("\(index)", content: data) } - guard createDirectoryIfNeeded(directory) else { - return false + + private func getProfilePhoto() async throws -> Data { + try await API.getProfilePhoto.fetch(Data.self) } - let fileURL = directory.appendingPathComponent(fileName) - do { - try content.write(to: fileURL, options: .atomic) - return true - } catch { - return false + } + + enum API: Networkable { + case getProfilePhoto + + var request: URLRequest { + URLRequest(url: URL(forceString: "https://picsum.photos/200/300")) } } - - private static func createDirectoryIfNeeded(_ directory: URL) -> Bool { - guard !FileManager.default.fileExists(atPath: directory.absoluteString) else { - // Directory exists, no need to recreate it. - return true + + enum UIImageManager { + static func convertImageToBase64String(img: UIImage) -> String { + img.pngData()?.base64EncodedString() ?? "" } - do { - try FileManager.default.createDirectory( - at: directory, withIntermediateDirectories: true, attributes: nil - ) - return true - } catch { - print(error.localizedDescription) - return false + + static func convertBase64StringToImage(data: Data) -> UIImage? { + UIImage(data: data) } } - - static func readFile(_ fileName: String) -> Data? { - guard let directory = getFileDirectory() else { - return nil + + enum FileIOManager { + private static let localDirectory = "kutup_pp" + + @discardableResult static func writeFile(_ fileName: String, content: Data) -> Bool { + guard let directory = getFileDirectory() else { + return false + } + guard createDirectoryIfNeeded(directory) else { + return false + } + let fileURL = directory.appendingPathComponent(fileName) + do { + try content.write(to: fileURL, options: .atomic) + return true + } catch { + return false + } } - let fileURL = directory.appendingPathComponent(fileName) - do { - return try Data(contentsOf: fileURL) - } catch { - return nil + + private static func createDirectoryIfNeeded(_ directory: URL) -> Bool { + guard !FileManager.default.fileExists(atPath: directory.absoluteString) else { + // Directory exists, no need to recreate it. + return true + } + do { + try FileManager.default.createDirectory( + at: directory, withIntermediateDirectories: true, attributes: nil + ) + return true + } catch { + print(error.localizedDescription) + return false + } } - } - - private static func getFileDirectory() -> URL? { - guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { - return nil + + static func readFile(_ fileName: String) -> Data? { + guard let directory = getFileDirectory() else { + return nil + } + let fileURL = directory.appendingPathComponent(fileName) + do { + return try Data(contentsOf: fileURL) + } catch { + return nil + } + } + + private static func getFileDirectory() -> URL? { + guard let directory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else { + return nil + } + return directory.appendingPathComponent(localDirectory, isDirectory: true) } - return directory.appendingPathComponent(localDirectory, isDirectory: true) } -} #endif - diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift index 32c48d9..d8e2e86 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift @@ -1,65 +1,65 @@ // -// NetworkablePerformanceTests.swift +// NetworkablePerformanceLegacyTests.swift // MBNetworkingTests // // Created by Rashid Ramazanov on 2/23/22. // #if canImport(UIKit) -import Foundation -import UIKit -import XCTest -@testable import MBErrorKit -@testable import MBNetworking -@testable import MobKitCore + import Foundation + import UIKit + import XCTest + @testable import MBErrorKit + @testable import MBNetworking + @testable import MobKitCore -class NetworkablePerformanceLegacyTests: XCTestCase { - var imageView: UIImageView = .init() + class NetworkablePerformanceLegacyTests: XCTestCase { + var imageView: UIImageView = .init() - override func setUp() { - MobKit.isDeveloperModeOn = true - NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) - } - - func testWhenMultipleDownloadCommandCalled() { - let expectation = XCTestExpectation(description: "wait for image") - for i in 0 ..< 1000 { - downloadImage(index: i) + override func setUp() { + MobKit.isDeveloperModeOn = true + NetworkableConfigs.default.set(configuration: URLSessionConfiguration.ephemeral) } - XCTWaiter().wait(for: [expectation], timeout: 10) - Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in - expectation.fulfill() + + func testWhenMultipleDownloadCommandCalled() { + let expectation = XCTestExpectation(description: "wait for image") + for i in 0 ..< 1000 { + downloadImage(index: i) + } + XCTWaiter().wait(for: [expectation], timeout: 10) + Timer.scheduledTimer(withTimeInterval: 10, repeats: false) { _ in + expectation.fulfill() + } } - } - private func downloadImage(index: Int) { - imageView.downloadImageFrom(index: index) + private func downloadImage(index: Int) { + imageView.downloadImageFrom(index: index) + } } -} -extension UIImageView { - func downloadImageFrom(index: Int) { - if let savedImage = FileIOManager.readFile("\(index)"), - let image = UIImage(data: savedImage) { - self.image = image - } - getProfilePhoto { result in - switch result { - case let .success(data): - self.image = UIImage(named: "ky_avatar") - let image = UIImage(data: data) + extension UIImageView { + func downloadImageFrom(index: Int) { + if let savedImage = FileIOManager.readFile("\(index)"), + let image = UIImage(data: savedImage) { self.image = image - FileIOManager.writeFile("\(index)", content: data) - case .failure(_): - return + } + getProfilePhoto { result in + switch result { + case let .success(data): + self.image = UIImage(named: "ky_avatar") + let image = UIImage(data: data) + self.image = image + FileIOManager.writeFile("\(index)", content: data) + case .failure: + return + } } } - } - private func getProfilePhoto( - completion: @escaping (Result) -> Void - ) { - API.getProfilePhoto.fetch(Data.self, completion: completion) + private func getProfilePhoto( + completion: @escaping (Result) -> Void + ) { + API.getProfilePhoto.fetch(Data.self, completion: completion) + } } -} #endif diff --git a/Tests/MBNetworkingTests/NetworkingAsyncTests.swift b/Tests/MBNetworkingTests/NetworkingAsyncTests.swift index d222c8f..9afac74 100644 --- a/Tests/MBNetworkingTests/NetworkingAsyncTests.swift +++ b/Tests/MBNetworkingTests/NetworkingAsyncTests.swift @@ -11,28 +11,28 @@ import XCTest @testable import MobKitCore #if canImport(UIKit) -class NetworkingAsyncTests: XCTestCase { - override func setUp() { - MobKit.isDeveloperModeOn = true - StubURLProtocol.delay = .zero - } - - func testDataDownloadAsync() async throws { - StubURLProtocol - .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) - var image: UIImage? - - let result = try await Download.data( - url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") - ).fetch(Data.self) - - if case let .success(actualData) = StubURLProtocol.result { - XCTAssertEqual(result, actualData) + class NetworkingAsyncTests: XCTestCase { + override func setUp() { + MobKit.isDeveloperModeOn = true + StubURLProtocol.delay = .zero + } + + func testDataDownloadAsync() async throws { + StubURLProtocol + .result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) + var image: UIImage? + + let result = try await Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) + + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(result, actualData) + } + + image = UIImage(data: result) + + XCTAssertNotNil(image) } - - image = UIImage(data: result) - - XCTAssertNotNil(image) } -} #endif diff --git a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift index 0bd4dd1..3627746 100644 --- a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift +++ b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift @@ -1,5 +1,5 @@ // -// NetworkingTests.swift +// NetworkingLegacyTests.swift // NetworkingTests // // Created by Rasid Ramazanov on 17.02.2020. @@ -11,39 +11,42 @@ import XCTest @testable import MobKitCore #if canImport(UIKit) -class NetworkingLegacyTests: XCTestCase { - override func setUp() { - MobKit.isDeveloperModeOn = true - StubURLProtocol.delay = .zero - } - - func testDataDownload() { - let expectation = expectation(description: "Data download should succeed and produce a valid image") - StubURLProtocol.result = .getData(from: Bundle.module.url(forResource: "imageDownload", withExtension: "jpg")) - var image: UIImage? - - Download.data( - url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") - ).fetch(Data.self) { result in - if case let .success(data) = result { - if case let .success(actualData) = StubURLProtocol.result { - XCTAssertEqual(data, actualData) + class NetworkingLegacyTests: XCTestCase { + override func setUp() { + MobKit.isDeveloperModeOn = true + StubURLProtocol.delay = .zero + } + + func testDataDownload() { + let expectation = expectation(description: "Data download should succeed and produce a valid image") + StubURLProtocol.result = .getData(from: Bundle.module.url( + forResource: "imageDownload", + withExtension: "jpg" + )) + var image: UIImage? + + Download.data( + url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") + ).fetch(Data.self) { result in + if case let .success(data) = result { + if case let .success(actualData) = StubURLProtocol.result { + XCTAssertEqual(data, actualData) + } + + image = UIImage(data: data) } - - image = UIImage(data: data) + expectation.fulfill() } - expectation.fulfill() + waitForExpectations(timeout: 5, handler: nil) + + XCTAssertNotNil(image) } - waitForExpectations(timeout: 5, handler: nil) - - XCTAssertNotNil(image) } -} #endif enum Download: Networkable { case data(url: URL) - + var request: URLRequest { switch self { case let .data(url): diff --git a/Tests/MBNetworkingTests/ResultTests/ResultTestAPI.swift b/Tests/MBNetworkingTests/ResultTests/ResultTestAPI.swift index 67dfdd5..38c4cbd 100644 --- a/Tests/MBNetworkingTests/ResultTests/ResultTestAPI.swift +++ b/Tests/MBNetworkingTests/ResultTests/ResultTestAPI.swift @@ -10,14 +10,12 @@ import Foundation @testable import MBNetworking enum ResultTestAPI: Networkable { - case fetch case underlyingError case httpError - + var request: URLRequest { switch self { - case .fetch: let url = URL(forceString: "https://itunes.apple.com/search") return getRequest(url: url, queryItems: ["media": "music"]) @@ -28,7 +26,5 @@ enum ResultTestAPI: Networkable { let url = URL(forceString: "https://itunes.apple.com/search") return getRequest(url: url, queryItems: ["media": "0"]) } - } - } diff --git a/Tests/MBNetworkingTests/StubURLProtocolTests.swift b/Tests/MBNetworkingTests/StubURLProtocolTests.swift index 5b97c5c..98a5428 100644 --- a/Tests/MBNetworkingTests/StubURLProtocolTests.swift +++ b/Tests/MBNetworkingTests/StubURLProtocolTests.swift @@ -57,14 +57,14 @@ class StubURLProtocolTests: XCTestCase { StubURLProtocol.result = .getData(from: Bundle.module.url(forResource: "results", withExtension: "json")) var response: DecodableTrue? let expectation = expectation(description: "wait for delay") - + Download.data( url: URL(forceString: "https://miro.medium.com/max/1400/1*2AodTHXf8giVb4QoIBGSww.png") ).fetch(DecodableTrue.self) { result in if case let .success(resp) = result { response = resp } - + expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -87,7 +87,7 @@ class StubURLProtocolTests: XCTestCase { if case let .success(data) = result { image = UIImage(data: data) } - + expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) @@ -110,7 +110,7 @@ class StubURLProtocolTests: XCTestCase { expectation.fulfill() } waitForExpectations(timeout: 5, handler: nil) - + // The real image in the link is 1400x637 size. XCTAssertNotNil(image) XCTAssertEqual(image?.size.width, 1400) From 0b2b460efeb171009b12563be89b4b42c34b371c Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Wed, 14 Aug 2024 23:26:34 +0300 Subject: [PATCH 11/18] Swiftformatted. --- .../MBNetworking/Extensions/SecTrust.swift | 6 ++--- .../MBNetworking/Networkable+DataTask.swift | 15 ++++++------ .../Networkable+DataTaskAsync.swift | 15 ++++++------ Sources/MBNetworking/Networkable+Logs.swift | 2 +- .../Networkable+PinningDelegate.swift | 4 ++-- .../MBNetworking/Networkable+URLRequest.swift | 10 ++++---- .../Networkable+UntrustedDelegate.swift | 6 ++--- Sources/MBNetworking/Session.swift | 2 +- .../StubURLProtocol/StubURLProtocol.swift | 24 +++++++++---------- Sources/MBNetworking/UploadFile.swift | 2 +- .../NetworkingLegacyTests.swift | 2 +- Tests/MBNetworkingTests/SSLPinningTests.swift | 2 +- 12 files changed, 44 insertions(+), 46 deletions(-) diff --git a/Sources/MBNetworking/Extensions/SecTrust.swift b/Sources/MBNetworking/Extensions/SecTrust.swift index a853e4d..92b3acc 100644 --- a/Sources/MBNetworking/Extensions/SecTrust.swift +++ b/Sources/MBNetworking/Extensions/SecTrust.swift @@ -7,7 +7,7 @@ import Foundation -public extension Array where Element == SecCertificate { +public extension [SecCertificate] { var publicKeys: [SecKey] { compactMap(\.publicKey) } @@ -32,9 +32,9 @@ public extension SecCertificate { public extension SecTrust { var certificates: [SecCertificate] { if #available(iOS 15, macOS 12, watchOS 8, *) { - return (SecTrustCopyCertificateChain(self) as? [SecCertificate]) ?? [] + (SecTrustCopyCertificateChain(self) as? [SecCertificate]) ?? [] } else { - return (0 ..< SecTrustGetCertificateCount(self)).compactMap { index in + (0 ..< SecTrustGetCertificateCount(self)).compactMap { index in SecTrustGetCertificateAtIndex(self, index) } } diff --git a/Sources/MBNetworking/Networkable+DataTask.swift b/Sources/MBNetworking/Networkable+DataTask.swift index 575c24e..692fa4b 100644 --- a/Sources/MBNetworking/Networkable+DataTask.swift +++ b/Sources/MBNetworking/Networkable+DataTask.swift @@ -33,19 +33,18 @@ extension Networkable { ) { requestData(urlRequest) { response, data, error in - if let error = error, + if let error, self.isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - } else if let error = error { - let networkingError: NetworkingError - if (error as NSError).code == NSURLErrorCancelled { - networkingError = .dataTaskCancelled + } else if let error { + let networkingError: NetworkingError = if (error as NSError).code == NSURLErrorCancelled { + .dataTaskCancelled } else { - networkingError = MBErrorKit.NetworkingError.underlyingError(error, response, data) + MBErrorKit.NetworkingError.underlyingError(error, response, data) } MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) self.printErrorLog(networkingError) @@ -58,13 +57,13 @@ extension Networkable { self.printErrorLog(error) completion(.failure(error)) - } else if let response = response, data == nil || data?.count == 0 { + } else if let response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) self.printErrorLog(error) completion(.failure(error)) - } else if let data = data, !data.isEmpty { + } else if let data, !data.isEmpty { do { // If requested decodable type is Data, received data will be returned. if V.Type.self == Data.Type.self { diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index fdaebf7..b2c2cf2 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -23,19 +23,18 @@ import MBErrorKit private func fetch(_ urlRequest: URLRequest) async throws -> V { let (response, data, error) = await requestData(urlRequest) - if let error = error, + if let error, isNetworkConnectionError((error as NSError).code) { let error = MBErrorKit.NetworkingError.networkConnectionError(error) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - } else if let error = error { - let networkingError: NetworkingError - if (error as NSError).code == NSURLErrorCancelled { - networkingError = .dataTaskCancelled + } else if let error { + let networkingError: NetworkingError = if (error as NSError).code == NSURLErrorCancelled { + .dataTaskCancelled } else { - networkingError = MBErrorKit.NetworkingError.underlyingError(error, response, data) + MBErrorKit.NetworkingError.underlyingError(error, response, data) } MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: networkingError) printErrorLog(networkingError) @@ -48,13 +47,13 @@ import MBErrorKit printErrorLog(error) throw error - } else if let response = response, data == nil || data?.count == 0 { + } else if let response, data == nil || data?.count == 0 { let error = MBErrorKit.NetworkingError.dataTaskError(response, data) MBErrorKit.ErrorKit.shared().delegate?.errorKitDidCatch(networkingError: error) printErrorLog(error) throw error - } else if let data = data, data.count > 0 { + } else if let data, data.count > 0 { do { // If requested decodable type is Data, received data will be returned. if V.Type.self == Data.Type.self { diff --git a/Sources/MBNetworking/Networkable+Logs.swift b/Sources/MBNetworking/Networkable+Logs.swift index 730206a..a10ab47 100644 --- a/Sources/MBNetworking/Networkable+Logs.swift +++ b/Sources/MBNetworking/Networkable+Logs.swift @@ -54,7 +54,7 @@ extension Networkable { } private func getStringFrom(_ data: Data?) -> String { - if let data = data { + if let data { if let jsonObject = try? JSONSerialization.jsonObject(with: data, options: .allowFragments), !(jsonObject is NSNull), let json = try? JSONSerialization.data(withJSONObject: jsonObject, options: .prettyPrinted), diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index b39d5ba..6be578d 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -15,9 +15,9 @@ protocol URLSessionPinningDelegateProtocol: URLSessionDelegate { func createURLSessionPinningDelegate() -> URLSessionPinningDelegateProtocol { if #available(iOS 13.0, *) { - return URLSessionPinningDelegateAsync() + URLSessionPinningDelegateAsync() } else { - return URLSessionPinningDelegateLegacy() + URLSessionPinningDelegateLegacy() } } diff --git a/Sources/MBNetworking/Networkable+URLRequest.swift b/Sources/MBNetworking/Networkable+URLRequest.swift index c60b95f..4744ac9 100644 --- a/Sources/MBNetworking/Networkable+URLRequest.swift +++ b/Sources/MBNetworking/Networkable+URLRequest.swift @@ -48,9 +48,9 @@ public extension Networkable { - parameter httpMethod: HTTP method. (DELETE, POST, PUT) - returns: `URLRequest` with specified url and encodable body object. */ - func getRequest( + func getRequest( url: URL, - encodable data: T, + encodable data: some Encodable, headers: [String: String] = [:], httpMethod: RequestType = .POST ) -> URLRequest { @@ -196,9 +196,9 @@ public enum NetworkContentType { var rawValue: String { switch self { - case .json: return "application/json" - case .urlencoded: return "application/x-www-form-urlencoded" - case let .multipartFormData(boundary): return "multipart/form-data; boundary=\(boundary)" + case .json: "application/json" + case .urlencoded: "application/x-www-form-urlencoded" + case let .multipartFormData(boundary): "multipart/form-data; boundary=\(boundary)" } } } diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift index bae55bb..b95f335 100644 --- a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -10,9 +10,9 @@ import Foundation func createUntrustedURLSessionDelegate() -> URLSessionDelegate { if #available(iOS 13.0, *) { - return UntrustedURLSessionDelegateAsync() + UntrustedURLSessionDelegateAsync() } else { - return UntrustedURLSessionDelegateLegacy() + UntrustedURLSessionDelegateLegacy() } } @@ -21,7 +21,7 @@ func createUntrustedURLSessionDelegate() -> URLSessionDelegate { _ session: URLSession, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - return (.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) + (.useCredential, URLCredential(trust: challenge.protectionSpace.serverTrust!)) } } diff --git a/Sources/MBNetworking/Session.swift b/Sources/MBNetworking/Session.swift index 678086b..133fa4c 100644 --- a/Sources/MBNetworking/Session.swift +++ b/Sources/MBNetworking/Session.swift @@ -11,7 +11,7 @@ import Foundation final class Session { static var instance: Session? static var shared: Session { - guard let instance = instance else { + guard let instance else { self.instance = Session() return self.instance! } diff --git a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift index dde79fb..a3f4914 100644 --- a/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift +++ b/Sources/MBNetworking/StubURLProtocol/StubURLProtocol.swift @@ -29,57 +29,57 @@ public final class StubURLProtocol: URLProtocol { public static var delay: TimeInterval = 0 static var isEnabled: Bool { - return result != nil + result != nil } } public extension StubURLProtocol { override class func canInit(with request: URLRequest) -> Bool { - return isEnabled + isEnabled } override class func canInit(with task: URLSessionTask) -> Bool { - return isEnabled + isEnabled } override class func canonicalRequest(for request: URLRequest) -> URLRequest { - return request + request } override func startLoading() { Timer.scheduledTimer(withTimeInterval: StubURLProtocol.delay, repeats: false) { [weak self] _ in - guard let self = self else { return } + guard let self else { return } guard let result = StubURLProtocol.result else { - self.client?.urlProtocolDidFinishLoading(self) + client?.urlProtocolDidFinishLoading(self) return } switch result { case let .success(data): - self.client?.urlProtocol(self, didLoad: data) + client?.urlProtocol(self, didLoad: data) if let url = request.url, let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) { - self.client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) + client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .allowed) } case let .failure(error): - self.client?.urlProtocol(self, didFailWithError: error) + client?.urlProtocol(self, didFailWithError: error) case let .failureStatusCode(statusCode): - if let url = self.request.url, + if let url = request.url, let response = HTTPURLResponse( url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil ) { - self.client?.urlProtocol( + client?.urlProtocol( self, cachedResponseIsValid: CachedURLResponse(response: response, data: Data()) ) } } - self.client?.urlProtocolDidFinishLoading(self) + client?.urlProtocolDidFinishLoading(self) } } diff --git a/Sources/MBNetworking/UploadFile.swift b/Sources/MBNetworking/UploadFile.swift index 3e5d5da..c1e2f45 100644 --- a/Sources/MBNetworking/UploadFile.swift +++ b/Sources/MBNetworking/UploadFile.swift @@ -22,7 +22,7 @@ public struct File { public var data: Data var fileNameWithExtension: String { - var fileName = self.fileName + var fileName = fileName if !fileExtension.isEmpty { fileName.append(".") fileName.append(fileExtension) diff --git a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift index 3627746..6fce636 100644 --- a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift +++ b/Tests/MBNetworkingTests/NetworkingLegacyTests.swift @@ -50,7 +50,7 @@ enum Download: Networkable { var request: URLRequest { switch self { case let .data(url): - return getRequest(url: url, queryItems: [:]) + getRequest(url: url, queryItems: [:]) } } } diff --git a/Tests/MBNetworkingTests/SSLPinningTests.swift b/Tests/MBNetworkingTests/SSLPinningTests.swift index b5b8200..124fd8c 100644 --- a/Tests/MBNetworkingTests/SSLPinningTests.swift +++ b/Tests/MBNetworkingTests/SSLPinningTests.swift @@ -63,7 +63,7 @@ class SSLPinningTests: XCTestCase { public var request: URLRequest { switch self { case let .sendToken(request): - return getRequest( + getRequest( url: URL(forceString: "https://api.macfit.com.tr/api/auth/sendToken"), encodable: request, headers: API.getHeaders() From 733d1eb062628ddbd1d4eac0af3d9827869789e1 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Thu, 15 Aug 2024 13:41:07 +0300 Subject: [PATCH 12/18] Removed TODO comments. Improved code quality in the urlSession didReceive function. --- .../Networkable+DataTaskAsync.swift | 18 +++--- .../Networkable+PinningDelegate.swift | 60 ++++++++++--------- .../NetworkableTasksTests.swift | 1 - 3 files changed, 41 insertions(+), 38 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index b2c2cf2..94f6bca 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -78,20 +78,22 @@ import MBErrorKit private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { let taskId = UUID().uuidString + + let task = Session.shared.session.dataTask(with: urlRequest) + Session.shared.tasksInProgress[taskId] = task + do { let (data, response) = try await Session.shared.session.data(for: urlRequest) - if let task = Session.shared.tasksInProgress[taskId] { - Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) - Self.finalizeTask(withId: taskId, task: task) - } + Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) + Self.finalizeTask(withId: taskId, task: task) printResponse(data) + return (response, data, nil) } catch { - if let task = Session.shared.tasksInProgress[taskId] { - Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) - Self.finalizeTask(withId: taskId, task: task) - } + Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) + Self.finalizeTask(withId: taskId, task: task) + return (nil, nil, error) } } diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index 6be578d..c801c5e 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -27,7 +27,7 @@ extension URLSessionPinningDelegateProtocol { } } -class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtocol { +private final class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtocol { var certificatePaths: [String] = [] func urlSession( @@ -35,48 +35,30 @@ class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtoc didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void ) { - guard let serverCertificate = URLSessionPinningHelper.getServerCertificate(forChallenge: challenge) else { - completionHandler(.cancelAuthenticationChallenge, nil) - return - } - - let disposition = URLSessionPinningHelper.handleChallengeCommon( - certificatePaths: certificatePaths, - - serverCertificate: serverCertificate + let result = URLSessionPinningHelper.processAuthenticationChallenge( + challenge: challenge, + certificatePaths: certificatePaths ) - if disposition == .useCredential { - completionHandler(disposition, URLCredential(trust: serverCertificate.trust)) - } else { - completionHandler(disposition, nil) - } + completionHandler(result.0, result.1) } } -@available(iOS 13.0, *) class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtocol { +@available(iOS 13.0, *) +private final class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningDelegateProtocol { var certificatePaths: [String] = [] func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - guard let serverCertificate = URLSessionPinningHelper.getServerCertificate(forChallenge: challenge) else { - return (.cancelAuthenticationChallenge, nil) - } - - let disposition = URLSessionPinningHelper.handleChallengeCommon( - certificatePaths: certificatePaths, - serverCertificate: serverCertificate + return URLSessionPinningHelper.processAuthenticationChallenge( + challenge: challenge, + certificatePaths: certificatePaths ) - if disposition == .useCredential { - return (disposition, URLCredential(trust: serverCertificate.trust)) - } else { - return (disposition, nil) - } } } -private class URLSessionPinningHelper { +private enum URLSessionPinningHelper { static func getServerCertificate( forChallenge challenge: URLAuthenticationChallenge ) -> (data: CFData, trust: SecTrust)? { @@ -114,4 +96,24 @@ private class URLSessionPinningHelper { return .cancelAuthenticationChallenge } } + + static func processAuthenticationChallenge( + challenge: URLAuthenticationChallenge, + certificatePaths: [String] + ) -> (URLSession.AuthChallengeDisposition, URLCredential?) { + guard let serverCertificate = getServerCertificate(forChallenge: challenge) else { + return (.cancelAuthenticationChallenge, nil) + } + + let disposition = handleChallengeCommon( + certificatePaths: certificatePaths, + serverCertificate: serverCertificate + ) + + if disposition == .useCredential { + return (disposition, URLCredential(trust: serverCertificate.trust)) + } else { + return (disposition, nil) + } + } } diff --git a/Tests/MBNetworkingTests/NetworkableTasksTests.swift b/Tests/MBNetworkingTests/NetworkableTasksTests.swift index c3e7747..a7584d7 100644 --- a/Tests/MBNetworkingTests/NetworkableTasksTests.swift +++ b/Tests/MBNetworkingTests/NetworkableTasksTests.swift @@ -36,7 +36,6 @@ class NetworkableTasksTests: XCTestCase { } func testSessionQueueHasRemovedDataTask_WhenTaskIsFinished() { - // TODO: test whether it's a bug in task queue or not? let expectation = XCTestExpectation(description: "waiting for image") makeACall(expectation) XCTAssertEqual(Session.shared.tasksInProgress.count, 1) From 346b0ee7dbe0453a0d1be931546f014635855a62 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Fri, 16 Aug 2024 16:59:50 +0300 Subject: [PATCH 13/18] Refactor delegate creation to use composer --- .../MBNetworking/Networkable+PinningDelegate.swift | 12 +++++++----- .../MBNetworking/Networkable+UntrustedDelegate.swift | 12 +++++++----- Sources/MBNetworking/Session.swift | 4 ++-- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index c801c5e..d42c51d 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -13,11 +13,13 @@ protocol URLSessionPinningDelegateProtocol: URLSessionDelegate { var certificatePaths: [String] { get set } } -func createURLSessionPinningDelegate() -> URLSessionPinningDelegateProtocol { - if #available(iOS 13.0, *) { - URLSessionPinningDelegateAsync() - } else { - URLSessionPinningDelegateLegacy() +enum URLSessionPinningComposer { + static func createDelegate() -> URLSessionPinningDelegateProtocol { + if #available(iOS 13.0, *) { + return URLSessionPinningDelegateAsync() + } else { + return URLSessionPinningDelegateLegacy() + } } } diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift index b95f335..ff0c2f7 100644 --- a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -8,11 +8,13 @@ import Foundation -func createUntrustedURLSessionDelegate() -> URLSessionDelegate { - if #available(iOS 13.0, *) { - UntrustedURLSessionDelegateAsync() - } else { - UntrustedURLSessionDelegateLegacy() +enum UntrustedURLSessionComposer { + static func createDelegate() -> URLSessionDelegate { + if #available(iOS 13.0, *) { + return UntrustedURLSessionDelegateAsync() + } else { + return UntrustedURLSessionDelegateLegacy() + } } } diff --git a/Sources/MBNetworking/Session.swift b/Sources/MBNetworking/Session.swift index 133fa4c..84c563a 100644 --- a/Sources/MBNetworking/Session.swift +++ b/Sources/MBNetworking/Session.swift @@ -56,7 +56,7 @@ final class Session { /// Configures networking to trust session authentication challenge, even if the certificate is not trusted. func setServerTrustedURLAuthenticationChallenge() { - delegate = createUntrustedURLSessionDelegate() + delegate = UntrustedURLSessionComposer.createDelegate() session = URLSession( configuration: configuration, delegate: delegate, @@ -65,7 +65,7 @@ final class Session { } required init() { - delegate = createURLSessionPinningDelegate() + delegate = URLSessionPinningComposer.createDelegate() session = URLSession( configuration: configuration, delegate: delegate, From c6e4fc549117e61585af8aafcef03a9a0d2b7980 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Fri, 16 Aug 2024 17:00:52 +0300 Subject: [PATCH 14/18] Refactoring requestData function --- .../Networkable+DataTaskAsync.swift | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 94f6bca..b107a16 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -75,33 +75,34 @@ import MBErrorKit throw error } } - + private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { let taskId = UUID().uuidString - let task = Session.shared.session.dataTask(with: urlRequest) - Session.shared.tasksInProgress[taskId] = task - + + Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) + task.resume() + Session.shared.tasksInProgress.updateValue(task, forKey: taskId) + + defer { + Session.shared.tasksInProgress.removeValue(forKey: taskId) + } + do { let (data, response) = try await Session.shared.session.data(for: urlRequest) - - Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) - Self.finalizeTask(withId: taskId, task: task) - printResponse(data) - + + if let task = Session.shared.tasksInProgress[taskId] { + Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) + Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: nil) + } + + self.printResponse(data) return (response, data, nil) } catch { - Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) - Self.finalizeTask(withId: taskId, task: task) - + if let task = Session.shared.tasksInProgress[taskId] { + Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) + } return (nil, nil, error) } } - - private static func finalizeTask(withId taskId: String, task: URLSessionDataTask) { - Session.shared.tasksInProgress.removeValue(forKey: taskId) - - Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) - Session.shared.tasksInProgress.updateValue(task, forKey: taskId) - } } From 7ee8f18698e5e541f49ccec787d60193d369e5d4 Mon Sep 17 00:00:00 2001 From: Umut Can ARDUC Date: Tue, 20 Aug 2024 17:04:01 +0300 Subject: [PATCH 15/18] Unit test fix and added todo task-async flow --- .../MBNetworking/Networkable+DataTask.swift | 4 +-- .../Networkable+DataTaskAsync.swift | 34 +++++++++---------- .../NetworkableTasksTests.swift | 8 +++-- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTask.swift b/Sources/MBNetworking/Networkable+DataTask.swift index 692fa4b..b41bf9b 100644 --- a/Sources/MBNetworking/Networkable+DataTask.swift +++ b/Sources/MBNetworking/Networkable+DataTask.swift @@ -19,12 +19,12 @@ extension Networkable { _ type: V.Type, completion: @escaping ((Result) -> Void) ) { + fetch(request, completion: completion) + // StubURLProtocol enabled and adding a small delay. if StubURLProtocol.isEnabled, ProcessInfo.isUnderTest { RunLoop.current.run(until: Date().addingTimeInterval(0.05)) } - - fetch(request, completion: completion) } private func fetch( diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index b107a16..58d9659 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -77,32 +77,32 @@ import MBErrorKit } private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { - let taskId = UUID().uuidString - let task = Session.shared.session.dataTask(with: urlRequest) - - Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) - task.resume() - Session.shared.tasksInProgress.updateValue(task, forKey: taskId) - - defer { - Session.shared.tasksInProgress.removeValue(forKey: taskId) - } + // TODO: Handle Async flow +// let taskId = UUID().uuidString +// let task = Session.shared.session.dataTask(with: urlRequest) do { let (data, response) = try await Session.shared.session.data(for: urlRequest) - if let task = Session.shared.tasksInProgress[taskId] { - Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) - Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: nil) - } + // TODO: Handle Async flow +// Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) +// Session.shared.tasksInProgress.removeValue(forKey: taskId) + self.printResponse(data) return (response, data, nil) } catch { - if let task = Session.shared.tasksInProgress[taskId] { - Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) - } +// if let task = Session.shared.tasksInProgress[taskId] { +// Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) +// } +// Session.shared.tasksInProgress.removeValue(forKey: taskId) + return (nil, nil, error) } + + // TODO: Handle Async flow +// Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) +// task.resume() +// Session.shared.tasksInProgress.updateValue(task, forKey: taskId) } } diff --git a/Tests/MBNetworkingTests/NetworkableTasksTests.swift b/Tests/MBNetworkingTests/NetworkableTasksTests.swift index a7584d7..51866d6 100644 --- a/Tests/MBNetworkingTests/NetworkableTasksTests.swift +++ b/Tests/MBNetworkingTests/NetworkableTasksTests.swift @@ -24,15 +24,19 @@ class NetworkableTasksTests: XCTestCase { } func testSessionQueueHasDataTask_WhenFetchCalled() { - makeACall() + let expectation = XCTestExpectation(description: "waiting for image") + makeACall(expectation) XCTAssertEqual(Session.shared.tasksInProgress.count, 1) + wait(for: [expectation], timeout: 1) } func testSessionQueueHasDataTaskWhenFetchCalledMultipleTimes() { + let expectation = XCTestExpectation(description: "waiting for image") for _ in 0 ... 3 { - makeACall() + makeACall(expectation) } XCTAssertEqual(Session.shared.tasksInProgress.count, 4) + wait(for: [expectation], timeout: 1) } func testSessionQueueHasRemovedDataTask_WhenTaskIsFinished() { From 6077af2fcc11762e48822c871b469bdae04c3bc4 Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Mon, 16 Sep 2024 14:12:49 +0300 Subject: [PATCH 16/18] Update Networkable+DataTaskAsync.swift --- .../Networkable+DataTaskAsync.swift | 26 +++---------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/Sources/MBNetworking/Networkable+DataTaskAsync.swift b/Sources/MBNetworking/Networkable+DataTaskAsync.swift index 58d9659..4ac99e4 100644 --- a/Sources/MBNetworking/Networkable+DataTaskAsync.swift +++ b/Sources/MBNetworking/Networkable+DataTaskAsync.swift @@ -75,34 +75,16 @@ import MBErrorKit throw error } } - + private func requestData(_ urlRequest: URLRequest) async -> (URLResponse?, Data?, Error?) { - // TODO: Handle Async flow -// let taskId = UUID().uuidString -// let task = Session.shared.session.dataTask(with: urlRequest) - + // TODO: Should async support Session.shared.taskInProgress cancellation logic? + do { let (data, response) = try await Session.shared.session.data(for: urlRequest) - - // TODO: Handle Async flow -// Session.shared.networkLogMonitoringDelegate?.logDataTask(dataTask: task, didReceive: data) -// Session.shared.tasksInProgress.removeValue(forKey: taskId) - - - self.printResponse(data) + printResponse(data) return (response, data, nil) } catch { -// if let task = Session.shared.tasksInProgress[taskId] { -// Session.shared.networkLogMonitoringDelegate?.logTask(task: task, didCompleteWithError: error) -// } -// Session.shared.tasksInProgress.removeValue(forKey: taskId) - return (nil, nil, error) } - - // TODO: Handle Async flow -// Session.shared.networkLogMonitoringDelegate?.logTaskCreated(task: task) -// task.resume() -// Session.shared.tasksInProgress.updateValue(task, forKey: taskId) } } From 083d6331002c8f82cc2d385bc250eb425c7fe2df Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Mon, 16 Sep 2024 14:17:06 +0300 Subject: [PATCH 17/18] Swiftformatted. --- Sources/MBNetworking/Networkable+PinningDelegate.swift | 6 +++--- Sources/MBNetworking/Networkable+UntrustedDelegate.swift | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index d42c51d..ec9cce0 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -16,9 +16,9 @@ protocol URLSessionPinningDelegateProtocol: URLSessionDelegate { enum URLSessionPinningComposer { static func createDelegate() -> URLSessionPinningDelegateProtocol { if #available(iOS 13.0, *) { - return URLSessionPinningDelegateAsync() + URLSessionPinningDelegateAsync() } else { - return URLSessionPinningDelegateLegacy() + URLSessionPinningDelegateLegacy() } } } @@ -53,7 +53,7 @@ private final class URLSessionPinningDelegateAsync: NSObject, URLSessionPinningD _ session: URLSession, didReceive challenge: URLAuthenticationChallenge ) async -> (URLSession.AuthChallengeDisposition, URLCredential?) { - return URLSessionPinningHelper.processAuthenticationChallenge( + URLSessionPinningHelper.processAuthenticationChallenge( challenge: challenge, certificatePaths: certificatePaths ) diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift index ff0c2f7..b87c6d7 100644 --- a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -11,9 +11,9 @@ import Foundation enum UntrustedURLSessionComposer { static func createDelegate() -> URLSessionDelegate { if #available(iOS 13.0, *) { - return UntrustedURLSessionDelegateAsync() + UntrustedURLSessionDelegateAsync() } else { - return UntrustedURLSessionDelegateLegacy() + UntrustedURLSessionDelegateLegacy() } } } From 4f31f36baae66aa668370aa753d24fc6483f63c0 Mon Sep 17 00:00:00 2001 From: Rashid Ramazanov Date: Mon, 16 Sep 2024 14:18:45 +0300 Subject: [PATCH 18/18] Remove Legacy keyword from clousere based netwroking files. --- Sources/MBNetworking/Networkable+PinningDelegate.swift | 4 ++-- Sources/MBNetworking/Networkable+UntrustedDelegate.swift | 4 ++-- ...nceLegacyTests.swift => NetworkablePerformanceTests.swift} | 4 ++-- .../{NetworkingLegacyTests.swift => NetworkingTests.swift} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename Tests/MBNetworkingTests/{NetworkablePerformanceLegacyTests.swift => NetworkablePerformanceTests.swift} (94%) rename Tests/MBNetworkingTests/{NetworkingLegacyTests.swift => NetworkingTests.swift} (95%) diff --git a/Sources/MBNetworking/Networkable+PinningDelegate.swift b/Sources/MBNetworking/Networkable+PinningDelegate.swift index ec9cce0..dc3a546 100644 --- a/Sources/MBNetworking/Networkable+PinningDelegate.swift +++ b/Sources/MBNetworking/Networkable+PinningDelegate.swift @@ -18,7 +18,7 @@ enum URLSessionPinningComposer { if #available(iOS 13.0, *) { URLSessionPinningDelegateAsync() } else { - URLSessionPinningDelegateLegacy() + URLSessionPinningDelegate() } } } @@ -29,7 +29,7 @@ extension URLSessionPinningDelegateProtocol { } } -private final class URLSessionPinningDelegateLegacy: NSObject, URLSessionPinningDelegateProtocol { +private final class URLSessionPinningDelegate: NSObject, URLSessionPinningDelegateProtocol { var certificatePaths: [String] = [] func urlSession( diff --git a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift index b87c6d7..61078ac 100644 --- a/Sources/MBNetworking/Networkable+UntrustedDelegate.swift +++ b/Sources/MBNetworking/Networkable+UntrustedDelegate.swift @@ -13,7 +13,7 @@ enum UntrustedURLSessionComposer { if #available(iOS 13.0, *) { UntrustedURLSessionDelegateAsync() } else { - UntrustedURLSessionDelegateLegacy() + UntrustedURLSessionDelegate() } } } @@ -27,7 +27,7 @@ enum UntrustedURLSessionComposer { } } -class UntrustedURLSessionDelegateLegacy: NSObject, URLSessionDelegate { +class UntrustedURLSessionDelegate: NSObject, URLSessionDelegate { func urlSession( _ session: URLSession, didReceive challenge: URLAuthenticationChallenge, diff --git a/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift similarity index 94% rename from Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift rename to Tests/MBNetworkingTests/NetworkablePerformanceTests.swift index d8e2e86..90b27d0 100644 --- a/Tests/MBNetworkingTests/NetworkablePerformanceLegacyTests.swift +++ b/Tests/MBNetworkingTests/NetworkablePerformanceTests.swift @@ -1,5 +1,5 @@ // -// NetworkablePerformanceLegacyTests.swift +// NetworkablePerformanceTests.swift // MBNetworkingTests // // Created by Rashid Ramazanov on 2/23/22. @@ -13,7 +13,7 @@ @testable import MBNetworking @testable import MobKitCore - class NetworkablePerformanceLegacyTests: XCTestCase { + class NetworkablePerformanceTests: XCTestCase { var imageView: UIImageView = .init() override func setUp() { diff --git a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift b/Tests/MBNetworkingTests/NetworkingTests.swift similarity index 95% rename from Tests/MBNetworkingTests/NetworkingLegacyTests.swift rename to Tests/MBNetworkingTests/NetworkingTests.swift index 6fce636..1ff5e37 100644 --- a/Tests/MBNetworkingTests/NetworkingLegacyTests.swift +++ b/Tests/MBNetworkingTests/NetworkingTests.swift @@ -1,5 +1,5 @@ // -// NetworkingLegacyTests.swift +// NetworkingTests.swift // NetworkingTests // // Created by Rasid Ramazanov on 17.02.2020. @@ -11,7 +11,7 @@ import XCTest @testable import MobKitCore #if canImport(UIKit) - class NetworkingLegacyTests: XCTestCase { + class NetworkingTests: XCTestCase { override func setUp() { MobKit.isDeveloperModeOn = true StubURLProtocol.delay = .zero