♻️ NetworkSession refactor

This commit is contained in:
Ben Chatelain 2019-01-06 12:26:08 -07:00
parent 84e2ba4177
commit 51927b17ca
18 changed files with 321 additions and 185 deletions

View file

@ -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)
}

View file

@ -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
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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)
}

View file

@ -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)
}
}

View 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)
}
}

View 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)
}
}

View file

@ -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

View file

@ -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)

View file

@ -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
}
}

View 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)
}
}

View 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
}
}

View file

@ -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) {

View file

@ -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

View 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
}
}

View file

@ -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 */,