mirror of
https://github.com/mas-cli/mas
synced 2024-11-21 19:23:01 +00:00
commit
12832f293d
62 changed files with 226 additions and 216 deletions
|
@ -47,7 +47,8 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt
|
|||
}
|
||||
|
||||
// If the download failed due to network issues, try again. Otherwise, fail immediately.
|
||||
guard case MASError.downloadFailed(let downloadError) = error,
|
||||
guard
|
||||
case MASError.downloadFailed(let downloadError) = error,
|
||||
case NSURLErrorDomain = downloadError?.domain
|
||||
else {
|
||||
throw error
|
||||
|
|
|
@ -32,7 +32,7 @@ extension ISStoreAccount: StoreAccount {
|
|||
return .value(CKAccountStore.shared().primaryAccount)
|
||||
}
|
||||
|
||||
static func signIn(username: String, password: String, systemDialog: Bool) -> Promise<ISStoreAccount> {
|
||||
static func signIn(appleID: String, password: String, systemDialog: Bool) -> Promise<ISStoreAccount> {
|
||||
// swift-format-ignore: UseEarlyExits
|
||||
if #available(macOS 10.13, *) {
|
||||
// Signing in is no longer possible as of High Sierra.
|
||||
|
@ -44,7 +44,7 @@ extension ISStoreAccount: StoreAccount {
|
|||
primaryAccount
|
||||
.then { account -> Promise<ISStoreAccount> in
|
||||
if account.isSignedIn {
|
||||
return Promise(error: MASError.alreadySignedIn(asAccountId: account.identifier))
|
||||
return Promise(error: MASError.alreadySignedIn(asAppleID: account.identifier))
|
||||
}
|
||||
|
||||
let password =
|
||||
|
@ -57,7 +57,7 @@ extension ISStoreAccount: StoreAccount {
|
|||
}
|
||||
|
||||
let context = ISAuthenticationContext(accountID: 0)
|
||||
context.appleIDOverride = username
|
||||
context.appleIDOverride = appleID
|
||||
|
||||
let signInPromise =
|
||||
Promise<ISStoreAccount> { seal in
|
||||
|
@ -77,7 +77,7 @@ extension ISStoreAccount: StoreAccount {
|
|||
}
|
||||
|
||||
context.demoMode = true
|
||||
context.demoAccountName = username
|
||||
context.demoAccountName = appleID
|
||||
context.demoAccountPassword = password
|
||||
context.demoAutologinMode = true
|
||||
|
||||
|
|
|
@ -20,7 +20,8 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
|
|||
}
|
||||
|
||||
func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
guard
|
||||
download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status
|
||||
else {
|
||||
return
|
||||
|
@ -42,7 +43,8 @@ class PurchaseDownloadObserver: NSObject, CKDownloadQueueObserver {
|
|||
}
|
||||
|
||||
func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) {
|
||||
guard download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
guard
|
||||
download.metadata.itemIdentifier == purchase.itemIdentifier,
|
||||
let status = download.status
|
||||
else {
|
||||
return
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
struct Account: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Prints the primary account Apple ID"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Opens app page on MAS Preview. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Home: ParsableCommand {
|
||||
|
@ -21,12 +21,12 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws {
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(appID: appID).wait() else {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Displays app details. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Info: ParsableCommand {
|
||||
|
@ -22,12 +22,12 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(storeSearch: MasStoreSearch())
|
||||
try run(searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(storeSearch: StoreSearch) throws {
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(appID: appID).wait() else {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Installs previously purchased apps from the Mac App Store.
|
||||
struct Install: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
@ -23,7 +23,7 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which lists all installed apps.
|
||||
struct List: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
@ -17,7 +17,7 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which installs the first search result.
|
||||
///
|
||||
/// This is handy as many MAS titles can be long with embedded keywords.
|
||||
|
@ -21,18 +21,18 @@ extension Mas {
|
|||
@Flag(help: "force reinstall")
|
||||
var force = false
|
||||
@Argument(help: "the app name to install")
|
||||
var appName: String
|
||||
var searchTerm: String
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
var appID: AppID?
|
||||
|
||||
do {
|
||||
let results = try storeSearch.search(for: appName).wait()
|
||||
let results = try searcher.search(for: searchTerm).wait()
|
||||
guard let result = results.first else {
|
||||
printError("No results found")
|
||||
throw MASError.noSearchResultsFound
|
||||
|
|
|
@ -11,12 +11,12 @@ import Foundation
|
|||
|
||||
private let masScheme = "macappstore"
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Opens app page in MAS app. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Open: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Opens app page in AppStore.app"
|
||||
abstract: "Opens app page in 'App Store.app'"
|
||||
)
|
||||
|
||||
@Argument(help: "the app ID")
|
||||
|
@ -24,10 +24,10 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws {
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let appID else {
|
||||
// If no app ID is given, just open the MAS GUI app
|
||||
|
@ -35,7 +35,7 @@ extension Mas {
|
|||
return
|
||||
}
|
||||
|
||||
guard let result = try storeSearch.lookup(appID: appID).wait() else {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,7 @@ import ArgumentParser
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which displays a list of installed apps which have available updates
|
||||
/// ready to be installed from the Mac App Store.
|
||||
struct Outdated: ParsableCommand {
|
||||
|
@ -23,15 +23,15 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
_ = try when(
|
||||
fulfilled:
|
||||
appLibrary.installedApps.map { installedApp in
|
||||
firstly {
|
||||
storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.done { storeApp in
|
||||
guard let storeApp else {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
struct Purchase: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Purchase and download free apps from the Mac App Store"
|
||||
|
@ -20,7 +20,7 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Kills several macOS processes as a means to reset the app store.
|
||||
struct Reset: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Search the Mac App Store using the iTunes Search API.
|
||||
///
|
||||
/// See - https://performance-partners.apple.com/search-api
|
||||
|
@ -20,15 +20,15 @@ extension Mas {
|
|||
@Flag(help: "Show price of found apps")
|
||||
var price = false
|
||||
@Argument(help: "the app name to search")
|
||||
var appName: String
|
||||
var searchTerm: String
|
||||
|
||||
func run() throws {
|
||||
try run(storeSearch: MasStoreSearch())
|
||||
try run(searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(storeSearch: StoreSearch) throws {
|
||||
func run(searcher: AppStoreSearcher) throws {
|
||||
do {
|
||||
let results = try storeSearch.search(for: appName).wait()
|
||||
let results = try searcher.search(for: searchTerm).wait()
|
||||
if results.isEmpty {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import StoreFoundation
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
struct SignIn: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signin",
|
||||
|
@ -19,14 +19,14 @@ extension Mas {
|
|||
@Flag(help: "Complete login with graphical dialog")
|
||||
var dialog = false
|
||||
@Argument(help: "Apple ID")
|
||||
var username: String
|
||||
var appleID: String
|
||||
@Argument(help: "Password")
|
||||
var password: String = ""
|
||||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
do {
|
||||
_ = try ISStoreAccount.signIn(username: username, password: password, systemDialog: dialog).wait()
|
||||
_ = try ISStoreAccount.signIn(appleID: appleID, password: password, systemDialog: dialog).wait()
|
||||
} catch {
|
||||
throw error as? MASError ?? MASError.signInFailed(error: error as NSError)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import CommerceKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
struct SignOut: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
commandName: "signout",
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
import ArgumentParser
|
||||
import Foundation
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which uninstalls apps managed by the Mac App Store.
|
||||
struct Uninstall: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
@ -24,7 +24,7 @@ extension Mas {
|
|||
|
||||
/// Runs the uninstall command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary())
|
||||
try run(appLibrary: SoftwareMapAppLibrary())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary) throws {
|
||||
|
|
|
@ -10,7 +10,7 @@ import ArgumentParser
|
|||
import Foundation
|
||||
import PromiseKit
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which upgrades apps with new versions available in the Mac App Store.
|
||||
struct Upgrade: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
@ -22,13 +22,13 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(appLibrary: MasAppLibrary(), storeSearch: MasStoreSearch())
|
||||
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
|
||||
}
|
||||
|
||||
func run(appLibrary: AppLibrary, storeSearch: StoreSearch) throws {
|
||||
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
|
||||
let apps: [(installedApp: SoftwareProduct, storeApp: SearchResult)]
|
||||
do {
|
||||
apps = try findOutdatedApps(appLibrary: appLibrary, storeSearch: storeSearch)
|
||||
apps = try findOutdatedApps(appLibrary: appLibrary, searcher: searcher)
|
||||
} catch {
|
||||
throw error as? MASError ?? .searchFailed
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ extension Mas {
|
|||
|
||||
private func findOutdatedApps(
|
||||
appLibrary: AppLibrary,
|
||||
storeSearch: StoreSearch
|
||||
searcher: AppStoreSearcher
|
||||
) throws -> [(SoftwareProduct, SearchResult)] {
|
||||
let apps =
|
||||
appIDs.isEmpty
|
||||
|
@ -71,7 +71,7 @@ extension Mas {
|
|||
let promises = apps.map { installedApp in
|
||||
// only upgrade apps whose local version differs from the store version
|
||||
firstly {
|
||||
storeSearch.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
|
||||
}
|
||||
.map { result -> (SoftwareProduct, SearchResult)? in
|
||||
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Opens vendor's app page in a browser. Uses the iTunes Lookup API:
|
||||
/// https://performance-partners.apple.com/search-api
|
||||
struct Vendor: ParsableCommand {
|
||||
|
@ -21,12 +21,12 @@ extension Mas {
|
|||
|
||||
/// Runs the command.
|
||||
func run() throws {
|
||||
try run(storeSearch: MasStoreSearch(), openCommand: OpenSystemCommand())
|
||||
try run(searcher: ITunesSearchAppStoreSearcher(), openCommand: OpenSystemCommand())
|
||||
}
|
||||
|
||||
func run(storeSearch: StoreSearch, openCommand: ExternalCommand) throws {
|
||||
func run(searcher: AppStoreSearcher, openCommand: ExternalCommand) throws {
|
||||
do {
|
||||
guard let result = try storeSearch.lookup(appID: appID).wait() else {
|
||||
guard let result = try searcher.lookup(appID: appID).wait() else {
|
||||
throw MASError.noSearchResultsFound
|
||||
}
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import ArgumentParser
|
||||
|
||||
extension Mas {
|
||||
extension MAS {
|
||||
/// Command which displays the version of the mas tool.
|
||||
struct Version: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// StoreSearch.swift
|
||||
// AppStoreSearcher.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
|
@ -10,9 +10,9 @@ import Foundation
|
|||
import PromiseKit
|
||||
|
||||
/// Protocol for searching the MAS catalog.
|
||||
protocol StoreSearch {
|
||||
protocol AppStoreSearcher {
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?>
|
||||
func search(for appName: String) -> Promise<[SearchResult]>
|
||||
func search(for searchTerm: String) -> Promise<[SearchResult]>
|
||||
}
|
||||
|
||||
enum Entity: String {
|
||||
|
@ -37,7 +37,7 @@ private enum URLAction {
|
|||
}
|
||||
|
||||
// MARK: - Common methods
|
||||
extension StoreSearch {
|
||||
extension AppStoreSearcher {
|
||||
/// Builds the search URL for an app.
|
||||
///
|
||||
/// - Parameters:
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MasStoreSearch.swift
|
||||
// ITunesSearchAppStoreSearcher.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/29/18.
|
||||
|
@ -12,7 +12,7 @@ import Regex
|
|||
import Version
|
||||
|
||||
/// Manages searching the MAS catalog through the iTunes Search and Lookup APIs.
|
||||
class MasStoreSearch: StoreSearch {
|
||||
class ITunesSearchAppStoreSearcher: AppStoreSearcher {
|
||||
private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#)
|
||||
|
||||
// CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed
|
||||
|
@ -77,15 +77,16 @@ class MasStoreSearch: StoreSearch {
|
|||
return .value(nil)
|
||||
}
|
||||
|
||||
guard let pageUrl = URL(string: result.trackViewUrl) else {
|
||||
guard let pageURL = URL(string: result.trackViewUrl) else {
|
||||
return .value(result)
|
||||
}
|
||||
|
||||
return firstly {
|
||||
self.scrapeAppStoreVersion(pageUrl)
|
||||
self.scrapeAppStoreVersion(pageURL)
|
||||
}
|
||||
.map { pageVersion in
|
||||
guard let pageVersion,
|
||||
guard
|
||||
let pageVersion,
|
||||
let searchVersion = Version(tolerant: result.version),
|
||||
pageVersion > searchVersion
|
||||
else {
|
||||
|
@ -120,12 +121,13 @@ class MasStoreSearch: StoreSearch {
|
|||
/// Scrape the app version from the App Store webpage at the given URL.
|
||||
///
|
||||
/// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API.
|
||||
private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise<Version?> {
|
||||
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageUrl)
|
||||
networkManager.loadData(from: pageURL)
|
||||
}
|
||||
.map { data in
|
||||
guard let html = String(data: data, encoding: .utf8),
|
||||
guard
|
||||
let html = String(data: data, encoding: .utf8),
|
||||
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
let version = Version(tolerant: capture)
|
||||
else {
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MasAppLibrary.swift
|
||||
// SoftwareMapAppLibrary.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
|
@ -10,7 +10,7 @@ import CommerceKit
|
|||
import ScriptingBridge
|
||||
|
||||
/// Utility for managing installed apps.
|
||||
class MasAppLibrary: AppLibrary {
|
||||
class SoftwareMapAppLibrary: AppLibrary {
|
||||
/// CommerceKit's singleton manager of installed software.
|
||||
private let softwareMap: SoftwareMap
|
||||
|
||||
|
@ -28,10 +28,10 @@ class MasAppLibrary: AppLibrary {
|
|||
|
||||
/// Finds an app using a bundle identifier.
|
||||
///
|
||||
/// - Parameter bundleId: Bundle identifier of app.
|
||||
/// - Returns: Software Product of app if found; nil otherwise.
|
||||
func installedApp(forBundleId bundleId: String) -> SoftwareProduct? {
|
||||
softwareMap.product(for: bundleId)
|
||||
/// - Parameter bundleID: Bundle identifier of app.
|
||||
/// - Returns: `SoftwareProduct` for app if found; `nil` otherwise.
|
||||
func installedApp(forBundleID bundleID: String) -> SoftwareProduct? {
|
||||
softwareMap.product(for: bundleID)
|
||||
}
|
||||
|
||||
/// Uninstalls all apps located at any of the elements of `appPaths`.
|
|
@ -18,7 +18,7 @@ enum MASError: Error, Equatable {
|
|||
case notSignedIn
|
||||
case noPasswordProvided
|
||||
case signInFailed(error: NSError?)
|
||||
case alreadySignedIn(asAccountId: String)
|
||||
case alreadySignedIn(asAppleID: String)
|
||||
|
||||
case purchaseFailed(error: NSError?)
|
||||
case downloadFailed(error: NSError?)
|
||||
|
@ -63,8 +63,8 @@ extension MASError: CustomStringConvertible {
|
|||
return "Sign in failed: \(error.localizedDescription)"
|
||||
}
|
||||
return "Sign in failed"
|
||||
case .alreadySignedIn(let accountId):
|
||||
return "Already signed in as \(accountId)"
|
||||
case .alreadySignedIn(let appleID):
|
||||
return "Already signed in as \(appleID)"
|
||||
case .purchaseFailed(let error):
|
||||
if let error {
|
||||
return "Download request failed: \(error.localizedDescription)"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// Mas.swift
|
||||
// MAS.swift
|
||||
// mas
|
||||
//
|
||||
// Created by Chris Araman on 4/22/21.
|
||||
|
@ -11,7 +11,7 @@ import Foundation
|
|||
import PromiseKit
|
||||
|
||||
@main
|
||||
struct Mas: ParsableCommand {
|
||||
struct MAS: ParsableCommand {
|
||||
static let configuration = CommandConfiguration(
|
||||
abstract: "Mac App Store command-line interface",
|
||||
subcommands: [
|
|
@ -45,7 +45,8 @@ extension SoftwareProduct {
|
|||
|
||||
// The App Store does not enforce semantic versioning, but we assume most apps follow versioning
|
||||
// schemes that increase numerically over time.
|
||||
guard let semanticBundleVersion = Version(tolerant: bundleVersion),
|
||||
guard
|
||||
let semanticBundleVersion = Version(tolerant: bundleVersion),
|
||||
let semanticAppStoreVersion = Version(tolerant: storeApp.version)
|
||||
else {
|
||||
// If a version string can't be parsed as a Semantic Version, our best effort is to check for
|
||||
|
|
|
@ -15,13 +15,13 @@ import Quick
|
|||
public class AccountSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
// account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#known-issues
|
||||
describe("Account command") {
|
||||
it("displays active account") {
|
||||
expect {
|
||||
try Mas.Account.parse([]).run()
|
||||
try MAS.Account.parse([]).run()
|
||||
}
|
||||
.to(throwError(MASError.notSupported))
|
||||
}
|
||||
|
|
|
@ -13,25 +13,25 @@ import Quick
|
|||
|
||||
public class HomeSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let searcher = MockAppStoreSearcher()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("home command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try Mas.Home.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Home.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try Mas.Home.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Home.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
|
@ -41,10 +41,10 @@ public class HomeSpec: QuickSpec {
|
|||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
storeSearch.apps[mockResult.trackId] = mockResult
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try Mas.Home.parse([String(mockResult.trackId)])
|
||||
.run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Home.parse([String(mockResult.trackId)])
|
||||
.run(searcher: searcher, openCommand: openCommand)
|
||||
return openCommand.arguments
|
||||
}
|
||||
== [mockResult.trackViewUrl]
|
||||
|
|
|
@ -14,24 +14,24 @@ import Quick
|
|||
|
||||
public class InfoSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let storeSearch = StoreSearchMock()
|
||||
let searcher = MockAppStoreSearcher()
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("Info command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try Mas.Info.parse(["--", "-999"]).run(storeSearch: storeSearch)
|
||||
try MAS.Info.parse(["--", "-999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try Mas.Info.parse(["999"]).run(storeSearch: storeSearch)
|
||||
try MAS.Info.parse(["999"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
|
@ -47,10 +47,10 @@ public class InfoSpec: QuickSpec {
|
|||
trackViewUrl: "https://awesome.app",
|
||||
version: "1.0"
|
||||
)
|
||||
storeSearch.apps[mockResult.trackId] = mockResult
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try captureStream(stdout) {
|
||||
try Mas.Info.parse([String(mockResult.trackId)]).run(storeSearch: storeSearch)
|
||||
try MAS.Info.parse([String(mockResult.trackId)]).run(searcher: searcher)
|
||||
}
|
||||
}
|
||||
== """
|
||||
|
|
|
@ -14,12 +14,12 @@ import Quick
|
|||
public class InstallSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
xdescribe("install command") {
|
||||
xit("installs apps") {
|
||||
expect {
|
||||
try Mas.Install.parse([]).run(appLibrary: AppLibraryMock())
|
||||
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ import Quick
|
|||
public class ListSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("list command") {
|
||||
it("lists apps") {
|
||||
expect {
|
||||
try captureStream(stderr) {
|
||||
try Mas.List.parse([]).run(appLibrary: AppLibraryMock())
|
||||
try MAS.List.parse([]).run(appLibrary: MockAppLibrary())
|
||||
}
|
||||
}
|
||||
== "Error: No installed apps found\n"
|
||||
|
|
|
@ -13,16 +13,16 @@ import Quick
|
|||
|
||||
public class LuckySpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json")
|
||||
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
xdescribe("lucky command") {
|
||||
xit("installs the first app matching a search") {
|
||||
expect {
|
||||
try Mas.Lucky.parse(["Slack"]).run(appLibrary: AppLibraryMock(), storeSearch: storeSearch)
|
||||
try MAS.Lucky.parse(["Slack"]).run(appLibrary: MockAppLibrary(), searcher: searcher)
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -14,25 +14,25 @@ import Quick
|
|||
|
||||
public class OpenSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let searcher = MockAppStoreSearcher()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("open command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try Mas.Open.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Open.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try Mas.Open.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Open.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
|
@ -42,17 +42,17 @@ public class OpenSpec: QuickSpec {
|
|||
trackViewUrl: "fakescheme://some/url",
|
||||
version: "0.0"
|
||||
)
|
||||
storeSearch.apps[mockResult.trackId] = mockResult
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try Mas.Open.parse([mockResult.trackId.description])
|
||||
.run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Open.parse([mockResult.trackId.description])
|
||||
.run(searcher: searcher, openCommand: openCommand)
|
||||
return openCommand.arguments
|
||||
}
|
||||
== ["macappstore://some/url"]
|
||||
}
|
||||
it("just opens MAS if no app specified") {
|
||||
expect {
|
||||
try Mas.Open.parse([]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Open.parse([]).run(searcher: searcher, openCommand: openCommand)
|
||||
return openCommand.arguments
|
||||
}
|
||||
== ["macappstore://"]
|
||||
|
|
|
@ -15,7 +15,7 @@ import Quick
|
|||
public class OutdatedSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("outdated command") {
|
||||
it("displays apps with pending updates") {
|
||||
|
@ -33,12 +33,12 @@ public class OutdatedSpec: QuickSpec {
|
|||
trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4",
|
||||
version: "1.28"
|
||||
)
|
||||
let mockStoreSearch = StoreSearchMock()
|
||||
mockStoreSearch.apps[mockSearchResult.trackId] = mockSearchResult
|
||||
let searcher = MockAppStoreSearcher()
|
||||
searcher.apps[mockSearchResult.trackId] = mockSearchResult
|
||||
|
||||
let mockAppLibrary = AppLibraryMock()
|
||||
let mockAppLibrary = MockAppLibrary()
|
||||
mockAppLibrary.installedApps.append(
|
||||
SoftwareProductMock(
|
||||
MockSoftwareProduct(
|
||||
appName: mockSearchResult.trackName,
|
||||
bundleIdentifier: mockSearchResult.bundleId,
|
||||
bundlePath: "/Applications/Bandwidth+.app",
|
||||
|
@ -48,7 +48,7 @@ public class OutdatedSpec: QuickSpec {
|
|||
)
|
||||
expect {
|
||||
try captureStream(stdout) {
|
||||
try Mas.Outdated.parse([]).run(appLibrary: mockAppLibrary, storeSearch: mockStoreSearch)
|
||||
try MAS.Outdated.parse([]).run(appLibrary: mockAppLibrary, searcher: searcher)
|
||||
}
|
||||
}
|
||||
== "490461369 Bandwidth+ (1.27 -> 1.28)\n"
|
||||
|
|
|
@ -14,12 +14,12 @@ import Quick
|
|||
public class PurchaseSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
xdescribe("purchase command") {
|
||||
xit("purchases apps") {
|
||||
expect {
|
||||
try Mas.Purchase.parse(["999"]).run(appLibrary: AppLibraryMock())
|
||||
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary())
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ import Quick
|
|||
public class ResetSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("reset command") {
|
||||
it("resets the App Store state") {
|
||||
expect {
|
||||
try Mas.Reset.parse([]).run()
|
||||
try MAS.Reset.parse([]).run()
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@ import Quick
|
|||
|
||||
public class SearchSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let storeSearch = StoreSearchMock()
|
||||
let searcher = MockAppStoreSearcher()
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("search command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
searcher.reset()
|
||||
}
|
||||
it("can find slack") {
|
||||
let mockResult = SearchResult(
|
||||
|
@ -30,17 +30,17 @@ public class SearchSpec: QuickSpec {
|
|||
trackViewUrl: "mas preview url",
|
||||
version: "0.0"
|
||||
)
|
||||
storeSearch.apps[mockResult.trackId] = mockResult
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try captureStream(stdout) {
|
||||
try Mas.Search.parse(["slack"]).run(storeSearch: storeSearch)
|
||||
try MAS.Search.parse(["slack"]).run(searcher: searcher)
|
||||
}
|
||||
}
|
||||
== " 1111 slack (0.0)\n"
|
||||
}
|
||||
it("fails when searching for nonexistent app") {
|
||||
expect {
|
||||
try Mas.Search.parse(["nonexistent"]).run(storeSearch: storeSearch)
|
||||
try MAS.Search.parse(["nonexistent"]).run(searcher: searcher)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
|
|
|
@ -15,13 +15,13 @@ import Quick
|
|||
public class SignInSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
// account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#known-issues
|
||||
describe("signin command") {
|
||||
it("signs in") {
|
||||
expect {
|
||||
try Mas.SignIn.parse(["", ""]).run()
|
||||
try MAS.SignIn.parse(["", ""]).run()
|
||||
}
|
||||
.to(throwError(MASError.notSupported))
|
||||
}
|
||||
|
|
|
@ -14,12 +14,12 @@ import Quick
|
|||
public class SignOutSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("signout command") {
|
||||
it("signs out") {
|
||||
expect {
|
||||
try Mas.SignOut.parse([]).run()
|
||||
try MAS.SignOut.parse([]).run()
|
||||
}
|
||||
.toNot(throwError())
|
||||
}
|
||||
|
|
|
@ -15,21 +15,21 @@ import Quick
|
|||
public class UninstallSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
xdescribe("uninstall command") {
|
||||
let appID: AppID = 12345
|
||||
let app = SoftwareProductMock(
|
||||
let app = MockSoftwareProduct(
|
||||
appName: "Some App",
|
||||
bundleIdentifier: "com.some.app",
|
||||
bundlePath: "/tmp/Some.app",
|
||||
bundleVersion: "1.0",
|
||||
itemIdentifier: NSNumber(value: appID)
|
||||
)
|
||||
let mockLibrary = AppLibraryMock()
|
||||
let mockLibrary = MockAppLibrary()
|
||||
|
||||
context("dry run") {
|
||||
let uninstall = try! Mas.Uninstall.parse(["--dry-run", String(appID)])
|
||||
let uninstall = try! MAS.Uninstall.parse(["--dry-run", String(appID)])
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
|
@ -51,7 +51,7 @@ public class UninstallSpec: QuickSpec {
|
|||
}
|
||||
}
|
||||
context("wet run") {
|
||||
let uninstall = try! Mas.Uninstall.parse([String(appID)])
|
||||
let uninstall = try! MAS.Uninstall.parse([String(appID)])
|
||||
|
||||
beforeEach {
|
||||
mockLibrary.reset()
|
||||
|
|
|
@ -15,14 +15,14 @@ import Quick
|
|||
public class UpgradeSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("upgrade command") {
|
||||
it("finds no upgrades") {
|
||||
expect {
|
||||
try captureStream(stderr) {
|
||||
try Mas.Upgrade.parse([])
|
||||
.run(appLibrary: AppLibraryMock(), storeSearch: StoreSearchMock())
|
||||
try MAS.Upgrade.parse([])
|
||||
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
|
||||
}
|
||||
}
|
||||
== "Warning: Nothing found to upgrade\n"
|
||||
|
|
|
@ -13,25 +13,25 @@ import Quick
|
|||
|
||||
public class VendorSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let storeSearch = StoreSearchMock()
|
||||
let openCommand = OpenSystemCommandMock()
|
||||
let searcher = MockAppStoreSearcher()
|
||||
let openCommand = MockOpenSystemCommand()
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("vendor command") {
|
||||
beforeEach {
|
||||
storeSearch.reset()
|
||||
searcher.reset()
|
||||
}
|
||||
it("fails to open app with invalid ID") {
|
||||
expect {
|
||||
try Mas.Vendor.parse(["--", "-999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError())
|
||||
}
|
||||
it("can't find app with unknown ID") {
|
||||
expect {
|
||||
try Mas.Vendor.parse(["999"]).run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Vendor.parse(["999"]).run(searcher: searcher, openCommand: openCommand)
|
||||
}
|
||||
.to(throwError(MASError.noSearchResultsFound))
|
||||
}
|
||||
|
@ -42,10 +42,10 @@ public class VendorSpec: QuickSpec {
|
|||
trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4",
|
||||
version: "0.0"
|
||||
)
|
||||
storeSearch.apps[mockResult.trackId] = mockResult
|
||||
searcher.apps[mockResult.trackId] = mockResult
|
||||
expect {
|
||||
try Mas.Vendor.parse([String(mockResult.trackId)])
|
||||
.run(storeSearch: storeSearch, openCommand: openCommand)
|
||||
try MAS.Vendor.parse([String(mockResult.trackId)])
|
||||
.run(searcher: searcher, openCommand: openCommand)
|
||||
return openCommand.arguments
|
||||
}
|
||||
== [mockResult.sellerUrl]
|
||||
|
|
|
@ -15,13 +15,13 @@ import Quick
|
|||
public class VersionSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("version command") {
|
||||
it("displays the current version") {
|
||||
expect {
|
||||
try captureStream(stdout) {
|
||||
try Mas.Version.parse([]).run()
|
||||
try MAS.Version.parse([]).run()
|
||||
}
|
||||
}
|
||||
== "\(Package.version)\n"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MasStoreSearchSpec.swift
|
||||
// ITunesSearchAppStoreSearcherSpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
|
@ -11,21 +11,21 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class MasStoreSearchSpec: QuickSpec {
|
||||
public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("url string") {
|
||||
it("contains the app name") {
|
||||
expect {
|
||||
MasStoreSearch().searchURL(for: "myapp", inCountry: "US")?.absoluteString
|
||||
ITunesSearchAppStoreSearcher().searchURL(for: "myapp", inCountry: "US")?.absoluteString
|
||||
}
|
||||
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp"
|
||||
}
|
||||
it("contains the encoded app name") {
|
||||
expect {
|
||||
MasStoreSearch().searchURL(for: "My App", inCountry: "US")?.absoluteString
|
||||
ITunesSearchAppStoreSearcher().searchURL(for: "My App", inCountry: "US")?.absoluteString
|
||||
}
|
||||
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App"
|
||||
}
|
||||
|
@ -33,11 +33,11 @@ public class MasStoreSearchSpec: QuickSpec {
|
|||
describe("store") {
|
||||
context("when searched") {
|
||||
it("can find slack") {
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json")
|
||||
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
expect {
|
||||
try storeSearch.search(for: "slack").wait()
|
||||
try searcher.search(for: "slack").wait()
|
||||
}
|
||||
.to(haveCount(39))
|
||||
}
|
||||
|
@ -46,12 +46,12 @@ public class MasStoreSearchSpec: QuickSpec {
|
|||
context("when lookup used") {
|
||||
it("can find slack") {
|
||||
let appID: AppID = 803_453_959
|
||||
let networkSession = NetworkSessionMockFromFile(responseFile: "lookup/slack.json")
|
||||
let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession))
|
||||
let networkSession = MockFromFileNetworkSession(responseFile: "lookup/slack.json")
|
||||
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
|
||||
|
||||
var result: SearchResult?
|
||||
do {
|
||||
result = try storeSearch.lookup(appID: appID).wait()
|
||||
result = try searcher.lookup(appID: appID).wait()
|
||||
} catch {
|
||||
let maserror = error as! MASError
|
||||
if case .jsonParsing(let nserror) = maserror {
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// AppLibraryMock.swift
|
||||
// MockAppLibrary.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
|
@ -8,7 +8,7 @@
|
|||
|
||||
@testable import mas
|
||||
|
||||
class AppLibraryMock: AppLibrary {
|
||||
class MockAppLibrary: AppLibrary {
|
||||
var installedApps: [SoftwareProduct] = []
|
||||
|
||||
func uninstallApps(atPaths appPaths: [String]) throws {
|
||||
|
@ -20,7 +20,7 @@ class AppLibraryMock: AppLibrary {
|
|||
}
|
||||
|
||||
/// Members not part of the AppLibrary protocol that are only for test state management.
|
||||
extension AppLibraryMock {
|
||||
extension MockAppLibrary {
|
||||
/// Clears out the list of installed apps.
|
||||
func reset() {
|
||||
installedApps = []
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// StoreSearchMock.swift
|
||||
// MockAppStoreSearcher.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
|
@ -10,11 +10,11 @@ import PromiseKit
|
|||
|
||||
@testable import mas
|
||||
|
||||
class StoreSearchMock: StoreSearch {
|
||||
class MockAppStoreSearcher: AppStoreSearcher {
|
||||
var apps: [AppID: SearchResult] = [:]
|
||||
|
||||
func search(for appName: String) -> Promise<[SearchResult]> {
|
||||
.value(apps.filter { $1.trackName.contains(appName) }.map { $1 })
|
||||
func search(for searchTerm: String) -> Promise<[SearchResult]> {
|
||||
.value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 })
|
||||
}
|
||||
|
||||
func lookup(appID: AppID) -> Promise<SearchResult?> {
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// MasAppLibrarySpec.swift
|
||||
// SoftwareMapAppLibrarySpec.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 3/1/20.
|
||||
|
@ -11,12 +11,12 @@ import Quick
|
|||
|
||||
@testable import mas
|
||||
|
||||
public class MasAppLibrarySpec: QuickSpec {
|
||||
public class SoftwareMapAppLibrarySpec: QuickSpec {
|
||||
override public func spec() {
|
||||
let library = MasAppLibrary(softwareMap: SoftwareMapMock(products: apps))
|
||||
let library = SoftwareMapAppLibrary(softwareMap: MockSoftwareMap(products: apps))
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("mas app library") {
|
||||
it("contains all installed apps") {
|
||||
|
@ -24,14 +24,14 @@ public class MasAppLibrarySpec: QuickSpec {
|
|||
expect(library.installedApps.first!.appName) == myApp.appName
|
||||
}
|
||||
it("can locate an app by bundle id") {
|
||||
expect(library.installedApp(forBundleId: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier
|
||||
expect(library.installedApp(forBundleID: "com.example")!.bundleIdentifier) == myApp.bundleIdentifier
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Data
|
||||
let myApp = SoftwareProductMock(
|
||||
let myApp = MockSoftwareProduct(
|
||||
appName: "MyApp",
|
||||
bundleIdentifier: "com.example",
|
||||
bundlePath: "/Applications/MyApp.app",
|
||||
|
@ -41,8 +41,8 @@ let myApp = SoftwareProductMock(
|
|||
|
||||
var apps: [SoftwareProduct] = [myApp]
|
||||
|
||||
// MARK: - SoftwareMapMock
|
||||
struct SoftwareMapMock: SoftwareMap {
|
||||
// MARK: - MockSoftwareMap
|
||||
struct MockSoftwareMap: SoftwareMap {
|
||||
var products: [SoftwareProduct] = []
|
||||
|
||||
func allSoftwareProducts() -> [SoftwareProduct] {
|
|
@ -32,7 +32,7 @@ class MASErrorTestCase: XCTestCase {
|
|||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
nserror = NSError(domain: errorDomain, code: 999)
|
||||
localizedDescription = "foo"
|
||||
}
|
||||
|
@ -59,7 +59,7 @@ class MASErrorTestCase: XCTestCase {
|
|||
}
|
||||
|
||||
func testAlreadySignedIn() {
|
||||
error = .alreadySignedIn(asAccountId: "person@example.com")
|
||||
error = .alreadySignedIn(asAppleID: "person@example.com")
|
||||
XCTAssertEqual(error.description, "Already signed in as person@example.com")
|
||||
}
|
||||
|
||||
|
|
|
@ -26,11 +26,12 @@ extension Bundle {
|
|||
static func url(for fileName: String) -> URL? {
|
||||
// The Swift Package Manager places resources in a separate bundle from the executable.
|
||||
// https://forums.swift.org/t/swift-5-3-spm-resources-in-tests-uses-wrong-bundle-path/37051
|
||||
let bundleURL = Bundle(for: NetworkSessionMock.self)
|
||||
let bundleURL = Bundle(for: MockNetworkSession.self)
|
||||
.bundleURL
|
||||
.deletingLastPathComponent()
|
||||
.appendingPathComponent("mas_masTests.bundle")
|
||||
guard let bundle = Bundle(url: bundleURL),
|
||||
guard
|
||||
let bundle = Bundle(url: bundleURL),
|
||||
let url = bundle.url(for: fileName)
|
||||
else {
|
||||
fatalError("Unable to load file \(fileName)")
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// OpenSystemCommandMock.swift
|
||||
// MockOpenSystemCommand.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 1/4/19.
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||
|
||||
@testable import mas
|
||||
|
||||
class OpenSystemCommandMock: ExternalCommand {
|
||||
class MockOpenSystemCommand: ExternalCommand {
|
||||
// Stub out protocol logic
|
||||
var succeeded = true
|
||||
var arguments: [String] = []
|
|
@ -14,7 +14,7 @@ import Quick
|
|||
public class OpenSystemCommandSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("open system command") {
|
||||
context("binary path") {
|
||||
|
|
|
@ -18,7 +18,7 @@ public class AppListFormatterSpec: QuickSpec {
|
|||
var products: [SoftwareProduct] = []
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("app list formatter") {
|
||||
beforeEach {
|
||||
|
@ -28,7 +28,7 @@ public class AppListFormatterSpec: QuickSpec {
|
|||
expect(format(products)).to(beEmpty())
|
||||
}
|
||||
it("can format a single product") {
|
||||
let product = SoftwareProductMock(
|
||||
let product = MockSoftwareProduct(
|
||||
appName: "Awesome App",
|
||||
bundleIdentifier: "",
|
||||
bundlePath: "",
|
||||
|
@ -39,14 +39,14 @@ public class AppListFormatterSpec: QuickSpec {
|
|||
}
|
||||
it("can format two products") {
|
||||
products = [
|
||||
SoftwareProductMock(
|
||||
MockSoftwareProduct(
|
||||
appName: "Awesome App",
|
||||
bundleIdentifier: "",
|
||||
bundlePath: "",
|
||||
bundleVersion: "19.2.1",
|
||||
itemIdentifier: 12345
|
||||
),
|
||||
SoftwareProductMock(
|
||||
MockSoftwareProduct(
|
||||
appName: "Even Better App",
|
||||
bundleIdentifier: "",
|
||||
bundlePath: "",
|
||||
|
|
|
@ -18,7 +18,7 @@ public class SearchResultFormatterSpec: QuickSpec {
|
|||
var results: [SearchResult] = []
|
||||
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("search results formatter") {
|
||||
beforeEach {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// SoftwareProductMock.swift
|
||||
// MockSoftwareProduct.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 12/27/18.
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||
|
||||
@testable import mas
|
||||
|
||||
struct SoftwareProductMock: SoftwareProduct {
|
||||
struct MockSoftwareProduct: SoftwareProduct {
|
||||
var appName: String
|
||||
var bundleIdentifier: String
|
||||
var bundlePath: String
|
|
@ -15,7 +15,7 @@ import Quick
|
|||
public class SearchResultListSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("search result list") {
|
||||
it("can parse bbedit") {
|
||||
|
|
|
@ -15,7 +15,7 @@ import Quick
|
|||
public class SearchResultSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("search result") {
|
||||
it("can parse things") {
|
||||
|
|
|
@ -15,10 +15,10 @@ import Quick
|
|||
public class SoftwareProductSpec: QuickSpec {
|
||||
override public func spec() {
|
||||
beforeSuite {
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
describe("software product") {
|
||||
let app = SoftwareProductMock(
|
||||
let app = MockSoftwareProduct(
|
||||
appName: "App",
|
||||
bundleIdentifier: "",
|
||||
bundlePath: "",
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// NetworkSessionMockFromFile.swift
|
||||
// MockFromFileNetworkSession.swift
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 2019-01-05.
|
||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import PromiseKit
|
||||
|
||||
/// Mock NetworkSession for testing with saved JSON response payload files.
|
||||
class NetworkSessionMockFromFile: NetworkSessionMock {
|
||||
class MockFromFileNetworkSession: MockNetworkSession {
|
||||
/// Path to response payload file relative to test bundle.
|
||||
private let responseFile: String
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
//
|
||||
// NetworkSessionMock
|
||||
// MockNetworkSession
|
||||
// masTests
|
||||
//
|
||||
// Created by Ben Chatelain on 11/13/18.
|
||||
|
@ -12,7 +12,7 @@ import PromiseKit
|
|||
@testable import mas
|
||||
|
||||
/// Mock NetworkSession for testing.
|
||||
class NetworkSessionMock: NetworkSession {
|
||||
class MockNetworkSession: NetworkSession {
|
||||
// Properties that enable us to set exactly what data or error
|
||||
// we want our mocked URLSession to return for any request.
|
||||
var data: Data?
|
|
@ -13,12 +13,12 @@ import XCTest
|
|||
class NetworkManagerTests: XCTestCase {
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
Mas.initialize()
|
||||
MAS.initialize()
|
||||
}
|
||||
|
||||
func testSuccessfulAsyncResponse() throws {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
|
@ -35,7 +35,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testSuccessfulSyncResponse() throws {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
// Create data and tell the session to always return it
|
||||
|
@ -52,7 +52,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testFailureAsyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = MASError.noData
|
||||
|
@ -73,7 +73,7 @@ class NetworkManagerTests: XCTestCase {
|
|||
|
||||
func testFailureSyncResponse() {
|
||||
// Setup our objects
|
||||
let session = NetworkSessionMock()
|
||||
let session = MockNetworkSession()
|
||||
let manager = NetworkManager(session: session)
|
||||
|
||||
session.error = MASError.noData
|
||||
|
|
|
@ -46,7 +46,7 @@ complete -c mas -n "__fish_seen_subcommand_from help" -xa "list"
|
|||
complete -c mas -n "__fish_use_subcommand" -f -a lucky -d "Install the first result from the Mac App Store"
|
||||
complete -c mas -n "__fish_seen_subcommand_from help" -xa "lucky"
|
||||
### open
|
||||
complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in AppStore.app"
|
||||
complete -c mas -n "__fish_use_subcommand" -f -a open -d "Opens app page in 'App Store.app'"
|
||||
complete -c mas -n "__fish_seen_subcommand_from help" -xa "open"
|
||||
### outdated
|
||||
complete -c mas -n "__fish_use_subcommand" -f -a outdated -d "Lists pending updates from the Mac App Store"
|
||||
|
|
|
@ -90,11 +90,14 @@ private extension MyClass {
|
|||
guard let singleTest = somethingFailable() else { return }
|
||||
guard statementThatShouldBeTrue else { return }
|
||||
|
||||
// If there is one long expression to guard or multiple expressions
|
||||
// move else to next line
|
||||
guard let oneItem = somethingFailable(),
|
||||
// If a guard clause requires multiple lines, chop down, then start `else` new line
|
||||
// In this case, always chop down else clause.
|
||||
guard
|
||||
let oneItem = somethingFailable(),
|
||||
let secondItem = somethingFailable2()
|
||||
else { return }
|
||||
else {
|
||||
return
|
||||
}
|
||||
|
||||
// If the return in else is long, move to next line
|
||||
guard let something = somethingFailable() else {
|
||||
|
|
Loading…
Reference in a new issue