diff --git a/App/mas-cli-Bridging-Header.h b/App/mas-cli-Bridging-Header.h deleted file mode 100644 index 902c42d..0000000 --- a/App/mas-cli-Bridging-Header.h +++ /dev/null @@ -1,36 +0,0 @@ -// -// mas-cli-Bridging-Header.h -// mas-cli -// -// Created by Andrew Naylor on 11/07/2015. -// Copyright © 2015 Andrew Naylor. All rights reserved. -// - -#ifndef mas_cli_Bridging_Header_h -#define mas_cli_Bridging_Header_h - -@import Foundation; - -//#import -// -//#import -//#import -//#import -//#import -//#import -// -//#import -//#import -//#import -//#import -//#import -//#import -//#import -//#import -//#import -//#import -//#import -//#import "ISAccountService-Protocol.h" -//#import - -#endif /* mas_cli_Bridging_Header_h */ diff --git a/CHANGELOG.md b/CHANGELOG.md index 608139d..7d97080 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] - +- ✨🗑 `uninstall` command #81, #191 ## [v1.4.4] 🧹 Cleanup - 2018-12-19 diff --git a/Homebrew/mas-tap.rb b/Homebrew/mas-tap.rb index fcaf22e..717b3ad 100644 --- a/Homebrew/mas-tap.rb +++ b/Homebrew/mas-tap.rb @@ -13,10 +13,9 @@ class Mas < Formula sha256 "237fd7270cb8f0d68a33e7ce05671a2e5c269d05d736abb0f66b50215439084e" => :high_sierra sha256 "237fd7270cb8f0d68a33e7ce05671a2e5c269d05d736abb0f66b50215439084e" => :sierra sha256 "237fd7270cb8f0d68a33e7ce05671a2e5c269d05d736abb0f66b50215439084e" => :el_capitan - sha256 "237fd7270cb8f0d68a33e7ce05671a2e5c269d05d736abb0f66b50215439084e" => :yosemite - sha256 "237fd7270cb8f0d68a33e7ce05671a2e5c269d05d736abb0f66b50215439084e" => :mavericks end + depends_on "trash" depends_on "carthage" => :build depends_on :xcode => ["10.1", :build] diff --git a/Homebrew/mas.rb b/Homebrew/mas.rb index 71b0b71..e8c20d4 100644 --- a/Homebrew/mas.rb +++ b/Homebrew/mas.rb @@ -12,6 +12,7 @@ class Mas < Formula sha256 "fc6658113d785a660e3f4d2e4e134ad02fe003ffa7d69271a2c53f503aaae726" => :high_sierra end + depends_on "trash" depends_on "carthage" => :build depends_on :xcode => ["10.1", :build] diff --git a/MasKit/AppLibrary.swift b/MasKit/AppLibrary.swift new file mode 100644 index 0000000..f35d73f --- /dev/null +++ b/MasKit/AppLibrary.swift @@ -0,0 +1,22 @@ +// +// AppLibrary.swift +// MasKit +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +/// Utility for managing installed apps. +public protocol AppLibrary { + /// Finds an app by ID from the set of installed apps + /// + /// - Parameter appId: MAS ID for app. + /// - Returns: Software Product of app if found; nil otherwise. + func installedApp(appId: UInt64) -> SoftwareProduct? + + /// Uninstalls an app. + /// + /// - Parameter app: App to be removed. + /// - Throws: Error if there is a problem. + func uninstallApp(app: SoftwareProduct) throws +} diff --git a/App/AppStore/CKSoftwareMap+AppLookup.swift b/MasKit/AppStore/CKSoftwareMap+AppLookup.swift similarity index 100% rename from App/AppStore/CKSoftwareMap+AppLookup.swift rename to MasKit/AppStore/CKSoftwareMap+AppLookup.swift diff --git a/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift b/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift new file mode 100644 index 0000000..22312b6 --- /dev/null +++ b/MasKit/AppStore/CKSoftwareProduct+SoftwareProduct.swift @@ -0,0 +1,12 @@ +// +// File.swift +// MasKit +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import StoreFoundation + +// MARK: - SoftwareProduct +extension CKSoftwareProduct: SoftwareProduct {} diff --git a/App/AppStore/Downloader.swift b/MasKit/AppStore/Downloader.swift similarity index 100% rename from App/AppStore/Downloader.swift rename to MasKit/AppStore/Downloader.swift diff --git a/App/AppStore/ISStoreAccount.swift b/MasKit/AppStore/ISStoreAccount.swift similarity index 100% rename from App/AppStore/ISStoreAccount.swift rename to MasKit/AppStore/ISStoreAccount.swift diff --git a/App/AppStore/PurchaseDownloadObserver.swift b/MasKit/AppStore/PurchaseDownloadObserver.swift similarity index 100% rename from App/AppStore/PurchaseDownloadObserver.swift rename to MasKit/AppStore/PurchaseDownloadObserver.swift diff --git a/App/AppStore/SSPurchase.swift b/MasKit/AppStore/SSPurchase.swift similarity index 100% rename from App/AppStore/SSPurchase.swift rename to MasKit/AppStore/SSPurchase.swift diff --git a/App/AppStore/StoreAccount.swift b/MasKit/AppStore/StoreAccount.swift similarity index 100% rename from App/AppStore/StoreAccount.swift rename to MasKit/AppStore/StoreAccount.swift diff --git a/App/Commands/Account.swift b/MasKit/Commands/Account.swift similarity index 100% rename from App/Commands/Account.swift rename to MasKit/Commands/Account.swift diff --git a/App/Commands/Info.swift b/MasKit/Commands/Info.swift similarity index 100% rename from App/Commands/Info.swift rename to MasKit/Commands/Info.swift diff --git a/App/Commands/Install.swift b/MasKit/Commands/Install.swift similarity index 94% rename from App/Commands/Install.swift rename to MasKit/Commands/Install.swift index 425dcd7..0b3675a 100644 --- a/App/Commands/Install.swift +++ b/MasKit/Commands/Install.swift @@ -16,15 +16,15 @@ public struct InstallCommand: CommandProtocol { public let function = "Install from the Mac App Store" public init() {} - + public func run(_ options: Options) -> Result<(), MASError> { // Try to download applications with given identifiers and collect results let downloadResults = options.appIds.compactMap { (appId) -> MASError? in - if let product = installedApp(appId) , !options.forceInstall { + if let product = installedApp(appId), !options.forceInstall { printWarning("\(product.appName) is already installed") return nil } - + return download(appId) } @@ -37,10 +37,10 @@ public struct InstallCommand: CommandProtocol { return .failure(.downloadFailed(error: nil)) } } - + fileprivate func installedApp(_ appId: UInt64) -> CKSoftwareProduct? { let appId = NSNumber(value: appId) - + let softwareMap = CKSoftwareMap.shared() return softwareMap.allProducts()?.first { $0.itemIdentifier == appId } } diff --git a/App/Commands/List.swift b/MasKit/Commands/List.swift similarity index 100% rename from App/Commands/List.swift rename to MasKit/Commands/List.swift diff --git a/App/Commands/Lucky.swift b/MasKit/Commands/Lucky.swift similarity index 99% rename from App/Commands/Lucky.swift rename to MasKit/Commands/Lucky.swift index 2926f81..a88b9a2 100644 --- a/App/Commands/Lucky.swift +++ b/MasKit/Commands/Lucky.swift @@ -8,7 +8,6 @@ import Commandant import Result - import CommerceKit public struct LuckyCommand: CommandProtocol { diff --git a/App/Commands/Outdated.swift b/MasKit/Commands/Outdated.swift similarity index 100% rename from App/Commands/Outdated.swift rename to MasKit/Commands/Outdated.swift diff --git a/App/Commands/Reset.swift b/MasKit/Commands/Reset.swift similarity index 97% rename from App/Commands/Reset.swift rename to MasKit/Commands/Reset.swift index 340879d..25137ac 100644 --- a/App/Commands/Reset.swift +++ b/MasKit/Commands/Reset.swift @@ -16,7 +16,7 @@ public struct ResetCommand: CommandProtocol { public let function = "Resets the Mac App Store" public init() {} - + public func run(_ options: Options) -> Result<(), MASError> { /* The "Reset Application" command in the Mac App Store debug menu performs @@ -26,7 +26,7 @@ public struct ResetCommand: CommandProtocol { - killall storeagent (storeagent no longer exists) - rm com.apple.appstore download directory - clear cookies (appears to be a no-op) - + As storeagent no longer exists we will implement a slight variant and kill all App Store-associated processes - storeaccountd @@ -35,7 +35,7 @@ public struct ResetCommand: CommandProtocol { - storeinstalld - storelegacy */ - + // Kill processes let killProcs = [ "Dock", @@ -45,24 +45,24 @@ public struct ResetCommand: CommandProtocol { "storeinstalld", "storelegacy", ] - + let kill = Process() let stdout = Pipe() let stderr = Pipe() - + kill.launchPath = "/usr/bin/killall" kill.arguments = killProcs kill.standardOutput = stdout kill.standardError = stderr - + kill.launch() kill.waitUntilExit() - + if kill.terminationStatus != 0 && options.debug { let output = stderr.fileHandleForReading.readDataToEndOfFile() printInfo("killall failed:\r\n\(String(data: output, encoding: String.Encoding.utf8)!)") } - + // Wipe Download Directory if let directory = CKDownloadDirectory(nil) { do { diff --git a/App/Commands/Search.swift b/MasKit/Commands/Search.swift similarity index 95% rename from App/Commands/Search.swift rename to MasKit/Commands/Search.swift index 4017360..aa14665 100644 --- a/App/Commands/Search.swift +++ b/MasKit/Commands/Search.swift @@ -18,6 +18,8 @@ struct ResultKeys { static let Price = "price" } +/// Search the Mac App Store using the iTunes Search API: +/// https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ public struct SearchCommand: CommandProtocol { public typealias Options = SearchOptions public let verb = "search" diff --git a/App/Commands/SignIn.swift b/MasKit/Commands/SignIn.swift similarity index 100% rename from App/Commands/SignIn.swift rename to MasKit/Commands/SignIn.swift diff --git a/App/Commands/SignOut.swift b/MasKit/Commands/SignOut.swift similarity index 100% rename from App/Commands/SignOut.swift rename to MasKit/Commands/SignOut.swift diff --git a/MasKit/Commands/Uninstall.swift b/MasKit/Commands/Uninstall.swift new file mode 100644 index 0000000..31de06b --- /dev/null +++ b/MasKit/Commands/Uninstall.swift @@ -0,0 +1,77 @@ +// +// Upgrade.swift +// mas-cli +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2015 Andrew Naylor. All rights reserved. +// + +import Commandant +import Result +import CommerceKit +import StoreFoundation + +/// Command which uninstalls apps managed by the Mac App Store. +public struct UninstallCommand: CommandProtocol { + public typealias Options = UninstallOptions + public let verb = "uninstall" + public let function = "Uninstall app installed from the Mac App Store" + + private let appLibrary: AppLibrary + + /// Designated initializer. + /// + /// - Parameter appLibrary: <#appLibrary description#> + public init(appLibrary: AppLibrary = MasAppLibrary()) { + self.appLibrary = appLibrary + } + + /// Runs the uninstall command. + /// + /// - Parameter options: UninstallOptions (arguments) for this command + /// - Returns: Success or an error. + public func run(_ options: Options) -> Result<(), MASError> { + let appId = UInt64(options.appId) + + guard let product = appLibrary.installedApp(appId: appId) else { + return .failure(.notInstalled) + } + + if options.dryRun { + printInfo("\(product.appName) \(product.bundlePath)") + printInfo("(not removed, dry run)") + + return .success(()) + } + + do { + try appLibrary.uninstallApp(app: product) + } + catch { + return .failure(.uninstallFailed) + } + + return .success(()) + } +} + +/// Options for the uninstall command. +public struct UninstallOptions: OptionsProtocol { + /// Numeric app ID + let appId: Int + + /// Flag indicating that removal shouldn't be performed + let dryRun: Bool + + static func create(_ appId: Int) -> (_ dryRun: Bool) -> UninstallOptions { + return { dryRun in + return UninstallOptions(appId: appId, dryRun: dryRun) + } + } + + public static func evaluate(_ m: CommandMode) -> Result> { + return create + <*> m <| Argument(usage: "ID of app to uninstall") + <*> m <| Switch(flag: nil, key: "dry-run", usage: "dry run") + } +} diff --git a/App/Commands/Upgrade.swift b/MasKit/Commands/Upgrade.swift similarity index 100% rename from App/Commands/Upgrade.swift rename to MasKit/Commands/Upgrade.swift diff --git a/App/Commands/Version.swift b/MasKit/Commands/Version.swift similarity index 100% rename from App/Commands/Version.swift rename to MasKit/Commands/Version.swift diff --git a/App/MASError.swift b/MasKit/MASError.swift similarity index 89% rename from App/MASError.swift rename to MasKit/MASError.swift index eb1ceb6..515be9f 100644 --- a/App/MASError.swift +++ b/MasKit/MASError.swift @@ -22,6 +22,9 @@ public enum MASError: Error, CustomStringConvertible, Equatable { case searchFailed case noSearchResultsFound + case notInstalled + case uninstallFailed + public var description: String { switch self { case .notSignedIn: @@ -67,7 +70,13 @@ public enum MASError: Error, CustomStringConvertible, Equatable { return "Search failed" case .noSearchResultsFound: - return "No results found" + return "No results found" + + case .notInstalled: + return "Not installed" + + case .uninstallFailed: + return "Uninstall failed" } } } diff --git a/MasKit/MasAppLibrary.swift b/MasKit/MasAppLibrary.swift new file mode 100644 index 0000000..28fae59 --- /dev/null +++ b/MasKit/MasAppLibrary.swift @@ -0,0 +1,78 @@ +// +// MasAppLibrary.swift +// MasKit +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +import CommerceKit + +/// Utility for managing installed apps. +public class MasAppLibrary: AppLibrary { + /// CommerceKit's singleton manager of installed software. + private let softwareMap = CKSoftwareMap.shared() + + public init() {} + + /// Finds an app by ID from the set of installed apps + /// + /// - Parameter appId: MAS ID for app. + /// - Returns: Software Product of app if found; nil otherwise. + public func installedApp(appId: UInt64) -> SoftwareProduct? { + let appId = NSNumber(value: appId) + return softwareMap.allProducts()?.first { $0.itemIdentifier == appId } + } + + /// Uninstalls an app. + /// + /// - Parameter app: App to be removed. + /// - Throws: Error if there is a problem. + public func uninstallApp(app: SoftwareProduct) throws { + let status = trash(path: app.bundlePath) + if !status { + throw MASError.uninstallFailed + } + } + + /// Runs the trash command in another process. Relies on the "trash" command + /// from Homebrew. Trash requires el_capitan or higher for core bottles: + /// https://github.com/Homebrew/homebrew-core/blob/master/Formula/trash.rb + /// + /// - Parameter path: Absolute path to the application bundle to uninstall. + /// - Returns: true on success; fail on error + func trash(path: String) -> Bool { + let binaryPath = "/usr/local/bin/trash" + let process = Process() + let stdout = Pipe() + let stderr = Pipe() + + process.standardOutput = stdout + process.standardError = stderr + process.arguments = [path] + + if #available(OSX 10.13, *) { + process.executableURL = URL(fileURLWithPath: binaryPath) + do { + try process.run() + } catch { + printError("Unable to launch trash command") + return false + } + } else { + process.launchPath = binaryPath + process.launch() + } + + process.waitUntilExit() + + if process.terminationStatus == 0 { + return true + } else { + let reason = process.terminationReason + let output = stderr.fileHandleForReading.readDataToEndOfFile() + printError("Uninstall failed: \(reason)\n\(String(data: output, encoding: .utf8)!)") + return false + } + } +} diff --git a/MasKit/MasKit.h b/MasKit/MasKit.h index d2be907..3b4b909 100644 --- a/MasKit/MasKit.h +++ b/MasKit/MasKit.h @@ -6,8 +6,6 @@ // Copyright © 2018 Andrew Naylor. All rights reserved. // -@import CommerceKit; -@import StoreFoundation; @import Cocoa; //! Project version number for MasKit. diff --git a/App/NSURLSession+Synchronous.swift b/MasKit/NSURLSession+Synchronous.swift similarity index 100% rename from App/NSURLSession+Synchronous.swift rename to MasKit/NSURLSession+Synchronous.swift diff --git a/MasKit/SoftwareProduct.swift b/MasKit/SoftwareProduct.swift new file mode 100644 index 0000000..2d0915f --- /dev/null +++ b/MasKit/SoftwareProduct.swift @@ -0,0 +1,14 @@ +// +// SoftwareProduct.swift +// MasKit +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +/// Protocol describing the members of CKSoftwareProduct used throughout MasKit. +public protocol SoftwareProduct { + var appName: String { get } + var bundlePath: String { get set } + var itemIdentifier: NSNumber { get set } +} diff --git a/App/Utilities.swift b/MasKit/Utilities.swift similarity index 100% rename from App/Utilities.swift rename to MasKit/Utilities.swift diff --git a/MasKitTests/Commands/ListCommandSpec.swift b/MasKitTests/Commands/ListCommandSpec.swift new file mode 100644 index 0000000..339eea4 --- /dev/null +++ b/MasKitTests/Commands/ListCommandSpec.swift @@ -0,0 +1,25 @@ +// +// ListCommandSpec.swift +// MasKitTests +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import MasKit +import Result +import Quick +import Nimble + +class ListCommandSpec: QuickSpec { + override func spec() { + describe("list command") { + it("lists stuff") { + let list = ListCommand() + let result = list.run(ListCommand.Options()) + print(result) +// expect(result).to(beSuccess()) + } + } + } +} diff --git a/MasKitTests/SearchSpec.swift b/MasKitTests/Commands/SearchSpec.swift similarity index 72% rename from MasKitTests/SearchSpec.swift rename to MasKitTests/Commands/SearchSpec.swift index a944152..b696ccf 100644 --- a/MasKitTests/SearchSpec.swift +++ b/MasKitTests/Commands/SearchSpec.swift @@ -60,26 +60,3 @@ class SearchSpec: QuickSpec { } } } - -/// Nimble predicate for result enum success case, no associated value -private func beSuccess() -> Predicate> { - return Predicate.define("be ") { expression, message in - if let actual = try expression.evaluate(), - case .success = actual { - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} - -/// Nimble predicate for result enum failure with associated error -private func beFailure(test: @escaping (MASError) -> Void = { _ in }) -> Predicate> { - return Predicate.define("be ") { expression, message in - if let actual = try expression.evaluate(), - case let .failure(error) = actual { - test(error) - return PredicateResult(status: .matches, message: message) - } - return PredicateResult(status: .fail, message: message) - } -} diff --git a/MasKitTests/Commands/UninstallCommandSpec.swift b/MasKitTests/Commands/UninstallCommandSpec.swift new file mode 100644 index 0000000..1087b9b --- /dev/null +++ b/MasKitTests/Commands/UninstallCommandSpec.swift @@ -0,0 +1,75 @@ +// +// UninstallCommandSpec.swift +// MasKitTests +// +// Created by Ben Chatelain on 2018-12-27. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import MasKit +import Result +import Quick +import Nimble + +class UninstallCommandSpec: QuickSpec { + override func spec() { + describe("uninstall command") { + let appId = 12345 + let app = MockSoftwareProduct( + appName: "Some App", + bundlePath: "/tmp/Some.app", + itemIdentifier: NSNumber(value: appId) + ) + let mockLibrary = MockAppLibrary() + let uninstall = UninstallCommand(appLibrary: mockLibrary) + + context("dry run") { + let options = UninstallCommand.Options(appId: appId, dryRun: true) + + it("can't remove a missing app") { + mockLibrary.apps = [] + + let result = uninstall.run(options) + expect(result).to(beFailure { error in + expect(error) == .notInstalled + }) + } + it("finds an app") { + mockLibrary.apps.append(app) + + let result = uninstall.run(options) + expect(result).to(beSuccess()) + } + } + context("wet run") { + let options = UninstallCommand.Options(appId: appId, dryRun: false) + + it("can't remove a missing app") { + mockLibrary.apps = [] + + let result = uninstall.run(options) + expect(result).to(beFailure { error in + expect(error) == .notInstalled + }) + } + it("removes an app") { + mockLibrary.apps.append(app) + + let result = uninstall.run(options) + expect(result).to(beSuccess()) + } + it("fails if there is a problem with the trash command") { + mockLibrary.apps = [] + var brokenUninstall = app // make mutable copy + brokenUninstall.bundlePath = "/dev/null" + mockLibrary.apps.append(brokenUninstall) + + let result = uninstall.run(options) + expect(result).to(beFailure { error in + expect(error) == .uninstallFailed + }) + } + } + } + } +} diff --git a/MasKitTests/MASErrorTestCase.swift b/MasKitTests/MASErrorTestCase.swift index a562f61..3684ba0 100644 --- a/MasKitTests/MASErrorTestCase.swift +++ b/MasKitTests/MASErrorTestCase.swift @@ -98,4 +98,14 @@ class MASErrorTestCase: XCTestCase { error = .noSearchResultsFound XCTAssertEqual(error.description, "No results found") } + + func testNotInstalled() { + error = .notInstalled + XCTAssertEqual(error.description, "Not installed") + } + + func testUninstallFailed() { + error = .uninstallFailed + XCTAssertEqual(error.description, "Uninstall failed") + } } diff --git a/MasKitTests/Mocks/MockAppLibrary.swift b/MasKitTests/Mocks/MockAppLibrary.swift new file mode 100644 index 0000000..920149e --- /dev/null +++ b/MasKitTests/Mocks/MockAppLibrary.swift @@ -0,0 +1,30 @@ +// +// MockAppLibrary.swift +// MasKitTests +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import MasKit + +class MockAppLibrary: AppLibrary { + var apps = [SoftwareProduct]() + + func installedApp(appId: UInt64) -> SoftwareProduct? { + return apps.first { $0.itemIdentifier == NSNumber(value: appId) } + } + + func uninstallApp(app: SoftwareProduct) throws { + if !apps.contains(where: { (product) -> Bool in + return app.itemIdentifier == product.itemIdentifier + }) { throw MASError.notInstalled } + + // Special case for testing where we pretend the trash command failed + if app.bundlePath == "/dev/null" { + throw MASError.uninstallFailed + } + + // Success is the default, watch out for false positives! + } +} diff --git a/MasKitTests/Mocks/MockSoftwareProduct.swift b/MasKitTests/Mocks/MockSoftwareProduct.swift new file mode 100644 index 0000000..9c54936 --- /dev/null +++ b/MasKitTests/Mocks/MockSoftwareProduct.swift @@ -0,0 +1,15 @@ +// +// MockSoftwareProduct.swift +// MasKitTests +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import MasKit + +struct MockSoftwareProduct: SoftwareProduct { + var appName: String + var bundlePath: String + var itemIdentifier: NSNumber +} diff --git a/MasKitTests/MockURLSession.swift b/MasKitTests/Mocks/MockURLSession.swift similarity index 100% rename from MasKitTests/MockURLSession.swift rename to MasKitTests/Mocks/MockURLSession.swift diff --git a/MasKitTests/ResultPredicates.swift b/MasKitTests/ResultPredicates.swift new file mode 100644 index 0000000..40d4bfb --- /dev/null +++ b/MasKitTests/ResultPredicates.swift @@ -0,0 +1,34 @@ +// +// ResultPreticates.swift +// MasKitTests +// +// Created by Ben Chatelain on 12/27/18. +// Copyright © 2018 mas-cli. All rights reserved. +// + +@testable import MasKit +import Result +import Nimble + +/// Nimble predicate for result enum success case, no associated value +func beSuccess() -> Predicate> { + return Predicate.define("be ") { expression, message in + if let actual = try expression.evaluate(), + case .success = actual { + return PredicateResult(status: .matches, message: message) + } + return PredicateResult(status: .fail, message: message) + } +} + +/// Nimble predicate for result enum failure with associated error +func beFailure(test: @escaping (MASError) -> Void = { _ in }) -> Predicate> { + return Predicate.define("be ") { expression, message in + if let actual = try expression.evaluate(), + case let .failure(error) = actual { + test(error) + return PredicateResult(status: .matches, message: message) + } + return PredicateResult(status: .fail, message: message) + } +} diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 5757d7d..470ba6e 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -21,6 +21,16 @@ B5552937219A23FF00ACB4CA /* Quick.framework in Copy Carthage Frameworks */ = {isa = PBXBuildFile; fileRef = 90CB406A213F4DDD0044E445 /* Quick.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; B5793E29219BDD4800135B39 /* JSON in Resources */ = {isa = PBXBuildFile; fileRef = B5793E28219BDD4800135B39 /* JSON */; }; B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5793E2A219BE0CD00135B39 /* MockURLSession.swift */; }; + B594B12021D53A8200F3AC59 /* Uninstall.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B11F21D53A8200F3AC59 /* Uninstall.swift */; }; + B594B12221D5416100F3AC59 /* ListCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12121D5416100F3AC59 /* ListCommandSpec.swift */; }; + B594B12521D580BB00F3AC59 /* UninstallCommandSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12421D580BB00F3AC59 /* UninstallCommandSpec.swift */; }; + B594B12721D5825800F3AC59 /* AppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12621D5825800F3AC59 /* AppLibrary.swift */; }; + B594B12921D5831D00F3AC59 /* SoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */; }; + B594B12B21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12A21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift */; }; + B594B12E21D5850700F3AC59 /* MockAppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */; }; + B594B13021D5855D00F3AC59 /* MasAppLibrary.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */; }; + B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B13121D5876200F3AC59 /* ResultPredicates.swift */; }; + B594B13421D5897100F3AC59 /* MockSoftwareProduct.swift in Sources */ = {isa = PBXBuildFile; fileRef = B594B13321D5897100F3AC59 /* MockSoftwareProduct.swift */; }; ED031A7C1B5127C00097692E /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED031A7B1B5127C00097692E /* main.swift */; }; F83213892173D3E1008BA8A0 /* CKAccountStore.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719B20F2EC4500F56FDC /* CKAccountStore.h */; }; F832138A2173D3E1008BA8A0 /* CKDownloadQueue.h in Headers */ = {isa = PBXBuildFile; fileRef = F8FB719C20F2EC4500F56FDC /* CKDownloadQueue.h */; }; @@ -156,6 +166,16 @@ B555292C219A1FE700ACB4CA /* SearchSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchSpec.swift; sourceTree = ""; }; B5793E28219BDD4800135B39 /* JSON */ = {isa = PBXFileReference; lastKnownFileType = folder; path = JSON; sourceTree = ""; }; B5793E2A219BE0CD00135B39 /* MockURLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLSession.swift; sourceTree = ""; }; + B594B11F21D53A8200F3AC59 /* Uninstall.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Uninstall.swift; sourceTree = ""; }; + B594B12121D5416100F3AC59 /* ListCommandSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListCommandSpec.swift; sourceTree = ""; }; + B594B12421D580BB00F3AC59 /* UninstallCommandSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UninstallCommandSpec.swift; sourceTree = ""; }; + B594B12621D5825800F3AC59 /* AppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppLibrary.swift; sourceTree = ""; }; + B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SoftwareProduct.swift; sourceTree = ""; }; + B594B12A21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CKSoftwareProduct+SoftwareProduct.swift"; sourceTree = ""; }; + B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockAppLibrary.swift; sourceTree = ""; }; + B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasAppLibrary.swift; sourceTree = ""; }; + B594B13121D5876200F3AC59 /* ResultPredicates.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResultPredicates.swift; sourceTree = ""; }; + B594B13321D5897100F3AC59 /* MockSoftwareProduct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockSoftwareProduct.swift; sourceTree = ""; }; ED031A781B5127C00097692E /* mas */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = mas; sourceTree = BUILT_PRODUCTS_DIR; }; ED031A7B1B5127C00097692E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; ED0F237E1B87522400AE40CD /* Install.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Install.swift; sourceTree = ""; }; @@ -167,14 +187,13 @@ ED0F238C1B8756E600AE40CD /* MASError.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MASError.swift; sourceTree = ""; }; ED0F238F1B87A56F00AE40CD /* ISStoreAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ISStoreAccount.swift; sourceTree = ""; }; EDA3BE511B8B84AF00C18D70 /* SSPurchase.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SSPurchase.swift; sourceTree = ""; }; - EDB6CE8A1BAEB95100648B4D /* mas-cli-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "mas-cli-Info.plist"; sourceTree = ""; }; + EDB6CE8A1BAEB95100648B4D /* mas-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "mas-Info.plist"; sourceTree = ""; }; EDB6CE8B1BAEC3D400648B4D /* Version.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; EDC90B641C70045E0019E396 /* SignIn.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignIn.swift; sourceTree = ""; }; EDCBF9521D89AC6F000039C6 /* Reset.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Reset.swift; sourceTree = ""; }; EDCBF9541D89CFC7000039C6 /* Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Utilities.swift; sourceTree = ""; }; EDD3B3621C34709400B56B88 /* Upgrade.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Upgrade.swift; sourceTree = ""; }; EDE296521C700F4300554778 /* SignOut.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SignOut.swift; sourceTree = ""; }; - EDEAA12C1B51CF8000F2FC3F /* mas-cli-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "mas-cli-Bridging-Header.h"; sourceTree = ""; }; F8242D8020746A510026DF35 /* StoreAccount.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StoreAccount.swift; sourceTree = ""; }; F83213A42173EF75008BA8A0 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; F83213A52173EF75008BA8A0 /* StoreFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreFoundation.framework; path = ../../../../../../../System/Library/PrivateFrameworks/StoreFoundation.framework; sourceTree = ""; }; @@ -260,12 +279,32 @@ path = Carthage/Build/Mac; sourceTree = ""; }; + B594B12321D57FF300F3AC59 /* Commands */ = { + isa = PBXGroup; + children = ( + B594B12121D5416100F3AC59 /* ListCommandSpec.swift */, + B594B12421D580BB00F3AC59 /* UninstallCommandSpec.swift */, + B555292C219A1FE700ACB4CA /* SearchSpec.swift */, + ); + path = Commands; + sourceTree = ""; + }; + B594B12C21D584E800F3AC59 /* Mocks */ = { + isa = PBXGroup; + children = ( + B594B12D21D5850700F3AC59 /* MockAppLibrary.swift */, + B594B13321D5897100F3AC59 /* MockSoftwareProduct.swift */, + B5793E2A219BE0CD00135B39 /* MockURLSession.swift */, + ); + path = Mocks; + sourceTree = ""; + }; ED031A6F1B5127C00097692E = { isa = PBXGroup; children = ( - ED031A7A1B5127C00097692E /* App */, 90CB4068213F4DDD0044E445 /* Carthage */, EDFC76381B642A2E00D0DBD7 /* Frameworks */, + ED031A7A1B5127C00097692E /* mas */, F8FB715320F2B41400F56FDC /* MasKit */, F8FB715E20F2B41400F56FDC /* MasKitTests */, F8FB719920F2EC4500F56FDC /* PrivateFrameworks */, @@ -283,19 +322,13 @@ name = Products; sourceTree = ""; }; - ED031A7A1B5127C00097692E /* App */ = { + ED031A7A1B5127C00097692E /* mas */ = { isa = PBXGroup; children = ( - ED0F238E1B87A54700AE40CD /* AppStore */, - ED0F23801B87524700AE40CD /* Commands */, ED031A7B1B5127C00097692E /* main.swift */, - EDEAA12C1B51CF8000F2FC3F /* mas-cli-Bridging-Header.h */, - EDB6CE8A1BAEB95100648B4D /* mas-cli-Info.plist */, - ED0F238C1B8756E600AE40CD /* MASError.swift */, - 693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */, - EDCBF9541D89CFC7000039C6 /* Utilities.swift */, + EDB6CE8A1BAEB95100648B4D /* mas-Info.plist */, ); - path = App; + path = mas; sourceTree = ""; }; ED0F23801B87524700AE40CD /* Commands */ = { @@ -311,17 +344,18 @@ 693A98981CBFFA760004D3B4 /* Search.swift */, EDC90B641C70045E0019E396 /* SignIn.swift */, EDE296521C700F4300554778 /* SignOut.swift */, + B594B11F21D53A8200F3AC59 /* Uninstall.swift */, EDD3B3621C34709400B56B88 /* Upgrade.swift */, EDB6CE8B1BAEC3D400648B4D /* Version.swift */, ); - name = Commands; - path = App/Commands; - sourceTree = SOURCE_ROOT; + path = Commands; + sourceTree = ""; }; ED0F238E1B87A54700AE40CD /* AppStore */ = { isa = PBXGroup; children = ( 4913269A1F48921D0010EB86 /* CKSoftwareMap+AppLookup.swift */, + B594B12A21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift */, ED0F238A1B87569C00AE40CD /* Downloader.swift */, ED0F238F1B87A56F00AE40CD /* ISStoreAccount.swift */, ED0F23881B87543D00AE40CD /* PurchaseDownloadObserver.swift */, @@ -344,8 +378,16 @@ F8FB715320F2B41400F56FDC /* MasKit */ = { isa = PBXGroup; children = ( + B594B12621D5825800F3AC59 /* AppLibrary.swift */, + ED0F238E1B87A54700AE40CD /* AppStore */, + ED0F23801B87524700AE40CD /* Commands */, F8FB715520F2B41400F56FDC /* Info.plist */, + B594B12F21D5855D00F3AC59 /* MasAppLibrary.swift */, + ED0F238C1B8756E600AE40CD /* MASError.swift */, F8FB715420F2B41400F56FDC /* MasKit.h */, + 693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */, + B594B12821D5831D00F3AC59 /* SoftwareProduct.swift */, + EDCBF9541D89CFC7000039C6 /* Utilities.swift */, ); path = MasKit; sourceTree = ""; @@ -353,11 +395,12 @@ F8FB715E20F2B41400F56FDC /* MasKitTests */ = { isa = PBXGroup; children = ( + B594B12321D57FF300F3AC59 /* Commands */, F8FB716120F2B41400F56FDC /* Info.plist */, B5793E28219BDD4800135B39 /* JSON */, B555292A219A1CB200ACB4CA /* MASErrorTestCase.swift */, - B5793E2A219BE0CD00135B39 /* MockURLSession.swift */, - B555292C219A1FE700ACB4CA /* SearchSpec.swift */, + B594B12C21D584E800F3AC59 /* Mocks */, + B594B13121D5876200F3AC59 /* ResultPredicates.swift */, ); path = MasKitTests; sourceTree = ""; @@ -592,6 +635,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + B594B12B21D5837200F3AC59 /* CKSoftwareProduct+SoftwareProduct.swift in Sources */, F8FB716F20F2B4DD00F56FDC /* Account.swift in Sources */, F8FB716920F2B4DD00F56FDC /* CKSoftwareMap+AppLookup.swift in Sources */, F8FB716A20F2B4DD00F56FDC /* Downloader.swift in Sources */, @@ -600,12 +644,16 @@ F8FB716B20F2B4DD00F56FDC /* ISStoreAccount.swift in Sources */, F8FB717220F2B4DD00F56FDC /* List.swift in Sources */, F8FB717320F2B4DD00F56FDC /* Lucky.swift in Sources */, + B594B12721D5825800F3AC59 /* AppLibrary.swift in Sources */, F8FB717B20F2B4DD00F56FDC /* MASError.swift in Sources */, F8FB717C20F2B4DD00F56FDC /* NSURLSession+Synchronous.swift in Sources */, F8FB717420F2B4DD00F56FDC /* Outdated.swift in Sources */, + B594B12921D5831D00F3AC59 /* SoftwareProduct.swift in Sources */, F8FB716C20F2B4DD00F56FDC /* PurchaseDownloadObserver.swift in Sources */, F8FB717520F2B4DD00F56FDC /* Reset.swift in Sources */, F8FB717620F2B4DD00F56FDC /* Search.swift in Sources */, + B594B13021D5855D00F3AC59 /* MasAppLibrary.swift in Sources */, + B594B12021D53A8200F3AC59 /* Uninstall.swift in Sources */, F8FB717720F2B4DD00F56FDC /* SignIn.swift in Sources */, F8FB717820F2B4DD00F56FDC /* SignOut.swift in Sources */, F8FB716D20F2B4DD00F56FDC /* SSPurchase.swift in Sources */, @@ -622,7 +670,12 @@ files = ( B5793E2B219BE0CD00135B39 /* MockURLSession.swift in Sources */, B555292B219A1CB200ACB4CA /* MASErrorTestCase.swift in Sources */, + B594B13421D5897100F3AC59 /* MockSoftwareProduct.swift in Sources */, + B594B12221D5416100F3AC59 /* ListCommandSpec.swift in Sources */, + B594B12521D580BB00F3AC59 /* UninstallCommandSpec.swift in Sources */, B555292D219A1FE700ACB4CA /* SearchSpec.swift in Sources */, + B594B12E21D5850700F3AC59 /* MockAppLibrary.swift in Sources */, + B594B13221D5876200F3AC59 /* ResultPredicates.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -696,7 +749,6 @@ "$(inherited)", "$(SRCROOT)/App/PrivateHeaders", ); - INFOPLIST_FILE = "App/mas-cli-Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; @@ -756,7 +808,6 @@ "$(inherited)", "$(SRCROOT)/App/PrivateHeaders", ); - INFOPLIST_FILE = "App/mas-cli-Info.plist"; MACOSX_DEPLOYMENT_TARGET = 10.9; MTL_ENABLE_DEBUG_INFO = NO; PRODUCT_BUNDLE_IDENTIFIER = "com.mphys.mas-cli"; @@ -775,13 +826,14 @@ "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/Mac", ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + INFOPLIST_FILE = "mas/mas-Info.plist"; INSTALL_PATH = /bin; LD_RUNPATH_SEARCH_PATHS = "@executable_path/. @executable_path/MasKit.framework/Versions/Current/Frameworks /usr/local/Frameworks /usr/local/Frameworks/MasKit.framework/Versions/Current/Frameworks $(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.mphys.mas-cli"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; SWIFT_FORCE_STATIC_LINK_STDLIB = NO; - SWIFT_OBJC_BRIDGING_HEADER = "App/mas-cli-Bridging-Header.h"; SWIFT_VERSION = 4.2; }; name = Debug; @@ -796,13 +848,14 @@ "$(inherited)", "$(PROJECT_DIR)/Carthage/Build/Mac", ); + GCC_SYMBOLS_PRIVATE_EXTERN = NO; + INFOPLIST_FILE = "mas/mas-Info.plist"; INSTALL_PATH = /bin; LD_RUNPATH_SEARCH_PATHS = "@executable_path/. @executable_path/MasKit.framework/Versions/Current/Frameworks /usr/local/Frameworks /usr/local/Frameworks/MasKit.framework/Versions/Current/Frameworks $(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.mphys.mas-cli"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_FORCE_DYNAMIC_LINK_STDLIB = YES; SWIFT_FORCE_STATIC_LINK_STDLIB = NO; - SWIFT_OBJC_BRIDGING_HEADER = "App/mas-cli-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; SWIFT_VERSION = 4.2; }; diff --git a/App/main.swift b/mas/main.swift similarity index 96% rename from App/main.swift rename to mas/main.swift index e0a26da..ad76be3 100644 --- a/App/main.swift +++ b/mas/main.swift @@ -28,6 +28,7 @@ registry.register(ResetCommand()) registry.register(SearchCommand()) registry.register(SignInCommand()) registry.register(SignOutCommand()) +registry.register(UninstallCommand()) registry.register(UpgradeCommand()) registry.register(VersionCommand()) registry.register(helpCommand) diff --git a/App/mas-cli-Info.plist b/mas/mas-Info.plist similarity index 100% rename from App/mas-cli-Info.plist rename to mas/mas-Info.plist