mirror of
https://github.com/mas-cli/mas
synced 2024-11-22 11:33:13 +00:00
♻️ NetworkSession refactor
This commit is contained in:
parent
84e2ba4177
commit
51927b17ca
18 changed files with 321 additions and 185 deletions
|
@ -25,15 +25,15 @@ public struct SearchCommand: CommandProtocol {
|
|||
public let verb = "search"
|
||||
public let function = "Search for apps from the Mac App Store"
|
||||
|
||||
private let urlSession: URLSession
|
||||
private let networkSession: NetworkSession
|
||||
|
||||
public init(urlSession: URLSession = URLSession.shared) {
|
||||
self.urlSession = urlSession
|
||||
public init(networkSession: NetworkSession = URLSession.shared) {
|
||||
self.networkSession = networkSession
|
||||
}
|
||||
|
||||
public func run(_ options: Options) -> Result<(), MASError> {
|
||||
guard let searchURLString = searchURLString(options.appName),
|
||||
let searchJson = urlSession.requestSynchronousJSONWithURLString(searchURLString) as? [String: Any] else {
|
||||
let searchJson = networkSession.requestSynchronousJSONWithURLString(searchURLString) as? [String: Any] else {
|
||||
return .failure(.searchFailed)
|
||||
}
|
||||
|
||||
|
|
|
@ -8,11 +8,11 @@
|
|||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
public class MasStoreSearch: StoreSearch {
|
||||
private let urlSession: URLSession
|
||||
private let networkManager: NetworkManager
|
||||
|
||||
/// Designated initializer.
|
||||
public init(urlSession: URLSession = URLSession.shared) {
|
||||
self.urlSession = urlSession
|
||||
public init(networkManager: NetworkManager = NetworkManager()) {
|
||||
self.networkManager = networkManager
|
||||
}
|
||||
|
||||
/// Looks up app details.
|
||||
|
@ -21,22 +21,29 @@ public class MasStoreSearch: StoreSearch {
|
|||
/// - Returns: Search result record of app or nil if no apps match the ID.
|
||||
/// - Throws: Error if there is a problem with the network request.
|
||||
public func lookup(app appId: String) throws -> SearchResult? {
|
||||
guard let lookupURLString = lookupURLString(forApp: appId),
|
||||
let jsonData = urlSession.requestSynchronousDataWithURLString(lookupURLString)
|
||||
guard let url = lookupURL(forApp: appId)
|
||||
else { throw MASError.searchFailed }
|
||||
|
||||
let result = networkManager.loadDataSync(from: url)
|
||||
|
||||
// Unwrap network result
|
||||
guard case let .success(data) = result
|
||||
else {
|
||||
// network error
|
||||
if case let .failure(error) = result {
|
||||
throw error
|
||||
}
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
|
||||
guard let results = try? JSONDecoder().decode(SearchResultList.self, from: jsonData)
|
||||
guard let results = try? JSONDecoder().decode(SearchResultList.self, from: data)
|
||||
else {
|
||||
// parse error
|
||||
throw MASError.searchFailed
|
||||
}
|
||||
|
||||
guard let result = results.results.first
|
||||
guard let searchResult = results.results.first
|
||||
else { return nil }
|
||||
|
||||
return result
|
||||
return searchResult
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
|
||||
/// Network abstraction
|
||||
class NetworkManager {
|
||||
public class NetworkManager {
|
||||
enum NetworkError: Error {
|
||||
case timeout
|
||||
}
|
||||
|
@ -18,8 +18,8 @@ class NetworkManager {
|
|||
|
||||
/// Designated initializer
|
||||
///
|
||||
/// - Parameter session: <#session description#>
|
||||
init(session: NetworkSession = URLSession.shared) {
|
||||
/// - Parameter session: A networking session.
|
||||
public init(session: NetworkSession = URLSession.shared) {
|
||||
self.session = session
|
||||
}
|
||||
|
||||
|
@ -30,8 +30,9 @@ class NetworkManager {
|
|||
/// - completionHandler: Closure where result is delivered.
|
||||
func loadData(from url: URL, completionHandler: @escaping (NetworkResult) -> Void) {
|
||||
session.loadData(from: url) { (data: Data?, error: Error?) in
|
||||
let result: NetworkResult = data != nil ? .success(data!) : .failure(error!)
|
||||
// let result = data.map(NetworkResult.success) ?? .failure(error)
|
||||
let result: NetworkResult = data != nil
|
||||
? .success(data!)
|
||||
: .failure(error!)
|
||||
completionHandler(result)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,52 @@
|
|||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
protocol NetworkSession {
|
||||
func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
@objc public protocol NetworkSession {
|
||||
@objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
}
|
||||
|
||||
// MARK: - URLSession+Synchronous
|
||||
extension NetworkSession {
|
||||
/// Return data from synchronous URL request
|
||||
public func requestSynchronousData(_ request: URLRequest) -> Data? {
|
||||
var data: Data? = nil
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) {
|
||||
(taskData, _, error) -> Void in
|
||||
data = taskData
|
||||
if data == nil, let error = error {
|
||||
print(error)
|
||||
}
|
||||
semaphore.signal()
|
||||
}
|
||||
task.resume()
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
return data
|
||||
}
|
||||
|
||||
/// Return data synchronous from specified endpoint
|
||||
public func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
guard let url = URL(string:requestString) else { return nil }
|
||||
let request = URLRequest(url: url)
|
||||
return requestSynchronousData(request)
|
||||
}
|
||||
|
||||
/// Return JSON synchronous from URL request
|
||||
public func requestSynchronousJSON(_ request: URLRequest) -> Any? {
|
||||
guard let data = requestSynchronousData(request) else { return nil }
|
||||
return try! JSONSerialization.jsonObject(with: data, options: [])
|
||||
}
|
||||
|
||||
/// Return JSON synchronous from specified endpoint
|
||||
public func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let url = URL(string: requestString) else { return nil }
|
||||
|
||||
var request = URLRequest(url:url)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
return requestSynchronousJSON(request)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,24 @@
|
|||
//
|
||||
// URLSession+Synchronous.swift
|
||||
// mas-cli
|
||||
// NetworkSession.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Michael Schneider on 4/14/16.
|
||||
// Copyright © 2016 Andrew Naylor. All rights reserved.
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
// Synchronous NSURLSession code found at: http://ericasadun.com/2015/11/12/more-bad-things-synchronous-nsurlsessions/
|
||||
@objc public protocol NetworkSession {
|
||||
@objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void)
|
||||
}
|
||||
|
||||
import Foundation
|
||||
|
||||
/// NSURLSession synchronous behavior
|
||||
/// Particularly for playground sessions that need to run sequentially
|
||||
public extension URLSession {
|
||||
// MARK: - URLSession+Synchronous
|
||||
extension NetworkSession {
|
||||
/// Return data from synchronous URL request
|
||||
public func requestSynchronousData(_ request: URLRequest) -> Data? {
|
||||
var data: Data? = nil
|
||||
let semaphore = DispatchSemaphore(value: 0)
|
||||
|
||||
let task = URLSession.shared.dataTask(with: request) {
|
||||
(taskData, _, error) -> Void in
|
||||
(taskData, _, error) -> Void in
|
||||
data = taskData
|
||||
if data == nil, let error = error {
|
||||
print(error)
|
||||
|
@ -26,12 +26,13 @@ public extension URLSession {
|
|||
semaphore.signal()
|
||||
}
|
||||
task.resume()
|
||||
|
||||
_ = semaphore.wait(timeout: .distantFuture)
|
||||
return data
|
||||
}
|
||||
|
||||
/// Return data synchronous from specified endpoint
|
||||
@objc public func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
public func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
guard let url = URL(string:requestString) else { return nil }
|
||||
let request = URLRequest(url: url)
|
||||
return requestSynchronousData(request)
|
||||
|
@ -44,18 +45,13 @@ public extension URLSession {
|
|||
}
|
||||
|
||||
/// Return JSON synchronous from specified endpoint
|
||||
@objc public func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
public func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let url = URL(string: requestString) else { return nil }
|
||||
|
||||
var request = URLRequest(url:url)
|
||||
request.httpMethod = "GET"
|
||||
request.addValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
return requestSynchronousJSON(request)
|
||||
}
|
||||
}
|
||||
|
||||
public extension String {
|
||||
/// Return an URL encoded string
|
||||
var URLEncodedString: String? {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
|
||||
extension URLSession: NetworkSession {
|
||||
func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
@objc open func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
let task = dataTask(with: url) { (data, _, error) in
|
||||
completionHandler(data, error)
|
||||
}
|
||||
|
|
|
@ -18,9 +18,16 @@ extension StoreSearch {
|
|||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: A string URL for the lookup service or nil if the appId can't be encoded.
|
||||
public func lookupURLString(forApp appId: String) -> String? {
|
||||
if let urlEncodedAppId = appId.URLEncodedString {
|
||||
return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)"
|
||||
}
|
||||
return nil
|
||||
guard let urlEncodedAppId = appId.URLEncodedString else { return nil }
|
||||
return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)"
|
||||
}
|
||||
|
||||
/// Builds the lookup URL for an app.
|
||||
///
|
||||
/// - Parameter appId: MAS app identifier.
|
||||
/// - Returns: A string URL for the lookup service or nil if the appId can't be encoded.
|
||||
public func lookupURL(forApp appId: String) -> URL? {
|
||||
guard let urlString = lookupURLString(forApp: appId) else { return nil }
|
||||
return URL(string: urlString)
|
||||
}
|
||||
}
|
||||
|
|
16
MasKit/String+PercentEncoding.swift
Normal file
16
MasKit/String+PercentEncoding.swift
Normal file
|
@ -0,0 +1,16 @@
|
|||
//
|
||||
// String+PercentEncoding.swift
|
||||
// MasKit
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
/// Return an URL encoded string
|
||||
var URLEncodedString: String? {
|
||||
return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)
|
||||
}
|
||||
}
|
30
MasKitTests/Bundle+JSON.swift
Normal file
30
MasKitTests/Bundle+JSON.swift
Normal file
|
@ -0,0 +1,30 @@
|
|||
//
|
||||
// Bundle+JSON.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
/// Locates a JSON response file from the test bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
static func jsonResponse(fileName: String) -> URL? {
|
||||
return Bundle(for: MockNetworkSession.self).fileURL(fileName: fileName)
|
||||
}
|
||||
|
||||
/// Builds a URL for a file in the JSON directory of the current bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
func fileURL(fileName: String) -> URL? {
|
||||
guard let path = self.path(forResource: fileName.fileNameWithoutExtension, ofType: fileName.fileExtension, inDirectory: "JSON")
|
||||
else { fatalError("Unable to load file \(fileName)") }
|
||||
|
||||
return URL(fileURLWithPath: path)
|
||||
}
|
||||
}
|
|
@ -16,7 +16,7 @@ class SearchSpec: QuickSpec {
|
|||
describe("search") {
|
||||
context("for slack") {
|
||||
it("succeeds") {
|
||||
let search = SearchCommand(urlSession: MockURLSession(responseFile: "search/slack.json"))
|
||||
let search = SearchCommand(networkSession: MockNetworkSessionFromFile(responseFile: "search/slack.json"))
|
||||
let searchOptions = SearchOptions(appName: "slack", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beSuccess())
|
||||
|
@ -24,7 +24,7 @@ class SearchSpec: QuickSpec {
|
|||
}
|
||||
context("for nonexistent") {
|
||||
it("fails") {
|
||||
let search = SearchCommand(urlSession: MockURLSession(responseFile: "search/nonexistent.json"))
|
||||
let search = SearchCommand(networkSession: MockNetworkSessionFromFile(responseFile: "search/nonexistent.json"))
|
||||
let searchOptions = SearchOptions(appName: "nonexistent", price: false)
|
||||
let result = search.run(searchOptions)
|
||||
expect(result).to(beFailure { error in
|
||||
|
|
|
@ -14,8 +14,9 @@ import Nimble
|
|||
class MasStoreSearchSpec: QuickSpec {
|
||||
override func spec() {
|
||||
let appId = 803453959
|
||||
let urlSession = MockURLSession(responseFile: "lookup/slack.json")
|
||||
let storeSearch = MasStoreSearch(urlSession: urlSession)
|
||||
let urlSession = MockNetworkSessionFromFile(responseFile: "lookup/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: urlSession))
|
||||
|
||||
describe("store search") {
|
||||
it("can find slack") {
|
||||
let result = try! storeSearch.lookup(app: appId.description)
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
//
|
||||
// MockURLSession.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/13/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
@testable import MasKit
|
||||
|
||||
/// Mock URLSession for testing.
|
||||
// FIXME: allow mock url session to operate offline
|
||||
//2019-01-04 17:20:41.741632-0800 xctest[76410:1817605] TIC TCP Conn Failed [3:0x100a67420]: 1:50 Err(50)
|
||||
//2019-01-04 17:20:41.741849-0800 xctest[76410:1817605] Task <0C05E774-1CDE-48FB-9408-AFFCD12F3F60>.<3> HTTP load failed (error code: -1009 [1:50])
|
||||
//2019-01-04 17:20:41.741903-0800 xctest[76410:1817605] Task <0C05E774-1CDE-48FB-9408-AFFCD12F3F60>.<3> finished with error - code: -1009
|
||||
//Error Domain=NSURLErrorDomain Code=-1009 "The Internet connection appears to be offline." UserInfo={NSUnderlyingError=0x100a692f0 {Error Domain=kCFErrorDomainCFNetwork Code=-1009 "(null)" UserInfo={_kCFStreamErrorCodeKey=50, _kCFStreamErrorDomainKey=1}}, NSErrorFailingURLStringKey=https://itunes.apple.com/lookup?id=803453959, NSErrorFailingURLKey=https://itunes.apple.com/lookup?id=803453959, _kCFStreamErrorDomainKey=1, _kCFStreamErrorCodeKey=50, NSLocalizedDescription=The Internet connection appears to be offline.}
|
||||
// Fatal error: 'try!' expression unexpectedly raised an error: Search failed: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.42/src/swift/stdlib/public/core/ErrorType.swift, line 184
|
||||
// 2019-01-04 17:20:41.818432-0800 xctest[76410:1817499] Fatal error: 'try!' expression unexpectedly raised an error: Search failed: file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.42/src/swift/stdlib/public/core/ErrorType.swift, line 184
|
||||
class MockURLSession: URLSession {
|
||||
// The singleton URL session, configured to use our custom config and delegate.
|
||||
static let session = URLSession(
|
||||
configuration: URLSessionConfiguration.testSessionConfiguration(),
|
||||
// Delegate is retained by the session.
|
||||
delegate: TestURLSessionDelegate(),
|
||||
delegateQueue: OperationQueue.main)
|
||||
|
||||
private let responseFile: String
|
||||
|
||||
/// Initializes a mock URL session with a file for the response.
|
||||
///
|
||||
/// - Parameter responseFile: Name of file containing JSON response body.
|
||||
init(responseFile: String) {
|
||||
self.responseFile = responseFile
|
||||
}
|
||||
|
||||
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
|
||||
|
||||
// Properties that enable us to set exactly what data or error
|
||||
// we want our mocked URLSession to return for any request.
|
||||
var data: Data?
|
||||
var error: Error?
|
||||
|
||||
override func dataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {
|
||||
let data = self.data
|
||||
let error = self.error
|
||||
|
||||
return URLSessionDataTaskMock {
|
||||
completionHandler(data, nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Override which returns Data from a file.
|
||||
///
|
||||
/// - Parameter requestString: Ignored URL string
|
||||
/// - Returns: Contents of responseFile
|
||||
@objc override func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
guard let fileURL = Bundle.jsonResponse(fileName: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
return data
|
||||
} catch {
|
||||
print("Error opening file: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Override which returns JSON contents from a file.
|
||||
///
|
||||
/// - Parameter requestString: Ignored URL string
|
||||
/// - Returns: Parsed contents of responseFile
|
||||
@objc override func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let data = requestSynchronousDataWithURLString(requestString)
|
||||
else { return nil }
|
||||
|
||||
do {
|
||||
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
|
||||
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
|
||||
return jsonResult
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing JSON: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
extension Bundle {
|
||||
/// Locates a JSON response file from the test bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
static func jsonResponse(fileName: String) -> URL? {
|
||||
return Bundle(for: MockURLSession.self).fileURL(fileName: fileName)
|
||||
}
|
||||
|
||||
/// Builds a URL for a file in the JSON directory of the current bundle.
|
||||
///
|
||||
/// - Parameter fileName: Name of file to locate.
|
||||
/// - Returns: URL to file.
|
||||
func fileURL(fileName: String) -> URL? {
|
||||
guard let path = self.path(forResource: fileName.fileNameWithoutExtension, ofType: fileName.fileExtension, inDirectory: "JSON")
|
||||
else { fatalError("Unable to load file \(fileName)") }
|
||||
|
||||
return URL(fileURLWithPath: path)
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
/// Returns the file name before the extension.
|
||||
var fileNameWithoutExtension: String {
|
||||
return (self as NSString).deletingPathExtension
|
||||
}
|
||||
|
||||
/// Returns the file extension.
|
||||
var fileExtension: String {
|
||||
return (self as NSString).pathExtension
|
||||
}
|
||||
}
|
44
MasKitTests/Network/MockNetworkSession.swift
Normal file
44
MasKitTests/Network/MockNetworkSession.swift
Normal file
|
@ -0,0 +1,44 @@
|
|||
//
|
||||
// MockURLSession.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/13/18.
|
||||
// Copyright © 2018 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import MasKit
|
||||
|
||||
/// Mock URLSession for testing.
|
||||
class MockNetworkSession: NetworkSession {
|
||||
typealias CompletionHandler = (Data?, URLResponse?, Error?) -> Void
|
||||
|
||||
// Properties that enable us to set exactly what data or error
|
||||
// we want our mocked URLSession to return for any request.
|
||||
var data: Data?
|
||||
var error: Error?
|
||||
|
||||
/// Creates a mock data task
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered both data and error properties (only one should be non-nil)
|
||||
/// - Returns: Mock data task
|
||||
func dataTask(with url: URL, completionHandler: @escaping CompletionHandler) -> URLSessionDataTask {
|
||||
let data = self.data
|
||||
let error = self.error
|
||||
|
||||
return MockURLSessionDataTask {
|
||||
completionHandler(data, nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Immediately passes data and error to completion handler.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
@objc func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
completionHandler(data, error)
|
||||
}
|
||||
}
|
||||
|
77
MasKitTests/Network/MockNetworkSessionFromFile.swift
Normal file
77
MasKitTests/Network/MockNetworkSessionFromFile.swift
Normal file
|
@ -0,0 +1,77 @@
|
|||
//
|
||||
// MockURLSession.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-05.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import MasKit
|
||||
|
||||
/// Mock URLSession for testing.
|
||||
class MockNetworkSessionFromFile: MockNetworkSession {
|
||||
private let responseFile: String
|
||||
|
||||
/// Initializes a mock URL session with a file for the response.
|
||||
///
|
||||
/// - Parameter responseFile: Name of file containing JSON response body.
|
||||
init(responseFile: String) {
|
||||
self.responseFile = responseFile
|
||||
}
|
||||
|
||||
/// Loads data from a file.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - url: unused
|
||||
/// - completionHandler: Closure which is delivered either data or an error.
|
||||
@objc override func loadData(from url: URL, completionHandler: @escaping (Data?, Error?) -> Void) {
|
||||
guard let fileURL = Bundle.jsonResponse(fileName: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
completionHandler(data, nil)
|
||||
} catch {
|
||||
print("Error opening file: \(error)")
|
||||
completionHandler(nil, error)
|
||||
}
|
||||
}
|
||||
|
||||
/// Override which returns Data from a file.
|
||||
///
|
||||
/// - Parameter requestString: Ignored URL string
|
||||
/// - Returns: Contents of responseFile
|
||||
@objc func requestSynchronousDataWithURLString(_ requestString: String) -> Data? {
|
||||
guard let fileURL = Bundle.jsonResponse(fileName: responseFile)
|
||||
else { fatalError("Unable to load file \(responseFile)") }
|
||||
|
||||
do {
|
||||
let data = try Data(contentsOf: fileURL, options: .mappedIfSafe)
|
||||
return data
|
||||
} catch {
|
||||
print("Error opening file: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/// Override which returns JSON contents from a file.
|
||||
///
|
||||
/// - Parameter requestString: Ignored URL string
|
||||
/// - Returns: Parsed contents of responseFile
|
||||
@objc func requestSynchronousJSONWithURLString(_ requestString: String) -> Any? {
|
||||
guard let data = requestSynchronousDataWithURLString(requestString)
|
||||
else { return nil }
|
||||
|
||||
do {
|
||||
let jsonResult = try JSONSerialization.jsonObject(with: data, options: .mutableLeaves)
|
||||
if let jsonResult = jsonResult as? Dictionary<String, AnyObject> {
|
||||
return jsonResult
|
||||
}
|
||||
} catch {
|
||||
print("Error parsing JSON: \(error)")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@
|
|||
import Foundation
|
||||
|
||||
// Partial mock subclassing the original class
|
||||
class URLSessionDataTaskMock: URLSessionDataTask {
|
||||
class MockURLSessionDataTask: URLSessionDataTask {
|
||||
private let closure: () -> Void
|
||||
|
||||
init(closure: @escaping () -> Void) {
|
|
@ -12,7 +12,7 @@ import XCTest
|
|||
class NetworkManagerTests: XCTestCase {
|
||||
func testSuccessfulAsyncResponse() {
|
||||
// Setup our objects
|
||||
let session = MockURLSession(responseFile: "")
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
|
@ -30,7 +30,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testSuccessfulSyncResponse() {
|
||||
// Setup our objects
|
||||
let session = MockURLSession(responseFile: "")
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
|
@ -47,7 +47,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testFailureAsyncResponse() {
|
||||
// Setup our objects
|
||||
let session = MockURLSession(responseFile: "")
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = NetworkManager.NetworkError.timeout
|
||||
|
@ -63,7 +63,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testFailureSyncResponse() {
|
||||
// Setup our objects
|
||||
let session = MockURLSession(responseFile: "")
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = NetworkManager.NetworkError.timeout
|
||||
|
|
21
MasKitTests/String+FileExtension.swift
Normal file
21
MasKitTests/String+FileExtension.swift
Normal file
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// String+FileExtension.swift
|
||||
// MasKitTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/5/19.
|
||||
// Copyright © 2019 mas-cli. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// Returns the file name before the extension.
|
||||
var fileNameWithoutExtension: String {
|
||||
return (self as NSString).deletingPathExtension
|
||||
}
|
||||
|
||||
/// Returns the file extension.
|
||||
var fileExtension: String {
|
||||
return (self as NSString).pathExtension
|
||||
}
|
||||
}
|
|
@ -31,8 +31,12 @@
|
|||
B576FE0621E114470016B39D /* MockURLSessionDataTask.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0521E114470016B39D /* MockURLSessionDataTask.swift */; };
|
||||
B576FE0821E114A80016B39D /* NetworkResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0721E114A80016B39D /* NetworkResult.swift */; };
|
||||
B576FE0C21E116590016B39D /* NetworkManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0B21E116590016B39D /* NetworkManagerTests.swift */; };
|
||||
B576FE0E21E1D6310016B39D /* String+PercentEncoding.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */; };
|
||||
B576FE1221E1D82D0016B39D /* MockNetworkSessionFromFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1121E1D82D0016B39D /* MockNetworkSessionFromFile.swift */; };
|
||||
B576FE1421E1D8A90016B39D /* Bundle+JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */; };
|
||||
B576FE1621E1D8CB0016B39D /* String+FileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */; };
|
||||
B5793E29219BDD4800135B39 /* JSON in Resources */ = {isa = PBXBuildFile; fileRef = B5793E28219BDD4800135B39 /* JSON */; };
|
||||
B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5793E2A219BE0CD00135B39 /* MockURLSession.swift */; };
|
||||
B5793E2B219BE0CD00135B39 /* MockNetworkSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5793E2A219BE0CD00135B39 /* MockNetworkSession.swift */; };
|
||||
B588CE0221DC89490047D305 /* ExternalCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B588CE0121DC89490047D305 /* ExternalCommand.swift */; };
|
||||
B588CE0421DC8AFB0047D305 /* TrashCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = B588CE0321DC8AFB0047D305 /* TrashCommand.swift */; };
|
||||
B594B12021D53A8200F3AC59 /* Uninstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B11F21D53A8200F3AC59 /* Uninstall.swift */; };
|
||||
|
@ -117,7 +121,6 @@
|
|||
F8FB717920F2B4DD00F56FDC /* Upgrade.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDD3B3621C34709400B56B88 /* Upgrade.swift */; };
|
||||
F8FB717A20F2B4DD00F56FDC /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDB6CE8B1BAEC3D400648B4D /* Version.swift */; };
|
||||
F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED0F238C1B8756E600AE40CD /* MASError.swift */; };
|
||||
F8FB717C20F2B4DD00F56FDC /* URLSession+Synchronous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A989A1CBFFAAA0004D3B4 /* URLSession+Synchronous.swift */; };
|
||||
F8FB717D20F2B4DD00F56FDC /* Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = EDCBF9541D89CFC7000039C6 /* Utilities.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
|
@ -188,7 +191,6 @@
|
|||
|
||||
/* Begin PBXFileReference section */
|
||||
693A98981CBFFA760004D3B4 /* Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
|
||||
693A989A1CBFFAAA0004D3B4 /* URLSession+Synchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URLSession+Synchronous.swift"; sourceTree = "<group>"; };
|
||||
8078FAA71EC4F2FB004B5B3F /* Lucky.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Lucky.swift; sourceTree = "<group>"; };
|
||||
900A1E801DBAC8CB0069B1A8 /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = "<group>"; };
|
||||
90CB4069213F4DDD0044E445 /* Result.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; path = Result.framework; sourceTree = "<group>"; };
|
||||
|
@ -208,8 +210,12 @@
|
|||
B576FE0521E114470016B39D /* MockURLSessionDataTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSessionDataTask.swift; sourceTree = "<group>"; };
|
||||
B576FE0721E114A80016B39D /* NetworkResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkResult.swift; sourceTree = "<group>"; };
|
||||
B576FE0B21E116590016B39D /* NetworkManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkManagerTests.swift; sourceTree = "<group>"; };
|
||||
B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+PercentEncoding.swift"; sourceTree = "<group>"; };
|
||||
B576FE1121E1D82D0016B39D /* MockNetworkSessionFromFile.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MockNetworkSessionFromFile.swift; sourceTree = "<group>"; };
|
||||
B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Bundle+JSON.swift"; sourceTree = "<group>"; };
|
||||
B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+FileExtension.swift"; sourceTree = "<group>"; };
|
||||
B5793E28219BDD4800135B39 /* JSON */ = {isa = PBXFileReference; lastKnownFileType = folder; path = JSON; sourceTree = "<group>"; };
|
||||
B5793E2A219BE0CD00135B39 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = "<group>"; };
|
||||
B5793E2A219BE0CD00135B39 /* MockNetworkSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockNetworkSession.swift; sourceTree = "<group>"; };
|
||||
B588CE0121DC89490047D305 /* ExternalCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExternalCommand.swift; sourceTree = "<group>"; };
|
||||
B588CE0321DC8AFB0047D305 /* TrashCommand.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TrashCommand.swift; sourceTree = "<group>"; };
|
||||
B594B11F21D53A8200F3AC59 /* Uninstall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Uninstall.swift; sourceTree = "<group>"; };
|
||||
|
@ -354,7 +360,6 @@
|
|||
B576FE0721E114A80016B39D /* NetworkResult.swift */,
|
||||
B576FDFF21E113610016B39D /* NetworkSession.swift */,
|
||||
B576FE0121E1139E0016B39D /* URLSession+NetworkSession.swift */,
|
||||
693A989A1CBFFAAA0004D3B4 /* URLSession+Synchronous.swift */,
|
||||
);
|
||||
path = Network;
|
||||
sourceTree = "<group>";
|
||||
|
@ -362,6 +367,9 @@
|
|||
B576FE0A21E116470016B39D /* Network */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B5793E2A219BE0CD00135B39 /* MockNetworkSession.swift */,
|
||||
B576FE1121E1D82D0016B39D /* MockNetworkSessionFromFile.swift */,
|
||||
B576FE0521E114470016B39D /* MockURLSessionDataTask.swift */,
|
||||
B576FE0B21E116590016B39D /* NetworkManagerTests.swift */,
|
||||
B576FDFB21E10A610016B39D /* TestURLSessionConfiguration.swift */,
|
||||
B576FDFD21E10B660016B39D /* TestURLSessionDelegate.swift */,
|
||||
|
@ -410,8 +418,6 @@
|
|||
B5DBF81621E02E3400F3B151 /* MockOpenSystemCommand.swift */,
|
||||
B576FDF821E107CA0016B39D /* MockSoftwareProduct.swift */,
|
||||
B5DBF81421E02BA900F3B151 /* MockStoreSearch.swift */,
|
||||
B5793E2A219BE0CD00135B39 /* MockURLSession.swift */,
|
||||
B576FE0521E114470016B39D /* MockURLSessionDataTask.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
|
@ -511,6 +517,7 @@
|
|||
B594B15521D89F5200F3AC59 /* SearchResultList.swift */,
|
||||
B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */,
|
||||
B594B14F21D8998000F3AC59 /* StoreSearch.swift */,
|
||||
B576FE0D21E1D6310016B39D /* String+PercentEncoding.swift */,
|
||||
EDCBF9541D89CFC7000039C6 /* Utilities.swift */,
|
||||
);
|
||||
path = MasKit;
|
||||
|
@ -519,6 +526,7 @@
|
|||
F8FB715E20F2B41400F56FDC /* MasKitTests */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B576FE1321E1D8A90016B39D /* Bundle+JSON.swift */,
|
||||
B594B12321D57FF300F3AC59 /* Commands */,
|
||||
F8FB716120F2B41400F56FDC /* Info.plist */,
|
||||
B5793E28219BDD4800135B39 /* JSON */,
|
||||
|
@ -527,6 +535,7 @@
|
|||
B594B12C21D584E800F3AC59 /* Mocks */,
|
||||
B576FE0A21E116470016B39D /* Network */,
|
||||
B594B13121D5876200F3AC59 /* ResultPredicates.swift */,
|
||||
B576FE1521E1D8CB0016B39D /* String+FileExtension.swift */,
|
||||
);
|
||||
path = MasKitTests;
|
||||
sourceTree = "<group>";
|
||||
|
@ -778,7 +787,6 @@
|
|||
B576FDF721E107AA0016B39D /* OpenSystemCommand.swift in Sources */,
|
||||
F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */,
|
||||
B594B15221D89A8B00F3AC59 /* MasStoreSearch.swift in Sources */,
|
||||
F8FB717C20F2B4DD00F56FDC /* URLSession+Synchronous.swift in Sources */,
|
||||
B5DBF80D21DEE4E600F3B151 /* Open.swift in Sources */,
|
||||
F8FB717420F2B4DD00F56FDC /* Outdated.swift in Sources */,
|
||||
F8FB716C20F2B4DD00F56FDC /* PurchaseDownloadObserver.swift in Sources */,
|
||||
|
@ -786,6 +794,7 @@
|
|||
F8FB717620F2B4DD00F56FDC /* Search.swift in Sources */,
|
||||
B576FE0021E113610016B39D /* NetworkSession.swift in Sources */,
|
||||
B594B15421D89DF400F3AC59 /* SearchResult.swift in Sources */,
|
||||
B576FE0E21E1D6310016B39D /* String+PercentEncoding.swift in Sources */,
|
||||
B576FDFA21E1081C0016B39D /* SearchResultList.swift in Sources */,
|
||||
F8FB717720F2B4DD00F56FDC /* SignIn.swift in Sources */,
|
||||
B576FE0221E1139E0016B39D /* URLSession+NetworkSession.swift in Sources */,
|
||||
|
@ -817,15 +826,17 @@
|
|||
B594B12E21D5850700F3AC59 /* MockAppLibrary.swift in Sources */,
|
||||
B5DBF81721E02E3400F3B151 /* MockOpenSystemCommand.swift in Sources */,
|
||||
B5DBF81521E02BA900F3B151 /* MockStoreSearch.swift in Sources */,
|
||||
B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */,
|
||||
B5793E2B219BE0CD00135B39 /* MockNetworkSession.swift in Sources */,
|
||||
B5DBF81321DEEC7C00F3B151 /* OpenCommandSpec.swift in Sources */,
|
||||
B594B14221D6D8EC00F3AC59 /* OutdatedCommandSpec.swift in Sources */,
|
||||
B576FE0621E114470016B39D /* MockURLSessionDataTask.swift in Sources */,
|
||||
B594B14021D6D8BF00F3AC59 /* ResetCommandSpec.swift in Sources */,
|
||||
B576FE1421E1D8A90016B39D /* Bundle+JSON.swift in Sources */,
|
||||
B576FDF921E107CA0016B39D /* MockSoftwareProduct.swift in Sources */,
|
||||
B576FE0C21E116590016B39D /* NetworkManagerTests.swift in Sources */,
|
||||
B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */,
|
||||
B594B13E21D6D78900F3AC59 /* SearchCommandSpec.swift in Sources */,
|
||||
B576FE1621E1D8CB0016B39D /* String+FileExtension.swift in Sources */,
|
||||
B555292D219A1FE700ACB4CA /* SearchSpec.swift in Sources */,
|
||||
B594B13C21D6D72E00F3AC59 /* SignInCommandSpec.swift in Sources */,
|
||||
B594B13A21D6D70400F3AC59 /* SignOutCommandSpec.swift in Sources */,
|
||||
|
@ -833,6 +844,7 @@
|
|||
B594B13821D6D6C100F3AC59 /* UpgradeCommandSpec.swift in Sources */,
|
||||
B576FDFE21E10B660016B39D /* TestURLSessionDelegate.swift in Sources */,
|
||||
B5DBF81121DEEC4200F3B151 /* VendorCommandSpec.swift in Sources */,
|
||||
B576FE1221E1D82D0016B39D /* MockNetworkSessionFromFile.swift in Sources */,
|
||||
B576FDF521E1078F0016B39D /* MASErrorTestCase.swift in Sources */,
|
||||
B594B13621D6D68600F3AC59 /* VersionCommandSpec.swift in Sources */,
|
||||
B576FDFC21E10A610016B39D /* TestURLSessionConfiguration.swift in Sources */,
|
||||
|
|
Loading…
Reference in a new issue