Merge pull request #600 from rgoldberg/585-naming

Improve naming
This commit is contained in:
Ross Goldberg 2024-10-25 23:44:08 -04:00 committed by GitHub
commit 12832f293d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
62 changed files with 226 additions and 216 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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://"]

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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

View file

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

View file

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

View file

@ -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] = []

View file

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

View file

@ -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: "",

View file

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

View file

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

View file

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

View file

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

View file

@ -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: "",

View file

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

View file

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

View file

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

View file

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

View file

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