From b4652ff0626526262ed2b07efe266a35859c0eb1 Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sat, 10 May 2025 11:31:04 -0400 Subject: [PATCH] Accept Bundle IDs as arguments alongside ADAM IDs (fka app IDs). No longer accept app names as arguments for `upgrade`. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- Sources/mas/AppStore/Downloader.swift | 37 +++++++++-- .../AppStore/PurchaseDownloadObserver.swift | 12 ++-- Sources/mas/AppStore/SSPurchase.swift | 13 ++-- Sources/mas/Commands/Install.swift | 25 +++----- Sources/mas/Commands/Lucky.swift | 26 ++++---- Sources/mas/Commands/Open.swift | 13 ++-- .../OptionGroups/AppIDsOptionGroup.swift | 8 ++- Sources/mas/Commands/Outdated.swift | 2 +- Sources/mas/Commands/Purchase.swift | 25 +++----- Sources/mas/Commands/Uninstall.swift | 2 +- Sources/mas/Commands/Upgrade.swift | 31 ++++------ .../ITunesSearchAppStoreSearcher.swift | 9 ++- .../Controllers/SpotlightInstalledApps.swift | 2 +- Sources/mas/Errors/MASError.swift | 4 +- Sources/mas/Formatters/AppListFormatter.swift | 2 +- Sources/mas/Models/AppID.swift | 61 ++++++++++++++++++- Sources/mas/Models/InstalledApp.swift | 9 +-- Sources/mas/Models/SearchResult.swift | 7 ++- Tests/masTests/Commands/HomeTests.swift | 2 +- Tests/masTests/Commands/InfoTests.swift | 4 +- Tests/masTests/Commands/OpenTests.swift | 2 +- Tests/masTests/Commands/OutdatedTests.swift | 7 ++- Tests/masTests/Commands/SearchTests.swift | 2 +- Tests/masTests/Commands/UninstallTests.swift | 16 ++--- Tests/masTests/Commands/VendorTests.swift | 2 +- .../ITunesSearchAppStoreSearcherTests.swift | 6 +- .../Formatters/AppListFormatterTests.swift | 6 +- Tests/masTests/Models/InstalledAppTests.swift | 2 +- 28 files changed, 204 insertions(+), 133 deletions(-) diff --git a/Sources/mas/AppStore/Downloader.swift b/Sources/mas/AppStore/Downloader.swift index 832532852..b931d5797 100644 --- a/Sources/mas/AppStore/Downloader.swift +++ b/Sources/mas/AppStore/Downloader.swift @@ -10,13 +10,42 @@ private import CommerceKit struct Downloader { let printer: Printer + func downloadApps( + withAppIDs appIDs: [AppID], + purchasing: Bool, + forceDownload: Bool, + installedApps: [InstalledApp], + searcher: AppStoreSearcher + ) async { + for appID in appIDs.filter({ appID in + if let installedApp = installedApps.first(where: { appID.matches($0) }), !forceDownload { + printer.warning( + purchasing ? "Already purchased: " : "Already installed: ", + installedApp.name, + " (", + appID, + ")", + separator: "" + ) + return false + } + return true + }) { + do { + try await downloadApp(withADAMID: try await appID.adamID(searcher: searcher), purchasing: purchasing) + } catch { + printer.error(error: error) + } + } + } + func downloadApp( - withAppID appID: AppID, + withADAMID adamID: ADAMID, purchasing: Bool = false, withAttemptCount attemptCount: UInt32 = 3 ) async throws { do { - let purchase = await SSPurchase(appID: appID, purchasing: purchasing) + let purchase = await SSPurchase(adamID: adamID, purchasing: purchasing) _ = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in CKPurchaseController.shared().perform(purchase, withOptions: 0) { _, _, error, response in if let error { @@ -24,7 +53,7 @@ struct Downloader { } else if response?.downloads?.isEmpty == false { Task { do { - try await PurchaseDownloadObserver(appID: appID, printer: printer).observeDownloadQueue() + try await PurchaseDownloadObserver(adamID: adamID, printer: printer).observeDownloadQueue() continuation.resume() } catch { continuation.resume(throwing: error) @@ -53,7 +82,7 @@ struct Downloader { error, separator: "" ) - try await downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount) + try await downloadApp(withADAMID: adamID, purchasing: purchasing, withAttemptCount: attemptCount) } } } diff --git a/Sources/mas/AppStore/PurchaseDownloadObserver.swift b/Sources/mas/AppStore/PurchaseDownloadObserver.swift index ab74b9809..509cabed1 100644 --- a/Sources/mas/AppStore/PurchaseDownloadObserver.swift +++ b/Sources/mas/AppStore/PurchaseDownloadObserver.swift @@ -13,15 +13,15 @@ private var initialPhaseType: Int64 { 4 } private var downloadedPhaseType: Int64 { 5 } class PurchaseDownloadObserver: CKDownloadQueueObserver { - private let appID: AppID + private let adamID: ADAMID private let printer: Printer private var completionHandler: (() -> Void)? private var errorHandler: ((Error) -> Void)? private var prevPhaseType: Int64? - init(appID: AppID, printer: Printer) { - self.appID = appID + init(adamID: ADAMID, printer: Printer) { + self.adamID = adamID self.printer = printer } @@ -32,14 +32,14 @@ class PurchaseDownloadObserver: CKDownloadQueueObserver { func downloadQueue(_ queue: CKDownloadQueue, statusChangedFor download: SSDownload) { guard let metadata = download.metadata, - metadata.itemIdentifier == appID, + metadata.itemIdentifier == adamID, let status = download.status else { return } if status.isFailed || status.isCancelled { - queue.removeDownload(withItemIdentifier: metadata.itemIdentifier) + queue.removeDownload(withItemIdentifier: adamID) } else { prevPhaseType = printer.progress(for: metadata.appNameAndVersion, status: status, prevPhaseType: prevPhaseType) } @@ -52,7 +52,7 @@ class PurchaseDownloadObserver: CKDownloadQueueObserver { func downloadQueue(_: CKDownloadQueue, changedWithRemoval download: SSDownload) { guard let metadata = download.metadata, - metadata.itemIdentifier == appID, + metadata.itemIdentifier == adamID, let status = download.status else { return diff --git a/Sources/mas/AppStore/SSPurchase.swift b/Sources/mas/AppStore/SSPurchase.swift index d5b175be4..f4ecda7a7 100644 --- a/Sources/mas/AppStore/SSPurchase.swift +++ b/Sources/mas/AppStore/SSPurchase.swift @@ -8,22 +8,21 @@ private import StoreFoundation extension SSPurchase { - convenience init(appID: AppID, purchasing: Bool) async { + convenience init(adamID: ADAMID, purchasing: Bool) async { self.init( buyParameters: """ - productType=C&price=0&salableAdamId=\(appID)&pg=default&appExtVrsId=0&pricingParameters=\ - \(purchasing ? "STDQ&macappinstalledconfirmed=1" : "STDRDL") + productType=C&price=0&pg=default&appExtVrsId=0&pricingParameters=\ + \(purchasing ? "STDQ&macappinstalledconfirmed=1" : "STDRDL")&salableAdamId=\(adamID) """ ) // Possibly unnecessary… isRedownload = !purchasing - itemIdentifier = appID + itemIdentifier = adamID - let downloadMetadata = SSDownloadMetadata() - downloadMetadata.kind = "software" - downloadMetadata.itemIdentifier = appID + let downloadMetadata = SSDownloadMetadata(kind: "software") + downloadMetadata.itemIdentifier = adamID self.downloadMetadata = downloadMetadata do { diff --git a/Sources/mas/Commands/Install.swift b/Sources/mas/Commands/Install.swift index 95d7f983f..e35bf4e6e 100644 --- a/Sources/mas/Commands/Install.swift +++ b/Sources/mas/Commands/Install.swift @@ -26,24 +26,13 @@ extension MAS { func run(installedApps: [InstalledApp], searcher: AppStoreSearcher) async throws { try await mas.run { printer in - await run(downloader: Downloader(printer: printer), installedApps: installedApps, searcher: searcher) - } - } - - private func run(downloader: Downloader, installedApps: [InstalledApp], searcher: AppStoreSearcher) async { - for appID in appIDsOptionGroup.appIDs.filter({ appID in - if let installedApp = installedApps.first(where: { $0.id == appID }), !forceOptionGroup.force { - downloader.printer.warning("Already installed:", installedApp.idAndName) - return false - } - return true - }) { - do { - _ = try await searcher.lookup(appID: appID) - try await downloader.downloadApp(withAppID: appID) - } catch { - downloader.printer.error(error: error) - } + await Downloader(printer: printer).downloadApps( + withAppIDs: appIDsOptionGroup.appIDs, + purchasing: false, + forceDownload: forceOptionGroup.force, + installedApps: installedApps, + searcher: searcher + ) } } } diff --git a/Sources/mas/Commands/Lucky.swift b/Sources/mas/Commands/Lucky.swift index 25b172fd0..318760712 100644 --- a/Sources/mas/Commands/Lucky.swift +++ b/Sources/mas/Commands/Lucky.swift @@ -43,21 +43,19 @@ extension MAS { throw MASError.noSearchResultsFound(for: searchTerm) } - try await install(appID: result.trackId, installedApps: installedApps, downloader: downloader) - } - - /// Installs an app. - /// - /// - Parameters: - /// - appID: App ID. - /// - installedApps: List of installed apps. - /// - downloader: `Downloader`. - /// - Throws: Any error that occurs while attempting to install the app. - private func install(appID: AppID, installedApps: [InstalledApp], downloader: Downloader) async throws { - if let installedApp = installedApps.first(where: { $0.id == appID }), !forceOptionGroup.force { - downloader.printer.warning("Already installed:", installedApp.idAndName) + let adamID = result.adamID + + if let installedApp = installedApps.first(where: { $0.adamID == adamID }), !forceOptionGroup.force { + downloader.printer.warning( + "Already installed: ", + installedApp.name, + " (search term ", + searchTerm, + ")", + separator: "" + ) } else { - try await downloader.downloadApp(withAppID: appID) + try await downloader.downloadApp(withADAMID: adamID) } } } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 49976ac32..1942d186a 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -21,8 +21,10 @@ extension MAS { abstract: "Open app page in 'App Store.app'" ) - @Argument(help: "App ID") - var appID: AppID? + @Flag(name: .customLong("bundle"), help: ArgumentHelp("Process all app IDs as bundle IDs")) + var forceBundleID = false + @Argument(help: ArgumentHelp("App ID", valueName: "app-id")) + var appIDString: String? /// Runs the command. func run() async throws { @@ -34,13 +36,16 @@ extension MAS { } private func run(printer _: Printer, searcher: AppStoreSearcher) async throws { - guard let appID else { + guard let appIDString else { // If no app ID was given, just open the MAS GUI app try await openMacAppStore() return } - try await openInMacAppStore(pageForAppID: appID, searcher: searcher) + try await openInMacAppStore( + pageForAppID: AppID(from: appIDString, forceBundleID: forceBundleID), + searcher: searcher + ) } } } diff --git a/Sources/mas/Commands/OptionGroups/AppIDsOptionGroup.swift b/Sources/mas/Commands/OptionGroups/AppIDsOptionGroup.swift index cb50007b9..81daba956 100644 --- a/Sources/mas/Commands/OptionGroups/AppIDsOptionGroup.swift +++ b/Sources/mas/Commands/OptionGroups/AppIDsOptionGroup.swift @@ -8,6 +8,12 @@ internal import ArgumentParser struct AppIDsOptionGroup: ParsableArguments { + @Flag(name: .customLong("bundle"), help: ArgumentHelp("Process all app IDs as bundle IDs")) + var forceBundleID = false @Argument(help: ArgumentHelp("App ID", valueName: "app-id")) - var appIDs: [AppID] + var appIDStrings: [String] + + var appIDs: [AppID] { + appIDStrings.compactMap { AppID(from: $0, forceBundleID: forceBundleID) } + } } diff --git a/Sources/mas/Commands/Outdated.swift b/Sources/mas/Commands/Outdated.swift index 398d27320..34d07176d 100644 --- a/Sources/mas/Commands/Outdated.swift +++ b/Sources/mas/Commands/Outdated.swift @@ -33,7 +33,7 @@ extension MAS { let storeApp = try await searcher.lookup(appID: installedApp.id) if installedApp.isOutdated(comparedTo: storeApp) { printer.info( - installedApp.id, + installedApp.adamID, " ", installedApp.name, " (", diff --git a/Sources/mas/Commands/Purchase.swift b/Sources/mas/Commands/Purchase.swift index 27dad3704..aa627a36b 100644 --- a/Sources/mas/Commands/Purchase.swift +++ b/Sources/mas/Commands/Purchase.swift @@ -24,24 +24,13 @@ extension MAS { func run(installedApps: [InstalledApp], searcher: AppStoreSearcher) async throws { try await mas.run { printer in - await run(downloader: Downloader(printer: printer), installedApps: installedApps, searcher: searcher) - } - } - - private func run(downloader: Downloader, installedApps: [InstalledApp], searcher: AppStoreSearcher) async { - for appID in appIDsOptionGroup.appIDs.filter({ appID in - if let installedApp = installedApps.first(where: { $0.id == appID }) { - downloader.printer.warning("Already purchased:", installedApp.idAndName) - return false - } - return true - }) { - do { - _ = try await searcher.lookup(appID: appID) - try await downloader.downloadApp(withAppID: appID, purchasing: true) - } catch { - downloader.printer.error(error: error) - } + await Downloader(printer: printer).downloadApps( + withAppIDs: appIDsOptionGroup.appIDs, + purchasing: true, + forceDownload: false, + installedApps: installedApps, + searcher: searcher + ) } } } diff --git a/Sources/mas/Commands/Uninstall.swift b/Sources/mas/Commands/Uninstall.swift index be0da3f70..86ca0da7a 100644 --- a/Sources/mas/Commands/Uninstall.swift +++ b/Sources/mas/Commands/Uninstall.swift @@ -89,7 +89,7 @@ extension MAS { var uninstallingAppSet = OrderedSet() for appID in appIDsOptionGroup.appIDs { - let apps = installedApps.filter { $0.id == appID } + let apps = installedApps.filter { appID.matches($0) } apps.isEmpty ? printer.error(appID.notInstalledMessage) // swiftformat:disable:this indent : uninstallingAppSet.formUnion(apps) diff --git a/Sources/mas/Commands/Upgrade.swift b/Sources/mas/Commands/Upgrade.swift index 07230fdd7..d84c5404d 100644 --- a/Sources/mas/Commands/Upgrade.swift +++ b/Sources/mas/Commands/Upgrade.swift @@ -16,8 +16,10 @@ extension MAS { @OptionGroup var verboseOptionGroup: VerboseOptionGroup - @Argument(help: ArgumentHelp("App ID/name", valueName: "app-id-or-name")) - var appIDOrNames = [String]() + @Flag(name: .customLong("bundle"), help: ArgumentHelp("Process all app IDs as bundle IDs")) + var forceBundleID = false + @Argument(help: ArgumentHelp("App ID", valueName: "app-id")) + var appIDStrings = [String]() /// Runs the command. func run() async throws { @@ -49,9 +51,9 @@ extension MAS { separator: "" ) - for appID in apps.map(\.storeApp.trackId) { + for adamID in apps.map(\.storeApp.adamID) { do { - try await downloader.downloadApp(withAppID: appID) + try await downloader.downloadApp(withADAMID: adamID) } catch { downloader.printer.error(error: error) } @@ -63,22 +65,13 @@ extension MAS { installedApps: [InstalledApp], searcher: AppStoreSearcher ) async -> [(installedApp: InstalledApp, storeApp: SearchResult)] { - let apps = appIDOrNames.isEmpty + let apps = appIDStrings.isEmpty ? installedApps // swiftformat:disable:this indent - : appIDOrNames.flatMap { appIDOrName in - if let appID = AppID(appIDOrName) { - // Find installed apps by app ID argument - let installedApps = installedApps.filter { $0.id == appID } - if installedApps.isEmpty { - printer.error(appID.notInstalledMessage) - } - return installedApps - } - - // Find installed apps by name argument - let installedApps = installedApps.filter { $0.name == appIDOrName } + : appIDStrings.flatMap { appIDString in + let appID = AppID(from: appIDString, forceBundleID: forceBundleID) + let installedApps = installedApps.filter { appID.matches($0) } if installedApps.isEmpty { - printer.error("No installed apps named", appIDOrName) + printer.error(appID.notInstalledMessage) } return installedApps } @@ -86,7 +79,7 @@ extension MAS { var outdatedApps = [(InstalledApp, SearchResult)]() for installedApp in apps { do { - let storeApp = try await searcher.lookup(appID: installedApp.id) + let storeApp = try await searcher.lookup(appID: .adamID(installedApp.adamID)) if installedApp.isOutdated(comparedTo: storeApp) { outdatedApps.append((installedApp, storeApp)) } diff --git a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift index 60d3c897d..5db7058e5 100644 --- a/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift +++ b/Sources/mas/Controllers/ITunesSearchAppStoreSearcher.swift @@ -59,7 +59,14 @@ struct ITunesSearchAppStoreSearcher: AppStoreSearcher { /// - Returns: URL for the lookup service. /// - Throws: An `MASError.urlParsing` if `appID` can't be encoded. private func lookupURL(forAppID appID: AppID, inRegion region: String) throws -> URL { - try url("lookup", URLQueryItem(name: "id", value: String(appID)), inRegion: region) + let queryItem = + switch appID { + case let .adamID(adamID): + URLQueryItem(name: "id", value: String(adamID)) + case let .bundleID(bundleID): + URLQueryItem(name: "bundleId", value: bundleID) + } + return try url("lookup", queryItem, inRegion: region) } /// Builds the search URL for an app. diff --git a/Sources/mas/Controllers/SpotlightInstalledApps.swift b/Sources/mas/Controllers/SpotlightInstalledApps.swift index ac3afa8c1..aceaa5e64 100644 --- a/Sources/mas/Controllers/SpotlightInstalledApps.swift +++ b/Sources/mas/Controllers/SpotlightInstalledApps.swift @@ -45,7 +45,7 @@ var installedApps: [InstalledApp] { .compactMap { result in // swiftformat:disable indent if let item = result as? NSMetadataItem { InstalledApp( - id: item.value(forAttribute: "kMDItemAppStoreAdamID") as? AppID ?? 0, + adamID: item.value(forAttribute: "kMDItemAppStoreAdamID") as? ADAMID ?? 0, bundleID: item.value(forAttribute: NSMetadataItemCFBundleIdentifierKey) as? String ?? "", name: (item.value(forAttribute: "_kMDItemDisplayNameWithExtensions") as? String ?? "").removingSuffix( ".app" diff --git a/Sources/mas/Errors/MASError.swift b/Sources/mas/Errors/MASError.swift index 3bfd45eac..730b95035 100644 --- a/Sources/mas/Errors/MASError.swift +++ b/Sources/mas/Errors/MASError.swift @@ -35,7 +35,7 @@ extension MASError: CustomStringConvertible { case let .noSearchResultsFound(searchTerm): "No apps found in the Mac App Store for search term: \(searchTerm)" case let .noVendorWebsite(appID): - "No vendor website available for app ID \(appID)" + "No vendor website available for \(appID)" case .notSupported: """ This command is not supported on this macOS version due to changes in macOS @@ -44,7 +44,7 @@ extension MASError: CustomStringConvertible { case let .runtimeError(message): message case let .unknownAppID(appID): - "No apps found in the Mac App Store for app ID \(appID)" + "No apps found in the Mac App Store for \(appID)" case let .urlParsing(string): "Unable to parse URL from \(string)" } diff --git a/Sources/mas/Formatters/AppListFormatter.swift b/Sources/mas/Formatters/AppListFormatter.swift index c43bc24bb..70057385a 100644 --- a/Sources/mas/Formatters/AppListFormatter.swift +++ b/Sources/mas/Formatters/AppListFormatter.swift @@ -23,7 +23,7 @@ enum AppListFormatter { return installedApps.map { installedApp in """ - \(installedApp.id.description.padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0))\ + \(installedApp.adamID.description.padding(toLength: idColumnMinWidth, withPad: " ", startingAt: 0))\ \(installedApp.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0))\ (\(installedApp.version)) """ diff --git a/Sources/mas/Models/AppID.swift b/Sources/mas/Models/AppID.swift index 293ea80b6..013969960 100644 --- a/Sources/mas/Models/AppID.swift +++ b/Sources/mas/Models/AppID.swift @@ -5,10 +5,65 @@ // Copyright © 2024 mas-cli. All rights reserved. // -typealias AppID = UInt64 +private import Foundation + +enum AppID: CustomStringConvertible, Equatable, Hashable { + case adamID(ADAMID) + case bundleID(String) + + var description: String { + switch self { + case let .adamID(adamID): + "ADAM ID \(adamID)" + case let .bundleID(bundleID): + "bundle ID \(bundleID)" + } + } -extension AppID { var notInstalledMessage: String { - "No installed apps with app ID \(self)" + "No installed apps with \(self)" + } + + init(from string: String, forceBundleID: Bool = false) { + if !forceBundleID, let adamID = ADAMID(string) { + self = .adamID(adamID) + return + } + + self = .bundleID(string) + } + + func matches(_ appIdentifying: any AppIdentifying) -> Bool { + switch self { + case let .adamID(adamID): + adamID == appIdentifying.adamID + case let .bundleID(bundleID): + bundleID == appIdentifying.bundleID + } + } + + func adamID(searcher: AppStoreSearcher) async throws -> ADAMID { + switch self { + case let .adamID(adamID): + adamID + case .bundleID: + try await searcher.lookup(appID: self).adamID + } + } +} + +typealias ADAMID = UInt64 + +// swiftlint:disable:next one_declaration_per_file +protocol AppIdentifying { + var adamID: ADAMID { get } + var bundleID: String { get } +} + +extension AppIdentifying { + var id: AppID { + bundleID.isEmpty + ? .adamID(adamID) // swiftformat:disable:this indent + : .bundleID(bundleID) } } diff --git a/Sources/mas/Models/InstalledApp.swift b/Sources/mas/Models/InstalledApp.swift index d8e01d07b..5e0e6938d 100644 --- a/Sources/mas/Models/InstalledApp.swift +++ b/Sources/mas/Models/InstalledApp.swift @@ -8,9 +8,8 @@ private import Foundation private import Version -struct InstalledApp: Hashable, Sendable { - let id: AppID - // periphery:ignore +struct InstalledApp: AppIdentifying, Hashable, Sendable { + let adamID: ADAMID let bundleID: String let name: String let path: String @@ -18,10 +17,6 @@ struct InstalledApp: Hashable, Sendable { } extension InstalledApp { - var idAndName: String { - "app ID \(id) (\(name))" - } - /// Determines whether the app is considered outdated. /// /// Updates that require a higher OS version are excluded. diff --git a/Sources/mas/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift index 9a3c9650f..d7bb7439d 100644 --- a/Sources/mas/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -6,19 +6,24 @@ // struct SearchResult: Decodable { + var bundleId = "" var currentVersionReleaseDate = "" var fileSizeBytes = "0" var formattedPrice = "0" as String? var minimumOsVersion = "" var sellerName = "" var sellerUrl = "" as String? - var trackId = 0 as AppID + var trackId = 0 as ADAMID var trackName = "" var trackViewUrl = "" var version = "" } extension SearchResult { + var adamID: ADAMID { + trackId + } + var outputPrice: String { formattedPrice ?? "?" } diff --git a/Tests/masTests/Commands/HomeTests.swift b/Tests/masTests/Commands/HomeTests.swift index 73d28fb68..32d3b828a 100644 --- a/Tests/masTests/Commands/HomeTests.swift +++ b/Tests/masTests/Commands/HomeTests.swift @@ -13,6 +13,6 @@ internal import Testing func cannotFindAppHomeForUnknownAppID() async { #expect( // swiftformat:disable:next indent await consequencesOf(try await MAS.Home.parse(["999"]).run(searcher: MockAppStoreSearcher())) - == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for app ID 999\n") + == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for ADAM ID 999\n") ) // swiftformat:disable:previous indent } diff --git a/Tests/masTests/Commands/InfoTests.swift b/Tests/masTests/Commands/InfoTests.swift index 3971a610c..2b8c4f086 100644 --- a/Tests/masTests/Commands/InfoTests.swift +++ b/Tests/masTests/Commands/InfoTests.swift @@ -13,7 +13,7 @@ internal import Testing func cannotFindAppInfoForUnknownAppID() async { #expect( // swiftformat:disable:next indent await consequencesOf(try await MAS.Info.parse(["999"]).run(searcher: MockAppStoreSearcher())) - == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for app ID 999\n") + == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for ADAM ID 999\n") ) // swiftformat:disable:previous indent } @@ -33,7 +33,7 @@ func outputsAppInfo() async { #expect( await consequencesOf( try await MAS.Info.parse([String(mockResult.trackId)]).run( - searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]) + searcher: MockAppStoreSearcher([.adamID(mockResult.adamID): mockResult]) ) ) == UnvaluedConsequences( // swiftformat:disable indent diff --git a/Tests/masTests/Commands/OpenTests.swift b/Tests/masTests/Commands/OpenTests.swift index 28b700343..ce40368ec 100644 --- a/Tests/masTests/Commands/OpenTests.swift +++ b/Tests/masTests/Commands/OpenTests.swift @@ -13,6 +13,6 @@ internal import Testing func cannotOpenUnknownAppID() async { #expect( // swiftformat:disable:next indent await consequencesOf(try await MAS.Open.parse(["999"]).run(searcher: MockAppStoreSearcher())) - == UnvaluedConsequences(ExitCode(1), "", "Error: \(MASError.unknownAppID(999))\n") + == UnvaluedConsequences(ExitCode(1), "", "Error: \(MASError.unknownAppID(.adamID(999)))\n") ) // swiftformat:disable:previous indent } diff --git a/Tests/masTests/Commands/OutdatedTests.swift b/Tests/masTests/Commands/OutdatedTests.swift index 439b99fdc..aeabe946e 100644 --- a/Tests/masTests/Commands/OutdatedTests.swift +++ b/Tests/masTests/Commands/OutdatedTests.swift @@ -13,6 +13,7 @@ internal import Testing func outputsOutdatedApps() async { let mockSearchResult = SearchResult( + bundleId: "au.id.haroldchu.mac.Bandwidth", currentVersionReleaseDate: "2024-09-02T00:27:00Z", fileSizeBytes: "998130", minimumOsVersion: "10.13", @@ -29,14 +30,14 @@ func outputsOutdatedApps() async { try await MAS.Outdated.parse([]).run( installedApps: [ InstalledApp( - id: mockSearchResult.trackId, - bundleID: "au.id.haroldchu.mac.Bandwidth", + adamID: mockSearchResult.trackId, + bundleID: mockSearchResult.bundleId, name: mockSearchResult.trackName, path: "/Applications/Bandwidth+.app", version: "1.27" ), ], - searcher: MockAppStoreSearcher([mockSearchResult.trackId: mockSearchResult]) + searcher: MockAppStoreSearcher([.bundleID(mockSearchResult.bundleId): mockSearchResult]) ) ) == UnvaluedConsequences(nil, "490461369 Bandwidth+ (1.27 -> 1.28)\n") // swiftformat:disable:this indent diff --git a/Tests/masTests/Commands/SearchTests.swift b/Tests/masTests/Commands/SearchTests.swift index 0df9488aa..2e5842f53 100644 --- a/Tests/masTests/Commands/SearchTests.swift +++ b/Tests/masTests/Commands/SearchTests.swift @@ -14,7 +14,7 @@ func searchesForSlack() async { let mockResult = SearchResult(trackId: 1111, trackName: "slack", trackViewUrl: "mas preview url", version: "0.0") #expect( await consequencesOf( - try await MAS.Search.parse(["slack"]).run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult])) + try await MAS.Search.parse(["slack"]).run(searcher: MockAppStoreSearcher([.adamID(mockResult.adamID): mockResult])) ) == UnvaluedConsequences(nil, " 1111 slack (0.0)\n") // swiftformat:disable:this indent ) diff --git a/Tests/masTests/Commands/UninstallTests.swift b/Tests/masTests/Commands/UninstallTests.swift index 99445a5c4..48b30af2e 100644 --- a/Tests/masTests/Commands/UninstallTests.swift +++ b/Tests/masTests/Commands/UninstallTests.swift @@ -9,9 +9,9 @@ private import ArgumentParser @testable private import mas internal import Testing -private let appID = 12345 as AppID +private let adamID = 12345 as ADAMID private let app = InstalledApp( - id: appID, + adamID: adamID, bundleID: "com.some.app", name: "Some App", path: "/tmp/Some.app", @@ -21,15 +21,15 @@ private let app = InstalledApp( @Test(.disabled()) func uninstallDryRunCannotRemoveMissingApp() { #expect( - consequencesOf(try MAS.Uninstall.parse(["--dry-run", String(appID)]).run(installedApps: [])) - == UnvaluedConsequences(nil, "No installed apps with app ID \(appID)") // swiftformat:disable:this indent + consequencesOf(try MAS.Uninstall.parse(["--dry-run", String(adamID)]).run(installedApps: [])) + == UnvaluedConsequences(nil, "No installed apps with ADAM ID \(adamID)") // swiftformat:disable:this indent ) } @Test(.disabled()) func uninstallDryRunFindsApp() { #expect( // swiftformat:disable:next indent - consequencesOf(try MAS.Uninstall.parse(["--dry-run", String(appID)]).run(installedApps: [app])) + consequencesOf(try MAS.Uninstall.parse(["--dry-run", String(adamID)]).run(installedApps: [app])) == UnvaluedConsequences(nil, "==> 'Some App' '/tmp/Some.app'\n==> (not removed, dry run)\n") ) // swiftformat:disable:previous indent } @@ -37,14 +37,14 @@ func uninstallDryRunFindsApp() { @Test(.disabled()) func uninstallCannotRemoveMissingApp() { #expect( - consequencesOf(try MAS.Uninstall.parse([String(appID)]).run(installedApps: [])) - == UnvaluedConsequences(nil, "No installed apps with app ID \(appID)") // swiftformat:disable:this indent + consequencesOf(try MAS.Uninstall.parse([String(adamID)]).run(installedApps: [])) + == UnvaluedConsequences(nil, "No installed apps with ADAM ID \(adamID)") // swiftformat:disable:this indent ) } @Test(.disabled()) func uninstallRemovesApp() { #expect( - consequencesOf(try MAS.Uninstall.parse([String(appID)]).run(installedApps: [app])) == UnvaluedConsequences() + consequencesOf(try MAS.Uninstall.parse([String(adamID)]).run(installedApps: [app])) == UnvaluedConsequences() ) } diff --git a/Tests/masTests/Commands/VendorTests.swift b/Tests/masTests/Commands/VendorTests.swift index 66936631d..dcb8ffc67 100644 --- a/Tests/masTests/Commands/VendorTests.swift +++ b/Tests/masTests/Commands/VendorTests.swift @@ -13,6 +13,6 @@ internal import Testing func cannotFindVendorOfUnknownAppID() async { #expect( // swiftformat:disable:next indent await consequencesOf(try await MAS.Vendor.parse(["999"]).run(searcher: MockAppStoreSearcher())) - == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for app ID 999\n") + == UnvaluedConsequences(ExitCode(1), "", "Error: No apps found in the Mac App Store for ADAM ID 999\n") ) // swiftformat:disable:previous indent } diff --git a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherTests.swift b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherTests.swift index a2c40e7f0..b3de85a50 100644 --- a/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherTests.swift +++ b/Tests/masTests/Controllers/ITunesSearchAppStoreSearcherTests.swift @@ -24,11 +24,11 @@ func iTunesSearchesForSlack() async { @Test func looksUpSlack() async { - let appID = 803_453_959 as AppID + let adamID = 803_453_959 as ADAMID let consequences = await consequencesOf( try await ITunesSearchAppStoreSearcher(networkSession: MockNetworkSession(responseResource: "lookup/slack.json")) - .lookup(appID: appID) // swiftformat:disable:this indent + .lookup(appID: .adamID(adamID)) // swiftformat:disable:this indent ) #expect( consequences.error == nil @@ -42,7 +42,7 @@ func looksUpSlack() async { } #expect( - result.trackId == appID + result.trackId == adamID && result.sellerName == "Slack Technologies, Inc." // swiftformat:disable indent && result.sellerUrl == "https://slack.com" && result.trackName == "Slack" diff --git a/Tests/masTests/Formatters/AppListFormatterTests.swift b/Tests/masTests/Formatters/AppListFormatterTests.swift index ff273b581..fca33f613 100644 --- a/Tests/masTests/Formatters/AppListFormatterTests.swift +++ b/Tests/masTests/Formatters/AppListFormatterTests.swift @@ -18,7 +18,7 @@ func formatsEmptyAppListAsEmptyString() { @Test func formatsSingleInstalledApp() { #expect( - consequencesOf(format([InstalledApp(id: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1")])) + consequencesOf(format([InstalledApp(adamID: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1")])) == ValuedConsequences("12345 Awesome App (19.2.1)") // swiftformat:disable:this indent ) } @@ -29,8 +29,8 @@ func formatsTwoInstalledApps() { consequencesOf( format( [ - InstalledApp(id: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1"), - InstalledApp(id: 67890, bundleID: "", name: "Even Better App", path: "", version: "1.2.0"), + InstalledApp(adamID: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1"), + InstalledApp(adamID: 67890, bundleID: "", name: "Even Better App", path: "", version: "1.2.0"), ] ) ) // swiftformat:disable:next indent diff --git a/Tests/masTests/Models/InstalledAppTests.swift b/Tests/masTests/Models/InstalledAppTests.swift index 6a6fc706b..22f62028a 100644 --- a/Tests/masTests/Models/InstalledAppTests.swift +++ b/Tests/masTests/Models/InstalledAppTests.swift @@ -8,7 +8,7 @@ @testable private import mas internal import Testing -private let app = InstalledApp(id: 111, bundleID: "", name: "App", path: "", version: "1.0.0") +private let app = InstalledApp(adamID: 111, bundleID: "", name: "App", path: "", version: "1.0.0") @Test func installedAppNotOutdatedWhenNoNewVersionAvailable() {