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