From 8c591f0a1fb5d71d1746dbf88472e371191ad12f Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Sun, 29 May 2022 14:04:41 +0200 Subject: [PATCH] Remove usage of Swift concurrency (#76) * Remove usage of Swift concurrency * Update integration tests workflow --- .github/workflows/integration-tests.yml | 8 +--- .gitignore | 1 + Sources/CLI/Commands/Auth.swift | 18 ++++----- Sources/CLI/Commands/Download.swift | 30 +++++++------- Sources/CLI/Commands/IPATool.swift | 2 +- Sources/CLI/Commands/Purchase.swift | 16 ++++---- Sources/CLI/Commands/Search.swift | 10 ++--- Sources/Networking/HTTPClient.swift | 24 +++++++++--- Sources/Networking/HTTPDownloadClient.swift | 39 ++++++++++++------- Sources/Networking/URLSession.swift | 30 +++++++------- Sources/StoreAPI/Store/StoreClient.swift | 24 ++++++------ Sources/StoreAPI/iTunes/iTunesClient.swift | 12 +++--- Tests/NetworkingTests/HTTPClientTests.swift | 30 +++++++------- .../HTTPDownloadClientTests.swift | 8 ++-- Tests/NetworkingTests/HTTPResponseTests.swift | 8 ++-- .../URLSessionDataTaskMock.swift | 21 ++++++++++ Tests/NetworkingTests/URLSessionMock.swift | 26 ++++++++++--- 17 files changed, 181 insertions(+), 126 deletions(-) create mode 100644 Tests/NetworkingTests/URLSessionDataTaskMock.swift diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0ef03caa..004c8b67 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -39,11 +39,5 @@ jobs: path: ipatool-current - name: Update permissions run: chmod +x ./ipatool-current/ipatool - - name: Install Swift toolchain - run: | - wget https://download.swift.org/swift-5.5.3-release/xcode/swift-5.5.3-RELEASE/swift-5.5.3-RELEASE-osx.pkg - sudo installer -pkg swift-5.5.3-RELEASE-osx.pkg -target / - name: Run tests - run: | - export DYLD_LIBRARY_PATH=/Library/Developer/Toolchains/swift-5.5.3-RELEASE.xctoolchain/usr/lib/swift/macosx - ./ipatool-current/ipatool ${{ matrix.command }} --help + run: ./ipatool-current/ipatool ${{ matrix.command }} --help diff --git a/.gitignore b/.gitignore index 3499dce7..814c3768 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ .DS_Store .AppleDouble .LSOverride +.vscode/ # Icon must end with two \r Icon diff --git a/Sources/CLI/Commands/Auth.swift b/Sources/CLI/Commands/Auth.swift index 0d2155b8..bf7c64f1 100644 --- a/Sources/CLI/Commands/Auth.swift +++ b/Sources/CLI/Commands/Auth.swift @@ -11,7 +11,7 @@ import Networking import StoreAPI import Persistence -struct Auth: AsyncParsableCommand { +struct Auth: ParsableCommand { static var configuration: CommandConfiguration { return .init( commandName: "auth", @@ -23,7 +23,7 @@ struct Auth: AsyncParsableCommand { } extension Auth { - struct Login: AsyncParsableCommand { + struct Login: ParsableCommand { static var configuration: CommandConfiguration { return .init(abstract: "Login to the App Store.") } @@ -43,7 +43,7 @@ extension Auth { lazy var logger = ConsoleLogger(level: logLevel) } - struct Revoke: AsyncParsableCommand { + struct Revoke: ParsableCommand { static var configuration: CommandConfiguration { return .init(abstract: "Revoke your App Store credentials.") } @@ -95,7 +95,7 @@ extension Auth.Login { } } - private mutating func authenticate(email: String, password: String) async -> Account { + private mutating func authenticate(email: String, password: String) -> Account { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -104,7 +104,7 @@ extension Auth.Login { do { logger.log("Authenticating with the App Store...", level: .info) - let account = try await storeClient.authenticate(email: email, password: password, code: nil) + let account = try storeClient.authenticate(email: email, password: password, code: nil) return Account( name: "\(account.firstName) \(account.lastName)", email: email, @@ -115,7 +115,7 @@ extension Auth.Login { switch error { case StoreResponse.Error.codeRequired: do { - let account = try await storeClient.authenticate(email: email, password: password, code: authCode()) + let account = try storeClient.authenticate(email: email, password: password, code: authCode()) return Account( name: "\(account.firstName) \(account.lastName)", email: email, @@ -161,7 +161,7 @@ extension Auth.Login { } } - mutating func run() async throws { + mutating func run() throws { // Get Apple ID email let email: String = email() @@ -169,7 +169,7 @@ extension Auth.Login { let password: String = password() // Authenticate with the App Store - let account: Account = await authenticate(email: email, password: password) + let account: Account = authenticate(email: email, password: password) // Store data in keychain do { @@ -187,7 +187,7 @@ extension Auth.Login { } extension Auth.Revoke { - mutating func run() async throws { + mutating func run() throws { let keychainStore = KeychainStore(service: "ipatool.service") guard let account: Account = try keychainStore.value(forKey: "account") else { diff --git a/Sources/CLI/Commands/Download.swift b/Sources/CLI/Commands/Download.swift index 722b7bbe..dd157d8e 100644 --- a/Sources/CLI/Commands/Download.swift +++ b/Sources/CLI/Commands/Download.swift @@ -11,7 +11,7 @@ import Networking import StoreAPI import Persistence -struct Download: AsyncParsableCommand { +struct Download: ParsableCommand { static var configuration: CommandConfiguration { return .init(abstract: "Download (encrypted) iOS app packages from the App Store.") } @@ -41,7 +41,7 @@ struct Download: AsyncParsableCommand { } extension Download { - private mutating func app(with bundleIdentifier: String, countryCode: String) async -> iTunesResponse.Result { + private mutating func app(with bundleIdentifier: String, countryCode: String) -> iTunesResponse.Result { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -50,7 +50,7 @@ extension Download { do { logger.log("Querying the iTunes Store for '\(bundleIdentifier)' in country '\(countryCode)'...", level: .info) - return try await itunesClient.lookup( + return try itunesClient.lookup( bundleIdentifier: bundleIdentifier, countryCode: countryCode, deviceFamily: deviceFamily @@ -70,7 +70,7 @@ extension Download { } - private mutating func purchase(app: iTunesResponse.Result, account: Account) async { + private mutating func purchase(app: iTunesResponse.Result, account: Account) { guard app.price == 0 else { logger.log("It is only possible to obtain a license for free apps. Purchase the app manually and run the \"download\" command again.", level: .error) _exit(1) @@ -84,7 +84,7 @@ extension Download { do { logger.log("Obtaining a license for '\(app.identifier)' from the App Store...", level: .info) - try await storeClient.purchase( + try storeClient.purchase( identifier: "\(app.identifier)", directoryServicesIdentifier: account.directoryServicesIdentifier, passwordToken: account.passwordToken, @@ -116,7 +116,7 @@ extension Download { from app: iTunesResponse.Result, account: Account, purchaseAttempted: Bool = false - ) async -> StoreResponse.Item { + ) -> StoreResponse.Item { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -125,7 +125,7 @@ extension Download { do { logger.log("Requesting a signed copy of '\(app.identifier)' from the App Store...", level: .info) - return try await storeClient.item( + return try storeClient.item( identifier: "\(app.identifier)", directoryServicesIdentifier: account.directoryServicesIdentifier ) @@ -141,10 +141,10 @@ extension Download { if !purchaseAttempted, purchase { logger.log("License is missing.", level: .info) - await purchase(app: app, account: account) + purchase(app: app, account: account) logger.log("Obtained a license for '\(app.identifier)'.", level: .debug) - return await item(from: app, account: account, purchaseAttempted: true) + return item(from: app, account: account, purchaseAttempted: true) } else { logger.log("Your Apple ID does not have a license for this app. Use the \"purchase\" command or the \"--purchase\" to obtain a license.", level: .error) } @@ -160,13 +160,13 @@ extension Download { } } - private mutating func download(item: StoreResponse.Item, to targetURL: URL) async { + private mutating func download(item: StoreResponse.Item, to targetURL: URL) { logger.log("Creating download client...", level: .debug) let downloadClient = HTTPDownloadClient() do { logger.log("Downloading app package...", level: .info) - try await downloadClient.download(from: item.url, to: targetURL) { [logger] progress in + try downloadClient.download(from: item.url, to: targetURL) { [logger] progress in logger.log("Downloading app package... [\(Int((progress * 100).rounded()))%]", prefix: "\u{1B}[1A\u{1B}[K", level: .info) @@ -216,7 +216,7 @@ extension Download { } } - mutating func run() async throws { + mutating func run() throws { // Authenticate with the App Store let keychainStore = KeychainStore(service: "ipatool.service") @@ -227,11 +227,11 @@ extension Download { logger.log("Authenticated as '\(account.name)'.", level: .info) // Query for app - let app: iTunesResponse.Result = await app(with: bundleIdentifier, countryCode: countryCode) + let app: iTunesResponse.Result = app(with: bundleIdentifier, countryCode: countryCode) logger.log("Found app: \(app.name) (\(app.version)).", level: .debug) // Query for store item - let item: StoreResponse.Item = await item(from: app, account: account) + let item: StoreResponse.Item = item(from: app, account: account) logger.log("Received a response of the signed copy: \(item.md5).", level: .debug) // Generate file name @@ -239,7 +239,7 @@ extension Download { logger.log("Output path: \(path).", level: .debug) // Download app package - await download(item: item, to: URL(fileURLWithPath: path)) + download(item: item, to: URL(fileURLWithPath: path)) logger.log("Saved app package to \(URL(fileURLWithPath: path).lastPathComponent).", level: .info) // Apply patches diff --git a/Sources/CLI/Commands/IPATool.swift b/Sources/CLI/Commands/IPATool.swift index 2b124845..d3dfd424 100644 --- a/Sources/CLI/Commands/IPATool.swift +++ b/Sources/CLI/Commands/IPATool.swift @@ -8,7 +8,7 @@ import ArgumentParser @main -struct IPATool: AsyncParsableCommand { +struct IPATool: ParsableCommand { static var configuration: CommandConfiguration { return CommandConfiguration( commandName: "ipatool", diff --git a/Sources/CLI/Commands/Purchase.swift b/Sources/CLI/Commands/Purchase.swift index bdbe7b7e..fea45f3c 100644 --- a/Sources/CLI/Commands/Purchase.swift +++ b/Sources/CLI/Commands/Purchase.swift @@ -11,7 +11,7 @@ import Networking import StoreAPI import Persistence -struct Purchase: AsyncParsableCommand { +struct Purchase: ParsableCommand { static var configuration: CommandConfiguration { return .init(abstract: "Obtain a license for the app from the App Store.") } @@ -35,7 +35,7 @@ struct Purchase: AsyncParsableCommand { } extension Purchase { - private mutating func app(with bundleIdentifier: String) async -> iTunesResponse.Result { + private mutating func app(with bundleIdentifier: String) -> iTunesResponse.Result { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -44,7 +44,7 @@ extension Purchase { do { logger.log("Querying the iTunes Store for '\(bundleIdentifier)' in country '\(countryCode)'...", level: .info) - let app = try await itunesClient.lookup( + let app = try itunesClient.lookup( bundleIdentifier: bundleIdentifier, countryCode: countryCode, deviceFamily: deviceFamily @@ -70,7 +70,7 @@ extension Purchase { } } - private mutating func purchase(app: iTunesResponse.Result, account: Account) async { + private mutating func purchase(app: iTunesResponse.Result, account: Account) { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -79,7 +79,7 @@ extension Purchase { do { logger.log("Obtaining a license for '\(app.identifier)' from the App Store...", level: .info) - try await storeClient.purchase( + try storeClient.purchase( identifier: "\(app.identifier)", directoryServicesIdentifier: account.directoryServicesIdentifier, passwordToken: account.passwordToken, @@ -107,7 +107,7 @@ extension Purchase { } } - mutating func run() async throws { + mutating func run() throws { // Authenticate with the App Store let keychainStore = KeychainStore(service: "ipatool.service") @@ -118,11 +118,11 @@ extension Purchase { logger.log("Authenticated as '\(account.name)'.", level: .info) // Query for app - let app: iTunesResponse.Result = await app(with: bundleIdentifier) + let app: iTunesResponse.Result = app(with: bundleIdentifier) logger.log("Found app: \(app.name) (\(app.version)).", level: .debug) // Obtain a license - await purchase(app: app, account: account) + purchase(app: app, account: account) logger.log("Obtained a license for '\(app.identifier)'.", level: .debug) logger.log("Done.", level: .info) } diff --git a/Sources/CLI/Commands/Search.swift b/Sources/CLI/Commands/Search.swift index 60cfa561..021db09f 100644 --- a/Sources/CLI/Commands/Search.swift +++ b/Sources/CLI/Commands/Search.swift @@ -10,7 +10,7 @@ import Foundation import Networking import StoreAPI -struct Search: AsyncParsableCommand { +struct Search: ParsableCommand { static var configuration: CommandConfiguration { return .init(abstract: "Search for iOS apps available on the App Store.") } @@ -37,7 +37,7 @@ struct Search: AsyncParsableCommand { } extension Search { - mutating func results(with term: String) async -> [iTunesResponse.Result] { + mutating func results(with term: String) -> [iTunesResponse.Result] { logger.log("Creating HTTP client...", level: .debug) let httpClient = HTTPClient(session: URLSession.shared) @@ -47,7 +47,7 @@ extension Search { logger.log("Searching for '\(term)' using the '\(countryCode)' store front...", level: .info) do { - let results = try await itunesClient.search( + let results = try itunesClient.search( term: term, limit: limit, countryCode: countryCode, @@ -67,9 +67,9 @@ extension Search { } } - mutating func run() async throws { + mutating func run() throws { // Search the iTunes store - let results = await results(with: term) + let results = results(with: term) // Compile output let output = results diff --git a/Sources/Networking/HTTPClient.swift b/Sources/Networking/HTTPClient.swift index a4273a69..9dfddde1 100644 --- a/Sources/Networking/HTTPClient.swift +++ b/Sources/Networking/HTTPClient.swift @@ -8,7 +8,7 @@ import Foundation public protocol HTTPClientInterface { - func send(_ request: HTTPRequest) async throws -> HTTPResponse + func send(_ request: HTTPRequest) throws -> HTTPResponse } public final class HTTPClient: HTTPClientInterface { @@ -18,15 +18,27 @@ public final class HTTPClient: HTTPClientInterface { self.session = session } - public func send(_ request: HTTPRequest) async throws -> HTTPResponse { + public func send(_ request: HTTPRequest) throws -> HTTPResponse { let request = try makeURLRequest(from: request) - let (data, response) = try await session.data(for: request) + let semaphore = DispatchSemaphore(value: 0) + var result: (data: Data?, response: URLResponse?, error: Swift.Error?) - guard let response = response as? HTTPURLResponse else { - throw Error.invalidResponse(response) + session.dataTask(with: request) { (data, response, error) in + result = (data, response, error) + semaphore.signal() + }.resume() + + semaphore.wait() + + if let error = result.error { + throw error + } + + guard let response = result.response as? HTTPURLResponse else { + throw Error.invalidResponse(result.response) } - return HTTPResponse(statusCode: response.statusCode, data: data) + return HTTPResponse(statusCode: response.statusCode, data: result.data) } private func makeURLRequest(from request: HTTPRequest) throws -> URLRequest { diff --git a/Sources/Networking/HTTPDownloadClient.swift b/Sources/Networking/HTTPDownloadClient.swift index a85d7964..190fa318 100644 --- a/Sources/Networking/HTTPDownloadClient.swift +++ b/Sources/Networking/HTTPDownloadClient.swift @@ -8,32 +8,40 @@ import Foundation public protocol HTTPDownloadClientInterface { - func download(from source: URL, to target: URL, progress: @escaping (Float) -> Void) async throws + func download(from source: URL, to target: URL, progress: @escaping (Float) -> Void) throws } public final class HTTPDownloadClient: NSObject, HTTPDownloadClientInterface { private var session: URLSession! private var progressHandler: ((Float) -> Void)? - private var continuation: CheckedContinuation? + private var semaphore: DispatchSemaphore? private var targetURL: URL? + private var result: Result? public override init() { super.init() self.session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) } - public func download(from source: URL, to target: URL, progress: @escaping (Float) -> Void) async throws { + public func download(from source: URL, to target: URL, progress: @escaping (Float) -> Void) throws { assert(progressHandler == nil) - assert(continuation == nil) + assert(semaphore == nil) assert(targetURL == nil) progressHandler = progress targetURL = target - try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in - self.continuation = continuation - let task = session.downloadTask(with: source) - task.resume() + semaphore = DispatchSemaphore(value: 0) + session.downloadTask(with: source).resume() + semaphore?.wait() + + switch result { + case let .failure(error): + throw error + case .success: + break + case .none: + preconditionFailure() } } } @@ -45,12 +53,13 @@ extension HTTPDownloadClient: URLSessionDownloadDelegate { } defer { + semaphore?.signal() progressHandler = nil - continuation = nil + semaphore = nil targetURL = nil } - continuation?.resume(throwing: error) + result = .failure(error) } public func urlSession( @@ -69,20 +78,22 @@ extension HTTPDownloadClient: URLSessionDownloadDelegate { didFinishDownloadingTo location: URL ) { defer { + semaphore?.signal() progressHandler = nil - continuation = nil + semaphore = nil targetURL = nil } guard let target = targetURL else { - return continuation?.resume(throwing: Error.invalidTarget) ?? () + result = .failure(Error.invalidTarget) + return } do { try FileManager.default.moveItem(at: location, to: target) - continuation?.resume() + result = .success(()) } catch { - continuation?.resume(throwing: error) + result = .failure(error) } } } diff --git a/Sources/Networking/URLSession.swift b/Sources/Networking/URLSession.swift index 05aca2fb..3781f21c 100644 --- a/Sources/Networking/URLSession.swift +++ b/Sources/Networking/URLSession.swift @@ -7,23 +7,25 @@ import Foundation -public protocol URLSessionInterface { - func data(for request: URLRequest) async throws -> (Data, URLResponse) +public protocol URLSessionDataTaskInterface { + func resume() } -extension URLSession: URLSessionInterface { - public func data(for request: URLRequest) async throws -> (Data, URLResponse) { - try await withCheckedThrowingContinuation { continuation in - let task = dataTask(with: request) { data, response, error in - guard let data = data, let response = response else { - let error = error ?? URLError(.badServerResponse) - return continuation.resume(throwing: error) - } +public protocol URLSessionInterface { + func dataTask( + with request: URLRequest, + completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void + ) -> URLSessionDataTaskInterface +} - continuation.resume(returning: (data, response)) - } +extension URLSessionDataTask: URLSessionDataTaskInterface {} - task.resume() - } +extension URLSession: URLSessionInterface { + public func dataTask( + with request: URLRequest, + completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void + ) -> URLSessionDataTaskInterface { + let task: URLSessionDataTask = dataTask(with: request, completionHandler: completionHandler) + return task } } diff --git a/Sources/StoreAPI/Store/StoreClient.swift b/Sources/StoreAPI/Store/StoreClient.swift index 36e40b93..38b7c01b 100644 --- a/Sources/StoreAPI/Store/StoreClient.swift +++ b/Sources/StoreAPI/Store/StoreClient.swift @@ -9,14 +9,14 @@ import Foundation import Networking public protocol StoreClientInterface { - func authenticate(email: String, password: String, code: String?) async throws -> StoreResponse.Account - func item(identifier: String, directoryServicesIdentifier: String) async throws -> StoreResponse.Item + func authenticate(email: String, password: String, code: String?) throws -> StoreResponse.Account + func item(identifier: String, directoryServicesIdentifier: String) throws -> StoreResponse.Item func purchase( identifier: String, directoryServicesIdentifier: String, passwordToken: String, countryCode: String - ) async throws + ) throws } public final class StoreClient: StoreClientInterface { @@ -26,16 +26,16 @@ public final class StoreClient: StoreClientInterface { self.httpClient = httpClient } - public func authenticate(email: String, password: String, code: String?) async throws -> StoreResponse.Account { - try await authenticate(email: email, password: password, code: code, isFirstAttempt: true) + public func authenticate(email: String, password: String, code: String?) throws -> StoreResponse.Account { + try authenticate(email: email, password: password, code: code, isFirstAttempt: true) } - public func item(identifier: String, directoryServicesIdentifier: String) async throws -> StoreResponse.Item { + public func item(identifier: String, directoryServicesIdentifier: String) throws -> StoreResponse.Item { let request = StoreRequest.download( appIdentifier: identifier, directoryServicesIdentifier: directoryServicesIdentifier ) - let response = try await httpClient.send(request) + let response = try httpClient.send(request) let storeResponse = try response.decode(StoreResponse.self, as: .xml) switch storeResponse { @@ -53,7 +53,7 @@ public final class StoreClient: StoreClientInterface { directoryServicesIdentifier: String, passwordToken: String, countryCode: String - ) async throws { + ) throws { let request = StoreRequest.purchase( appIdentifier: identifier, directoryServicesIdentifier: directoryServicesIdentifier, @@ -61,7 +61,7 @@ public final class StoreClient: StoreClientInterface { countryCode: countryCode ) - let response = try await httpClient.send(request) + let response = try httpClient.send(request) // Returns status code 500 if the Apple ID already contains a license switch response.statusCode { @@ -86,9 +86,9 @@ public final class StoreClient: StoreClientInterface { private func authenticate(email: String, password: String, code: String?, - isFirstAttempt: Bool) async throws -> StoreResponse.Account { + isFirstAttempt: Bool) throws -> StoreResponse.Account { let request = StoreRequest.authenticate(email: email, password: password, code: code) - let response = try await httpClient.send(request) + let response = try httpClient.send(request) let decoded = try response.decode(StoreResponse.self, as: .xml) switch decoded { @@ -98,7 +98,7 @@ public final class StoreClient: StoreClientInterface { switch error { case StoreResponse.Error.invalidCredentials: if isFirstAttempt { - return try await authenticate(email: email, password: password, code: code, isFirstAttempt: false) + return try authenticate(email: email, password: password, code: code, isFirstAttempt: false) } throw error diff --git a/Sources/StoreAPI/iTunes/iTunesClient.swift b/Sources/StoreAPI/iTunes/iTunesClient.swift index 11f46524..49854cb2 100644 --- a/Sources/StoreAPI/iTunes/iTunesClient.swift +++ b/Sources/StoreAPI/iTunes/iTunesClient.swift @@ -13,14 +13,14 @@ public protocol iTunesClientInterface { bundleIdentifier: String, countryCode: String, deviceFamily: DeviceFamily - ) async throws -> iTunesResponse.Result + ) throws -> iTunesResponse.Result func search( term: String, limit: Int, countryCode: String, deviceFamily: DeviceFamily - ) async throws -> [iTunesResponse.Result] + ) throws -> [iTunesResponse.Result] } public final class iTunesClient: iTunesClientInterface { @@ -34,13 +34,13 @@ public final class iTunesClient: iTunesClientInterface { bundleIdentifier: String, countryCode: String, deviceFamily: DeviceFamily - ) async throws -> iTunesResponse.Result { + ) throws -> iTunesResponse.Result { let request = iTunesRequest.lookup( bundleIdentifier: bundleIdentifier, countryCode: countryCode, deviceFamily: deviceFamily ) - let response = try await httpClient.send(request) + let response = try httpClient.send(request) let decoded = try response.decode(iTunesResponse.self, as: .json) guard let result = decoded.results.first else { throw Error.appNotFound } return result @@ -51,14 +51,14 @@ public final class iTunesClient: iTunesClientInterface { limit: Int, countryCode: String, deviceFamily: DeviceFamily - ) async throws -> [iTunesResponse.Result] { + ) throws -> [iTunesResponse.Result] { let request = iTunesRequest.search( term: term, limit: limit, countryCode: countryCode, deviceFamily: deviceFamily ) - let response = try await httpClient.send(request) + let response = try httpClient.send(request) let decoded = try response.decode(iTunesResponse.self, as: .json) return decoded.results } diff --git a/Tests/NetworkingTests/HTTPClientTests.swift b/Tests/NetworkingTests/HTTPClientTests.swift index f3c795ae..21ceb581 100644 --- a/Tests/NetworkingTests/HTTPClientTests.swift +++ b/Tests/NetworkingTests/HTTPClientTests.swift @@ -17,8 +17,8 @@ final class HTTPClientTests: XCTestCase { sut = HTTPClient(session: session) } - func test_GET_success_returnsValidResposne() async throws { - session.onData = { request in + func test_GET_success_returnsValidResposne() throws { + session.onDataTask = { request in let url = try XCTUnwrap(request.url?.absoluteString) XCTAssertTrue(url.hasPrefix("https://api.example.com")) XCTAssertEqual(request.httpMethod, "GET") @@ -33,13 +33,13 @@ final class HTTPClientTests: XCTestCase { return (data, response) } - let response = try await sut.send(TestRequest.get(nil)) + let response = try sut.send(TestRequest.get(nil)) let data = try XCTUnwrap(response.data) XCTAssertEqual(String(data: data, encoding: .utf8), "foo") } - func test_GET_failure_returnsInvalidResposne() async throws { - session.onData = { request in + func test_GET_failure_returnsInvalidResposne() throws { + session.onDataTask = { request in let url = try XCTUnwrap(request.url?.absoluteString) XCTAssertTrue(url.hasPrefix("https://api.example.com")) XCTAssertEqual(request.httpMethod, "GET") @@ -56,15 +56,15 @@ final class HTTPClientTests: XCTestCase { } do { - _ = try await sut.send(TestRequest.get(nil)) + _ = try sut.send(TestRequest.get(nil)) XCTFail() } catch { XCTAssertNotNil(error) } } - func test_POST_xmlEncoding_returnsValidResponse() async throws { - session.onData = { request in + func test_POST_xmlEncoding_returnsValidResponse() throws { + session.onDataTask = { request in let url = try XCTUnwrap(request.url?.absoluteString) XCTAssertTrue(url.hasPrefix("https://api.example.com")) XCTAssertEqual(request.httpMethod, "POST") @@ -87,11 +87,11 @@ final class HTTPClientTests: XCTestCase { return (data, response) } - _ = try await sut.send(TestRequest.post(.xml(["foo": "bar"]))) + _ = try sut.send(TestRequest.post(.xml(["foo": "bar"]))) } - func test_GET_urlEncoding_returnsValidResponse() async throws { - session.onData = { request in + func test_GET_urlEncoding_returnsValidResponse() throws { + session.onDataTask = { request in let url = try XCTUnwrap(request.url?.absoluteString) XCTAssertTrue(url.hasPrefix("https://api.example.com")) XCTAssertTrue(url.hasSuffix("?foo=bar")) @@ -106,11 +106,11 @@ final class HTTPClientTests: XCTestCase { return (Data(), response) } - _ = try await sut.send(TestRequest.get(.urlEncoding(["foo": "bar"]))) + _ = try sut.send(TestRequest.get(.urlEncoding(["foo": "bar"]))) } - func test_POST_urlEncoding_returnsValidResponse() async throws { - session.onData = { request in + func test_POST_urlEncoding_returnsValidResponse() throws { + session.onDataTask = { request in let url = try XCTUnwrap(request.url?.absoluteString) let data = try XCTUnwrap(request.httpBody) let decoded = String(data: data, encoding: .utf8) @@ -130,7 +130,7 @@ final class HTTPClientTests: XCTestCase { return (Data(), response) } - _ = try await sut.send(TestRequest.post(.urlEncoding(["foo": "bar"]))) + _ = try sut.send(TestRequest.post(.urlEncoding(["foo": "bar"]))) } } diff --git a/Tests/NetworkingTests/HTTPDownloadClientTests.swift b/Tests/NetworkingTests/HTTPDownloadClientTests.swift index 7aa61da9..14858955 100644 --- a/Tests/NetworkingTests/HTTPDownloadClientTests.swift +++ b/Tests/NetworkingTests/HTTPDownloadClientTests.swift @@ -15,12 +15,12 @@ final class HTTPDownloadClientTests: XCTestCase { sut = HTTPDownloadClient() } - func test_download_success_returnsValidResponse() async throws { + func test_download_success_returnsValidResponse() throws { let source = try XCTUnwrap(URL(string: "https://proof.ovh.net/files/1Mb.dat")) let target = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) var lastValue: Float = 0 - try await sut.download(from: source, to: target) { value in + try sut.download(from: source, to: target) { value in XCTAssertGreaterThanOrEqual(value, lastValue) lastValue = value } @@ -28,12 +28,12 @@ final class HTTPDownloadClientTests: XCTestCase { XCTAssertTrue(FileManager.default.fileExists(atPath: target.path)) } - func test_download_failure_throwsError() async throws { + func test_download_failure_throwsError() throws { let source = try XCTUnwrap(URL(string: "https://\(UUID().uuidString).test")) let target = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(UUID().uuidString) do { - try await sut.download(from: source, to: target) { _ in } + try sut.download(from: source, to: target) { _ in } } catch { XCTAssertNotNil(error) } diff --git a/Tests/NetworkingTests/HTTPResponseTests.swift b/Tests/NetworkingTests/HTTPResponseTests.swift index 13962f52..946e4650 100644 --- a/Tests/NetworkingTests/HTTPResponseTests.swift +++ b/Tests/NetworkingTests/HTTPResponseTests.swift @@ -9,7 +9,7 @@ import XCTest @testable import Networking final class HTTPResponseTests: XCTestCase { - func test_decode_jsonData_returnsObject() async throws { + func test_decode_jsonData_returnsObject() throws { let data = try JSONEncoder().encode(["foo": "bar"]) let response = HTTPResponse(statusCode: 200, data: data) @@ -17,7 +17,7 @@ final class HTTPResponseTests: XCTestCase { XCTAssertEqual(object["foo"], "bar") } - func test_decode_xmlData_returnsObject() async throws { + func test_decode_xmlData_returnsObject() throws { let data = try PropertyListEncoder().encode(["foo": "bar"]) let response = HTTPResponse(statusCode: 200, data: data) @@ -25,12 +25,12 @@ final class HTTPResponseTests: XCTestCase { XCTAssertEqual(object["foo"], "bar") } - func test_decode_noData_returnsObject() async throws { + func test_decode_noData_returnsObject() throws { let response = HTTPResponse(statusCode: 200, data: nil) XCTAssertThrowsError(try response.decode([String: String].self, as: .xml)) } - func test_decode_invalidData_returnsObject() async throws { + func test_decode_invalidData_returnsObject() throws { let data = try PropertyListEncoder().encode(["foo": "bar"]) let response = HTTPResponse(statusCode: 200, data: data) diff --git a/Tests/NetworkingTests/URLSessionDataTaskMock.swift b/Tests/NetworkingTests/URLSessionDataTaskMock.swift new file mode 100644 index 00000000..d611892b --- /dev/null +++ b/Tests/NetworkingTests/URLSessionDataTaskMock.swift @@ -0,0 +1,21 @@ +// +// URLSessionDataTaskMock.swift +// NetworkingTests +// +// Created by Majd Alfhaily on 29.05.22. +// + +import Foundation +import Networking + +final class URLSessionDataTaskMock: URLSessionDataTaskInterface { + var onResume: (() -> Void)? + + func resume() { + guard let onResume = onResume else { + fatalError("Override implementation using `onResume`.") + } + + onResume() + } +} diff --git a/Tests/NetworkingTests/URLSessionMock.swift b/Tests/NetworkingTests/URLSessionMock.swift index 67fb3a47..d27942e2 100644 --- a/Tests/NetworkingTests/URLSessionMock.swift +++ b/Tests/NetworkingTests/URLSessionMock.swift @@ -9,13 +9,27 @@ import Foundation import Networking final class URLSessionMock: URLSessionInterface { - var onData: ((_ request: URLRequest) async throws -> (Data, URLResponse))? + var onDataTask: ((URLRequest) throws -> (data: Data?, response: URLResponse?))? - func data(for request: URLRequest) async throws -> (Data, URLResponse) { - guard let onData = onData else { - fatalError("Override implementation using `onData`.") + func dataTask( + with request: URLRequest, + completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void + ) -> URLSessionDataTaskInterface { + guard let onDataTask = onDataTask else { + fatalError("Override implementation using `onDataTask`.") } - - return try await onData(request) + + let dataTask = URLSessionDataTaskMock() + + dataTask.onResume = { + do { + let result = try onDataTask(request) + completionHandler(result.data, result.response, nil) + } catch { + completionHandler(nil, nil, error) + } + } + + return dataTask } }