Merge pull request #623 from rgoldberg/533-errors

Improve error messages
This commit is contained in:
Ross Goldberg 2024-10-29 05:57:14 -04:00 committed by GitHub
commit 6d443993ec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 235 additions and 240 deletions

View file

@ -10,21 +10,53 @@ import CommerceKit
import PromiseKit
import StoreFoundation
/// Downloads a list of apps, one after the other, printing progress to the console.
/// Sequentially downloads apps, printing progress to the console.
///
/// Verifies that each supplied app ID is valid before attempting to download.
///
/// - Parameters:
/// - appIDs: The IDs of the apps to be downloaded
/// - purchase: Flag indicating whether the apps needs to be purchased.
/// Only works for free apps. Defaults to false.
/// - unverifiedAppIDs: The app IDs of the apps to be verified and downloaded.
/// - searcher: The `AppStoreSearcher` used to verify app IDs.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A `Promise` that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
func downloadApps(
withAppIDs unverifiedAppIDs: [AppID],
verifiedBy searcher: AppStoreSearcher,
purchasing: Bool = false
) -> Promise<Void> {
when(resolved: unverifiedAppIDs.map { searcher.lookup(appID: $0) })
.then { results in
downloadApps(
withAppIDs:
results.compactMap { result in
switch result {
case .fulfilled(let searchResult):
return searchResult.trackId
case .rejected(let error):
printError(String(describing: error))
return nil
}
},
purchasing: purchasing
)
}
}
/// Sequentially downloads apps, printing progress to the console.
///
/// - Parameters:
/// - appIDs: The app IDs of the apps to be downloaded.
/// - purchasing: Flag indicating if the apps will be purchased. Only works for free apps. Defaults to false.
/// - Returns: A promise that completes when the downloads are complete. If any fail,
/// the promise is rejected with the first error, after all remaining downloads are attempted.
func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
func downloadApps(withAppIDs appIDs: [AppID], purchasing: Bool = false) -> Promise<Void> {
var firstError: Error?
return
appIDs
.reduce(Guarantee.value(())) { previous, appID in
previous.then {
downloadWithRetries(appID, purchase: purchase)
downloadApp(withAppID: appID, purchasing: purchasing)
.recover { error in
if firstError == nil {
firstError = error
@ -39,10 +71,15 @@ func downloadAll(_ appIDs: [AppID], purchase: Bool = false) -> Promise<Void> {
}
}
private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempts: Int = 3) -> Promise<Void> {
SSPurchase().perform(appID: appID, purchase: purchase)
private func downloadApp(
withAppID appID: AppID,
purchasing: Bool = false,
withAttemptCount attemptCount: UInt32 = 3
) -> Promise<Void> {
SSPurchase()
.perform(appID: appID, purchasing: purchasing)
.recover { error in
guard attempts > 1 else {
guard attemptCount > 1 else {
throw error
}
@ -54,9 +91,9 @@ private func downloadWithRetries(_ appID: AppID, purchase: Bool = false, attempt
throw error
}
let attempts = attempts - 1
let attemptCount = attemptCount - 1
printWarning((downloadError ?? error).localizedDescription)
printWarning("Trying again up to \(attempts) more \(attempts == 1 ? "time" : "times").")
return downloadWithRetries(appID, purchase: purchase, attempts: attempts)
printWarning("Trying again up to \(attemptCount) more \(attemptCount == 1 ? "time" : "times").")
return downloadApp(withAppID: appID, purchasing: purchasing, withAttemptCount: attemptCount)
}
}

View file

@ -11,7 +11,7 @@ import PromiseKit
import StoreFoundation
extension SSPurchase {
func perform(appID: AppID, purchase: Bool) -> Promise<Void> {
func perform(appID: AppID, purchasing: Bool) -> Promise<Void> {
var parameters: [String: Any] = [
"productType": "C",
"price": 0,
@ -20,9 +20,11 @@ extension SSPurchase {
"appExtVrsId": 0,
]
if purchase {
if purchasing {
parameters["macappinstalledconfirmed"] = 1
parameters["pricingParameters"] = "STDQ"
// Possibly unnecessary
isRedownload = false
} else {
parameters["pricingParameters"] = "STDRDL"
}
@ -35,11 +37,6 @@ extension SSPurchase {
itemIdentifier = appID
// Not sure if this is needed
if purchase {
isRedownload = false
}
downloadMetadata = SSDownloadMetadata()
downloadMetadata.kind = "software"
downloadMetadata.itemIdentifier = appID

View file

@ -26,9 +26,7 @@ extension MAS {
}
func run(searcher: AppStoreSearcher) throws {
guard let result = try searcher.lookup(appID: appID).wait() else {
throw MASError.noSearchResultsFound
}
let result = try searcher.lookup(appID: appID).wait()
guard let url = URL(string: result.trackViewUrl) else {
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")

View file

@ -27,11 +27,7 @@ extension MAS {
func run(searcher: AppStoreSearcher) throws {
do {
guard let result = try searcher.lookup(appID: appID).wait() else {
throw MASError.noSearchResultsFound
}
print(AppInfoFormatter.format(app: result))
print(AppInfoFormatter.format(app: try searcher.lookup(appID: appID).wait()))
} catch {
throw error as? MASError ?? .searchFailed
}

View file

@ -18,15 +18,15 @@ extension MAS {
@Flag(help: "Force reinstall")
var force = false
@Argument(help: "App ID(s)")
@Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
var appIDs: [AppID]
/// Runs the command.
func run() throws {
try run(appLibrary: SoftwareMapAppLibrary())
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
}
func run(appLibrary: AppLibrary) throws {
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName, !force {
@ -38,7 +38,7 @@ extension MAS {
}
do {
try downloadAll(appIDs).wait()
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}

View file

@ -38,7 +38,6 @@ extension MAS {
do {
let results = try searcher.search(for: searchTerm).wait()
guard let result = results.first else {
printError("No results found")
throw MASError.noSearchResultsFound
}
@ -66,7 +65,7 @@ extension MAS {
printWarning("\(appName) is already installed")
} else {
do {
try downloadAll([appID]).wait()
try downloadApps(withAppIDs: [appID]).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}

View file

@ -63,9 +63,7 @@ private func openMacAppStore() -> Promise<Void> {
}
private func openInMacAppStore(pageForAppID appID: AppID, searcher: AppStoreSearcher) throws {
guard let result = try searcher.lookup(appID: appID).wait() else {
throw MASError.runtimeError("Unknown app ID \(appID)")
}
let result = try searcher.lookup(appID: appID).wait()
guard var urlComponents = URLComponents(string: result.trackViewUrl) else {
throw MASError.runtimeError("Unable to construct URL from: \(result.trackViewUrl)")

View file

@ -30,11 +30,22 @@ extension MAS {
_ = try when(
fulfilled:
appLibrary.installedApps.map { installedApp in
firstly {
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
}
.done { storeApp in
guard let storeApp else {
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
.done { storeApp in
if installedApp.isOutdatedWhenComparedTo(storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
"""
)
}
}
.recover { error in
guard case MASError.unknownAppID = error else {
throw error
}
if verbose {
printWarning(
"""
@ -43,18 +54,7 @@ extension MAS {
"""
)
}
return
}
if installedApp.isOutdatedWhenComparedTo(storeApp) {
print(
"""
\(installedApp.itemIdentifier) \(installedApp.appName) \
(\(installedApp.bundleVersion) -> \(storeApp.version))
"""
)
}
}
}
)
.wait()

View file

@ -15,15 +15,15 @@ extension MAS {
abstract: "\"Purchase\" and install free apps from the Mac App Store"
)
@Argument(help: "App ID(s)")
@Argument(help: ArgumentHelp("App ID", valueName: "app-id"))
var appIDs: [AppID]
/// Runs the command.
func run() throws {
try run(appLibrary: SoftwareMapAppLibrary())
try run(appLibrary: SoftwareMapAppLibrary(), searcher: ITunesSearchAppStoreSearcher())
}
func run(appLibrary: AppLibrary) throws {
func run(appLibrary: AppLibrary, searcher: AppStoreSearcher) throws {
// Try to download applications with given identifiers and collect results
let appIDs = appIDs.filter { appID in
if let appName = appLibrary.installedApps(withAppID: appID).first?.appName {
@ -35,7 +35,7 @@ extension MAS {
}
do {
try downloadAll(appIDs, purchase: true).wait()
try downloadApps(withAppIDs: appIDs, verifiedBy: searcher, purchasing: true).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}

View file

@ -32,12 +32,12 @@ extension MAS {
throw MASError.macOSUserMustBeRoot
}
guard let username = getSudoUsername() else {
guard let username = ProcessInfo.processInfo.sudoUsername else {
throw MASError.runtimeError("Could not determine the original username")
}
guard
let uid = getSudoUID(),
let uid = ProcessInfo.processInfo.sudoUID,
seteuid(uid) == 0
else {
throw MASError.runtimeError("Failed to switch effective user from 'root' to '\(username)'")

View file

@ -18,7 +18,7 @@ extension MAS {
"Upgrade outdated app(s) installed from the Mac App Store"
)
@Argument(help: "App ID(s)/app name(s)")
@Argument(help: ArgumentHelp("App ID/app name", valueName: "app-id-or-name"))
var appIDOrNames: [String] = []
/// Runs the command.
@ -35,7 +35,6 @@ extension MAS {
}
guard !apps.isEmpty else {
printWarning("Nothing found to upgrade")
return
}
@ -46,7 +45,7 @@ extension MAS {
)
do {
try downloadAll(apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
try downloadApps(withAppIDs: apps.map(\.installedApp.itemIdentifier.appIDValue)).wait()
} catch {
throw error as? MASError ?? .downloadFailed(error: error as NSError)
}
@ -59,28 +58,39 @@ extension MAS {
let apps =
appIDOrNames.isEmpty
? appLibrary.installedApps
: appIDOrNames.flatMap { appID in
if let appID = AppID(appID) {
: appIDOrNames.flatMap { appIDOrName in
if let appID = AppID(appIDOrName) {
// argument is an AppID, lookup apps by id using argument
return appLibrary.installedApps(withAppID: appID)
let installedApps = appLibrary.installedApps(withAppID: appID)
if installedApps.isEmpty {
printError(appID.unknownMessage)
}
return installedApps
}
// argument is not an AppID, lookup apps by name using argument
return appLibrary.installedApps(named: appID)
let installedApps = appLibrary.installedApps(named: appIDOrName)
if installedApps.isEmpty {
printError("Unknown app name '\(appIDOrName)'")
}
return installedApps
}
let promises = apps.map { installedApp in
// only upgrade apps whose local version differs from the store version
firstly {
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
}
.map { result -> (SoftwareProduct, SearchResult)? in
guard let storeApp = result, installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
searcher.lookup(appID: installedApp.itemIdentifier.appIDValue)
.map { storeApp -> (SoftwareProduct, SearchResult)? in
guard installedApp.isOutdatedWhenComparedTo(storeApp) else {
return nil
}
return (installedApp, storeApp)
}
.recover { error -> Promise<(SoftwareProduct, SearchResult)?> in
guard case MASError.unknownAppID = error else {
return Promise(error: error)
}
return .value(nil)
}
return (installedApp, storeApp)
}
}
return try when(fulfilled: promises).wait().compactMap { $0 }

View file

@ -26,9 +26,7 @@ extension MAS {
}
func run(searcher: AppStoreSearcher) throws {
guard let result = try searcher.lookup(appID: appID).wait() else {
throw MASError.noSearchResultsFound
}
let result = try searcher.lookup(appID: appID).wait()
guard let urlString = result.sellerUrl else {
throw MASError.noVendorWebsite

View file

@ -14,9 +14,11 @@ protocol AppStoreSearcher {
/// Looks up app details.
///
/// - Parameter appID: App ID.
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
/// or an `Error` if any problems occur.
func lookup(appID: AppID) -> Promise<SearchResult?>
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
/// An `Promise` for some other `Error` if any problems occur.
func lookup(appID: AppID) -> Promise<SearchResult>
/// Searches for apps.
///
/// - Parameter searchTerm: Term for which to search.

View file

@ -32,49 +32,46 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
self.networkManager = networkManager
}
/// Looks up app details.
///
/// - Parameter appID: App ID.
/// - Returns: A `Promise` for the `SearchResult` for the given `appID`, `nil` if no apps match,
/// or an `Error` if any problems occur.
func lookup(appID: AppID) -> Promise<SearchResult?> {
/// - Returns: A `Promise` for the `SearchResult` for the given `appID` if `appID` is valid.
/// A `Promise` for `MASError.unknownAppID(appID)` if `appID` is invalid.
/// An `Promise` for some other `Error` if any problems occur.
func lookup(appID: AppID) -> Promise<SearchResult> {
guard let url = lookupURL(forAppID: appID, inCountry: country) else {
fatalError("Failed to build URL for \(appID)")
}
return firstly {
return
loadSearchResults(url)
}
.then { results -> Guarantee<SearchResult?> in
guard let result = results.first else {
return .value(nil)
}
guard let pageURL = URL(string: result.trackViewUrl) else {
return .value(result)
}
return firstly {
self.scrapeAppStoreVersion(pageURL)
}
.map { pageVersion in
guard
let pageVersion,
let searchVersion = Version(tolerant: result.version),
pageVersion > searchVersion
else {
return result
.then { results -> Guarantee<SearchResult> in
guard let result = results.first else {
throw MASError.unknownAppID(appID)
}
// Update the search result with the version from the App Store page.
var result = result
result.version = pageVersion.description
return result
guard let pageURL = URL(string: result.trackViewUrl) else {
return .value(result)
}
return
self.scrapeAppStoreVersion(pageURL)
.map { pageVersion in
guard
let pageVersion,
let searchVersion = Version(tolerant: result.version),
pageVersion > searchVersion
else {
return result
}
// Update the search result with the version from the App Store page.
var result = result
result.version = pageVersion.description
return result
}
.recover { _ in
// If we were unable to scrape the App Store page, assume compatibility.
.value(result)
}
}
.recover { _ in
// If we were unable to scrape the App Store page, assume compatibility.
.value(result)
}
}
}
/// Searches for apps from the MAS.
@ -106,36 +103,32 @@ class ITunesSearchAppStoreSearcher: AppStoreSearcher {
}
private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> {
firstly {
networkManager.loadData(from: url)
}
.map { data in
do {
return try JSONDecoder().decode(SearchResultList.self, from: data).results
} catch {
throw MASError.jsonParsing(data: data)
networkManager.loadData(from: url)
.map { data in
do {
return try JSONDecoder().decode(SearchResultList.self, from: data).results
} catch {
throw MASError.jsonParsing(data: data)
}
}
}
}
/// Scrape the app version from the App Store webpage at the given URL.
///
/// App Store webpages frequently report a version that is newer than what is reported by the iTunes Search API.
private func scrapeAppStoreVersion(_ pageURL: URL) -> Promise<Version?> {
firstly {
networkManager.loadData(from: pageURL)
}
.map { data in
guard
let html = String(data: data, encoding: .utf8),
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
let version = Version(tolerant: capture)
else {
return nil
}
networkManager.loadData(from: pageURL)
.map { data in
guard
let html = String(data: data, encoding: .utf8),
let capture = Self.appVersionExpression.firstMatch(in: html)?.captures[0],
let version = Version(tolerant: capture)
else {
return nil
}
return version
}
return version
}
}
/// Builds the search URL for an app.

View file

@ -43,24 +43,6 @@ class SoftwareMapAppLibrary: AppLibrary {
}
}
func getSudoUsername() -> String? {
ProcessInfo.processInfo.environment["SUDO_USER"]
}
func getSudoUID() -> uid_t? {
guard let uid = ProcessInfo.processInfo.environment["SUDO_UID"] else {
return nil
}
return uid_t(uid)
}
func getSudoGID() -> gid_t? {
guard let gid = ProcessInfo.processInfo.environment["SUDO_GID"] else {
return nil
}
return gid_t(gid)
}
private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t) {
do {
let attributes = try FileManager.default.attributesOfItem(atPath: path)
@ -75,11 +57,11 @@ private func getOwnerAndGroupOfItem(atPath path: String) throws -> (uid_t, gid_t
}
private func chown(paths: [String]) throws -> [String: (uid_t, gid_t)] {
guard let sudoUID = getSudoUID() else {
guard let sudoUID = ProcessInfo.processInfo.sudoUID else {
throw MASError.runtimeError("Failed to get original uid")
}
guard let sudoGID = getSudoGID() else {
guard let sudoGID = ProcessInfo.processInfo.sudoGID else {
throw MASError.runtimeError("Failed to get original gid")
}

View file

@ -27,6 +27,9 @@ enum MASError: Error, Equatable {
case searchFailed
case noSearchResultsFound
case unknownAppID(AppID)
case noVendorWebsite
case notInstalled(appID: AppID)
@ -82,7 +85,9 @@ extension MASError: CustomStringConvertible {
case .searchFailed:
return "Search failed"
case .noSearchResultsFound:
return "No results found"
return "No apps found"
case .unknownAppID(let appID):
return appID.unknownMessage
case .noVendorWebsite:
return "App does not have a vendor website"
case .notInstalled(let appID):

View file

@ -7,7 +7,6 @@
//
import ArgumentParser
import Foundation
import PromiseKit
@main
@ -55,11 +54,3 @@ struct MAS: ParsableCommand {
Self.initialize()
}
}
typealias AppID = UInt64
extension NSNumber {
var appIDValue: AppID {
uint64Value
}
}

View file

@ -0,0 +1,23 @@
//
// AppID.swift
// mas
//
// Created by Ross Goldberg on 2024-10-29.
// Copyright © 2024 mas-cli. All rights reserved.
//
import Foundation
typealias AppID = UInt64
extension AppID {
var unknownMessage: String {
"Unknown app ID \(self)"
}
}
extension NSNumber {
var appIDValue: AppID {
uint64Value
}
}

View file

@ -0,0 +1,29 @@
//
// ProcessInfo.swift
// mas
//
// Created by Ross Goldberg on 2024-10-29.
// Copyright © 2024 mas-cli. All rights reserved.
//
import Foundation
extension ProcessInfo {
var sudoUsername: String? {
environment["SUDO_USER"]
}
var sudoUID: uid_t? {
guard let uid = environment["SUDO_UID"] else {
return nil
}
return uid_t(uid)
}
var sudoGID: gid_t? {
guard let gid = environment["SUDO_GID"] else {
return nil
}
return gid_t(gid)
}
}

View file

@ -22,28 +22,11 @@ public class HomeSpec: QuickSpec {
beforeEach {
searcher.reset()
}
it("fails to open app with invalid ID") {
expect {
try MAS.Home.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
try MAS.Home.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
it("opens app on MAS Preview") {
let mockResult = SearchResult(
trackId: 1111,
trackViewUrl: "mas preview url",
version: "0.0"
)
searcher.apps[mockResult.trackId] = mockResult
expect {
try MAS.Home.parse([String(mockResult.trackId)]).run(searcher: searcher)
}
.to(throwError(MASError.unknownAppID(999)))
}
}
}

View file

@ -23,17 +23,11 @@ public class InfoSpec: QuickSpec {
beforeEach {
searcher.reset()
}
it("fails to open app with invalid ID") {
expect {
try MAS.Info.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
try MAS.Info.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
.to(throwError(MASError.unknownAppID(999)))
}
it("displays app details") {
let mockResult = SearchResult(

View file

@ -19,7 +19,7 @@ public class InstallSpec: QuickSpec {
xdescribe("install command") {
xit("installs apps") {
expect {
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary())
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
}

View file

@ -23,33 +23,11 @@ public class OpenSpec: QuickSpec {
beforeEach {
searcher.reset()
}
it("fails to open app with invalid ID") {
expect {
try MAS.Open.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
try MAS.Open.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
xit("opens app in MAS") {
let mockResult = SearchResult(
trackId: 1111,
trackViewUrl: "fakescheme://some/url",
version: "0.0"
)
searcher.apps[mockResult.trackId] = mockResult
expect {
try MAS.Open.parse([mockResult.trackId.description]).run(searcher: searcher)
}
}
xit("just opens MAS if no app specified") {
expect {
try MAS.Open.parse([]).run(searcher: searcher)
}
.to(throwError(MASError.unknownAppID(999)))
}
}
}

View file

@ -19,7 +19,7 @@ public class PurchaseSpec: QuickSpec {
xdescribe("purchase command") {
xit("purchases apps") {
expect {
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary())
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
}

View file

@ -25,7 +25,7 @@ public class UpgradeSpec: QuickSpec {
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
}
== "Warning: Nothing found to upgrade\n"
.toNot(throwError())
}
}
}

View file

@ -22,29 +22,11 @@ public class VendorSpec: QuickSpec {
beforeEach {
searcher.reset()
}
it("fails to open app with invalid ID") {
expect {
try MAS.Vendor.parse(["--", "-999"]).run(searcher: searcher)
}
.to(throwError())
}
it("can't find app with unknown ID") {
expect {
try MAS.Vendor.parse(["999"]).run(searcher: searcher)
}
.to(throwError(MASError.noSearchResultsFound))
}
it("opens vendor app page in browser") {
let mockResult = SearchResult(
sellerUrl: "https://awesome.app",
trackId: 1111,
trackViewUrl: "https://apps.apple.com/us/app/awesome/id1111?mt=12&uo=4",
version: "0.0"
)
searcher.apps[mockResult.trackId] = mockResult
expect {
try MAS.Vendor.parse([String(mockResult.trackId)]).run(searcher: searcher)
}
.to(throwError(MASError.unknownAppID(999)))
}
}
}

View file

@ -17,9 +17,9 @@ class MockAppStoreSearcher: AppStoreSearcher {
.value(apps.filter { $1.trackName.contains(searchTerm) }.map { $1 })
}
func lookup(appID: AppID) -> Promise<SearchResult?> {
func lookup(appID: AppID) -> Promise<SearchResult> {
guard let result = apps[appID] else {
return Promise(error: MASError.noSearchResultsFound)
return Promise(error: MASError.unknownAppID(appID))
}
return .value(result)

View file

@ -100,7 +100,7 @@ class MASErrorTestCase: XCTestCase {
func testNoSearchResultsFound() {
error = .noSearchResultsFound
XCTAssertEqual(error.description, "No results found")
XCTAssertEqual(error.description, "No apps found")
}
func testNoVendorWebsite() {