diff --git a/Package.resolved b/Package.resolved index 23ad6e5..ba46a29 100644 --- a/Package.resolved +++ b/Package.resolved @@ -55,49 +55,40 @@ "version": "5.0.1" } }, + { + "package": "Regex", + "repositoryURL": "https://github.com/sharplet/Regex.git", + "state": { + "branch": null, + "revision": "76c2b73d4281d77fc3118391877efd1bf972f515", + "version": "2.1.1" + } + }, { "package": "swift-argument-parser", "repositoryURL": "https://github.com/apple/swift-argument-parser.git", "state": { "branch": null, - "revision": "9f39744e025c7d377987f30b03770805dcb0bcd1", - "version": "1.1.4" + "revision": "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version": "1.3.0" } }, { "package": "swift-format", "repositoryURL": "https://github.com/apple/swift-format", "state": { - "branch": "release/5.7", - "revision": "3dd9b517b9e9846435aa782d769ef5825e7c2d65", + "branch": "release/5.9", + "revision": "1323e87eced56bdcfed1bb78af1f16f39274d032", "version": null } }, { - "package": "SwiftSyntax", - "repositoryURL": "https://github.com/apple/swift-syntax", + "package": "swift-syntax", + "repositoryURL": "https://github.com/apple/swift-syntax.git", "state": { - "branch": null, - "revision": "72d3da66b085c2299dd287c2be3b92b5ebd226de", - "version": "0.50700.1" - } - }, - { - "package": "swift-system", - "repositoryURL": "https://github.com/apple/swift-system.git", - "state": { - "branch": null, - "revision": "836bc4557b74fe6d2660218d56e3ce96aff76574", - "version": "1.1.1" - } - }, - { - "package": "swift-tools-support-core", - "repositoryURL": "https://github.com/apple/swift-tools-support-core.git", - "state": { - "branch": null, - "revision": "4f07be3dc201f6e2ee85b6942d0c220a16926811", - "version": "0.2.7" + "branch": "release/5.9", + "revision": "9a101b70eee2a9dec04f92d2d47b22ebe57a1aae", + "version": null } }, { diff --git a/Package.swift b/Package.swift index f730174..15da902 100644 --- a/Package.swift +++ b/Package.swift @@ -26,6 +26,7 @@ let package = Package( .package(url: "https://github.com/Quick/Quick.git", from: "5.0.0"), .package(url: "https://github.com/mxcl/PromiseKit.git", from: "6.16.2"), .package(url: "https://github.com/mxcl/Version.git", from: "2.0.1"), + .package(url: "https://github.com/sharplet/Regex.git", from: "2.1.1"), ], targets: [ // Targets are the basic building blocks of a package. A target can define a module or a test suite. @@ -42,7 +43,12 @@ let package = Package( ), .target( name: "MasKit", - dependencies: ["Commandant", "PromiseKit", "Version"], + dependencies: [ + "Commandant", + "PromiseKit", + "Regex", + "Version", + ], swiftSettings: [ .unsafeFlags([ "-I", "Sources/PrivateFrameworks/CommerceKit", diff --git a/Sources/MasKit/Commands/Outdated.swift b/Sources/MasKit/Commands/Outdated.swift index e433d78..ed3545f 100644 --- a/Sources/MasKit/Commands/Outdated.swift +++ b/Sources/MasKit/Commands/Outdated.swift @@ -84,6 +84,6 @@ public struct OutdatedOptions: OptionsProtocol { public static func evaluate(_ mode: CommandMode) -> Result> { create - <*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps") + <*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps") } } diff --git a/Sources/MasKit/Commands/Upgrade.swift b/Sources/MasKit/Commands/Upgrade.swift index 638bced..1acf0f0 100644 --- a/Sources/MasKit/Commands/Upgrade.swift +++ b/Sources/MasKit/Commands/Upgrade.swift @@ -65,18 +65,18 @@ public struct UpgradeCommand: CommandProtocol { } private func findOutdatedApps(_ options: Options) throws -> [(SoftwareProduct, SearchResult)] { - let apps: [SoftwareProduct] = options.apps.isEmpty + let apps: [SoftwareProduct] = + options.apps.isEmpty ? appLibrary.installedApps - : - options.apps.compactMap { - if let appId = UInt64($0) { - // if argument a UInt64, lookup app by id using argument - return appLibrary.installedApp(forId: appId) - } else { - // if argument not a UInt64, lookup app by name using argument - return appLibrary.installedApp(named: $0) - } + : options.apps.compactMap { + if let appId = UInt64($0) { + // if argument a UInt64, lookup app by id using argument + return appLibrary.installedApp(forId: appId) + } else { + // if argument not a UInt64, lookup app by name using argument + return appLibrary.installedApp(named: $0) } + } let promises = apps.map { installedApp in // only upgrade apps whose local version differs from the store version diff --git a/Sources/MasKit/Controllers/MasStoreSearch.swift b/Sources/MasKit/Controllers/MasStoreSearch.swift index 2f14575..f5a32e4 100644 --- a/Sources/MasKit/Controllers/MasStoreSearch.swift +++ b/Sources/MasKit/Controllers/MasStoreSearch.swift @@ -8,21 +8,27 @@ import Foundation import PromiseKit +import Regex import Version /// Manages searching the MAS catalog through the iTunes Search and Lookup APIs. class MasStoreSearch: StoreSearch { + private static let appVersionExpression = Regex(#"\"versionDisplay\"\:\"([^\"]+)\""#) + + // CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed + // into the App Store. Instead, we'll make an educated guess that it matches the currently + // selected locale in macOS. This obviously isn't always going to match, but it's probably + // better than passing no "country" at all to the iTunes Search API. + // https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ + private let country: String? private let networkManager: NetworkManager - private static let versionExpression: NSRegularExpression = { - do { - return try NSRegularExpression(pattern: #"\"versionDisplay\"\:\"([^\"]+)\""#) - } catch { - fatalError("Unexpected error initializing NSRegularExpression: \(error.localizedDescription)") - } - }() /// Designated initializer. - init(networkManager: NetworkManager = NetworkManager()) { + init( + country: String? = Locale.autoupdatingCurrent.regionCode, + networkManager: NetworkManager = NetworkManager() + ) { + self.country = country self.networkManager = networkManager } @@ -32,12 +38,25 @@ class MasStoreSearch: StoreSearch { /// - Parameter completion: A closure that receives the search results or an Error if there is a /// problem with the network request. Results array will be empty if there were no matches. func search(for appName: String) -> Promise<[SearchResult]> { - guard let url = searchURL(for: appName) - else { - return Promise(error: MASError.urlEncoding) + // Search for apps for compatible platforms, in order of preference. + // Macs with Apple Silicon can run iPad and iPhone apps. + var entities = [Entity.macSoftware] + if SysCtlSystemCommand.isAppleSilicon { + entities += [.iPadSoftware, .iPhoneSoftware] } - return loadSearchResults(url) + let results = entities.map { entity -> Promise<[SearchResult]> in + guard let url = searchURL(for: appName, inCountry: country, ofEntity: entity) else { + fatalError("Failed to build URL for \(appName)") + } + return loadSearchResults(url) + } + + // Combine the results, removing any duplicates. + var seenAppIDs = Set() + return when(fulfilled: results).flatMapValues { $0 }.filterValues { result in + seenAppIDs.insert(result.trackId).inserted + } } /// Looks up app details. @@ -46,64 +65,71 @@ class MasStoreSearch: StoreSearch { /// - Returns: A Promise for the search result record of app, or nil if no apps match the ID, /// or an Error if there is a problem with the network request. func lookup(app appId: Int) -> Promise { - guard let url = lookupURL(forApp: appId) - else { - return Promise(error: MASError.urlEncoding) + guard let url = lookupURL(forApp: appId, inCountry: country) else { + fatalError("Failed to build URL for \(appId)") } + return firstly { + loadSearchResults(url) + }.then { results -> Guarantee in + guard let result = results.first else { + return .value(nil) + } - return loadSearchResults(url).map { results in results.first } + 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 + } + + // 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) + } + } } private func loadSearchResults(_ url: URL) -> Promise<[SearchResult]> { firstly { networkManager.loadData(from: url) - }.map { data -> SearchResultList in + }.map { data -> [SearchResult] in do { - return try JSONDecoder().decode(SearchResultList.self, from: data) + return try JSONDecoder().decode(SearchResultList.self, from: data).results } catch { throw MASError.jsonParsing(error: error as NSError) } - }.then { list -> Promise<[SearchResult]> in - var results = list.results - let scraping = results.indices.compactMap { index -> Guarantee? in - let result = results[index] - guard let searchVersion = Version(tolerant: result.version), - let pageUrl = URL(string: result.trackViewUrl) - else { - return nil - } - - return firstly { - self.scrapeVersionFromPage(pageUrl) - }.done { pageVersion in - if let pageVersion, pageVersion > searchVersion { - results[index].version = pageVersion.description - } - } - } - - return when(fulfilled: scraping).map { results } } } - // The App Store often lists a newer version available in an app's page than in - // the search results. We attempt to scrape it here. - private func scrapeVersionFromPage(_ pageUrl: URL) -> Guarantee { + // App Store pages indicate: + // - compatibility with Macs with Apple Silicon + // - (often) a version that is newer than what is listed in search results + // + // We attempt to scrape this information here. + private func scrapeAppStoreVersion(_ pageUrl: URL) -> Promise { firstly { networkManager.loadData(from: pageUrl) }.map { data in let html = String(decoding: data, as: UTF8.self) - let fullRange = NSRange(html.startIndex.. Promise<[SearchResult]> } +enum Entity: String { + case macSoftware + case iPadSoftware + case iPhoneSoftware = "software" +} + // MARK: - Common methods extension StoreSearch { /// Builds the search URL for an app. /// /// - Parameter appName: MAS app identifier. /// - Returns: URL for the search service or nil if appName can't be encoded. - func searchURL(for appName: String) -> URL? { + func searchURL(for appName: String, inCountry country: String?, ofEntity entity: Entity = .macSoftware) -> URL? { guard var components = URLComponents(string: "https://itunes.apple.com/search") else { return nil } components.queryItems = [ URLQueryItem(name: "media", value: "software"), - URLQueryItem(name: "entity", value: "macSoftware"), + URLQueryItem(name: "entity", value: entity.rawValue), URLQueryItem(name: "term", value: appName), ] if let country { - components.queryItems!.append(country) + components.queryItems!.append(URLQueryItem(name: "country", value: country)) } return components.url @@ -43,7 +49,7 @@ extension StoreSearch { /// /// - Parameter appId: MAS app identifier. /// - Returns: URL for the lookup service or nil if appId can't be encoded. - func lookupURL(forApp appId: Int) -> URL? { + func lookupURL(forApp appId: Int, inCountry country: String?) -> URL? { guard var components = URLComponents(string: "https://itunes.apple.com/lookup") else { return nil } @@ -54,22 +60,9 @@ extension StoreSearch { ] if let country { - components.queryItems!.append(country) + components.queryItems!.append(URLQueryItem(name: "country", value: country)) } return components.url } - - private var country: URLQueryItem? { - // CommerceKit and StoreFoundation don't seem to expose the region of the Apple ID signed - // into the App Store. Instead, we'll make an educated guess that it matches the currently - // selected locale in macOS. This obviously isn't always going to match, but it's probably - // better than passing no "country" at all to the iTunes Search API. - // https://affiliate.itunes.apple.com/resources/documentation/itunes-store-web-service-search-api/ - guard let region = Locale.autoupdatingCurrent.regionCode else { - return nil - } - - return URLQueryItem(name: "country", value: region) - } } diff --git a/Sources/MasKit/Errors/MASError.swift b/Sources/MasKit/Errors/MASError.swift index 4fc0648..716f4ea 100644 --- a/Sources/MasKit/Errors/MASError.swift +++ b/Sources/MasKit/Errors/MASError.swift @@ -27,7 +27,6 @@ public enum MASError: Error, Equatable { case notInstalled case uninstallFailed - case urlEncoding case noData case jsonParsing(error: NSError?) } @@ -91,9 +90,6 @@ extension MASError: CustomStringConvertible { case .uninstallFailed: return "Uninstall failed" - case .urlEncoding: - return "Unable to encode service URL" - case .noData: return "Service did not return data" diff --git a/Sources/MasKit/ExternalCommands/ExternalCommand.swift b/Sources/MasKit/ExternalCommands/ExternalCommand.swift index caf3ef0..785bc84 100644 --- a/Sources/MasKit/ExternalCommands/ExternalCommand.swift +++ b/Sources/MasKit/ExternalCommands/ExternalCommand.swift @@ -14,6 +14,7 @@ protocol ExternalCommand { var process: Process { get } + var stdout: String { get } var stderr: String { get } var stdoutPipe: Pipe { get } var stderrPipe: Pipe { get } @@ -28,6 +29,11 @@ protocol ExternalCommand { /// Common implementation extension ExternalCommand { + var stdout: String { + let data = stdoutPipe.fileHandleForReading.readDataToEndOfFile() + return String(data: data, encoding: .utf8) ?? "" + } + var stderr: String { let data = stderrPipe.fileHandleForReading.readDataToEndOfFile() return String(data: data, encoding: .utf8) ?? "" diff --git a/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift b/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift new file mode 100644 index 0000000..3c2a401 --- /dev/null +++ b/Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift @@ -0,0 +1,40 @@ +// +// SysCtlSystemCommand.swift +// MasKit +// +// Created by Chris Araman on 6/3/21. +// Copyright © 2021 mas-cli. All rights reserved. +// + +import Foundation + +/// Wrapper for the external sysctl system command. +/// https://ss64.com/osx/sysctl.html +struct SysCtlSystemCommand: ExternalCommand { + var binaryPath: String + + let process = Process() + + let stdoutPipe = Pipe() + let stderrPipe = Pipe() + + init(binaryPath: String = "/usr/sbin/sysctl") { + self.binaryPath = binaryPath + } + + static var isAppleSilicon: Bool = { + let sysctl = SysCtlSystemCommand() + do { + // Returns 1 on Apple Silicon even when run in an Intel context in Rosetta. + try sysctl.run(arguments: "-in", "hw.optional.arm64") + } catch { + fatalError("sysctl failed") + } + + guard sysctl.succeeded else { + fatalError("sysctl failed") + } + + return sysctl.stdout.trimmingCharacters(in: .newlines) == "1" + }() +} diff --git a/Tests/MasKitTests/Commands/AccountCommandSpec.swift b/Tests/MasKitTests/Commands/AccountCommandSpec.swift index f2473cd..b78f16e 100644 --- a/Tests/MasKitTests/Commands/AccountCommandSpec.swift +++ b/Tests/MasKitTests/Commands/AccountCommandSpec.swift @@ -11,17 +11,18 @@ import Quick @testable import MasKit +// Deprecated test public class AccountCommandSpec: QuickSpec { override public func spec() { beforeSuite { MasKit.initialize() } - describe("Account command") { - it("displays active account") { + // account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + xdescribe("Account command") { + xit("displays active account") { let cmd = AccountCommand() let result = cmd.run(AccountCommand.Options()) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/InstallCommandSpec.swift b/Tests/MasKitTests/Commands/InstallCommandSpec.swift index 06b4bcd..a870649 100644 --- a/Tests/MasKitTests/Commands/InstallCommandSpec.swift +++ b/Tests/MasKitTests/Commands/InstallCommandSpec.swift @@ -20,8 +20,7 @@ public class InstallCommandSpec: QuickSpec { it("installs apps") { let cmd = InstallCommand() let result = cmd.run(InstallCommand.Options(appIds: [], forceInstall: false)) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/ListCommandSpec.swift b/Tests/MasKitTests/Commands/ListCommandSpec.swift index 7b47f84..2901753 100644 --- a/Tests/MasKitTests/Commands/ListCommandSpec.swift +++ b/Tests/MasKitTests/Commands/ListCommandSpec.swift @@ -17,11 +17,10 @@ public class ListCommandSpec: QuickSpec { MasKit.initialize() } describe("list command") { - it("lists stuff") { + it("lists apps") { let list = ListCommand() let result = list.run(ListCommand.Options()) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift b/Tests/MasKitTests/Commands/LuckyCommandSpec.swift index ec7cd03..034d15c 100644 --- a/Tests/MasKitTests/Commands/LuckyCommandSpec.swift +++ b/Tests/MasKitTests/Commands/LuckyCommandSpec.swift @@ -13,15 +13,17 @@ import Quick public class LuckyCommandSpec: QuickSpec { override public func spec() { + let networkSession = NetworkSessionMockFromFile(responseFile: "search/slack.json") + let storeSearch = MasStoreSearch(networkManager: NetworkManager(session: networkSession)) + beforeSuite { MasKit.initialize() } describe("lucky command") { - it("installs the first app matching a search") { - let cmd = LuckyCommand() - let result = cmd.run(LuckyCommand.Options(appName: "", forceInstall: false)) - print(result) - // expect(result).to(beSuccess()) + xit("installs the first app matching a search") { + let cmd = LuckyCommand(storeSearch: storeSearch) + let result = cmd.run(LuckyCommand.Options(appName: "Slack", forceInstall: false)) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift b/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift index 6ead96d..ce85977 100644 --- a/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift +++ b/Tests/MasKitTests/Commands/PurchaseCommandSpec.swift @@ -20,8 +20,11 @@ public class PurchaseCommandSpec: QuickSpec { it("purchases apps") { let cmd = PurchaseCommand() let result = cmd.run(PurchaseCommand.Options(appIds: [])) - print(result) - // expect(result).to(beSuccess()) + expect(result) + .to( + beFailure { error in + expect(error) == .notSupported + }) } } } diff --git a/Tests/MasKitTests/Commands/ResetCommandSpec.swift b/Tests/MasKitTests/Commands/ResetCommandSpec.swift index cefa4d3..6150274 100644 --- a/Tests/MasKitTests/Commands/ResetCommandSpec.swift +++ b/Tests/MasKitTests/Commands/ResetCommandSpec.swift @@ -17,11 +17,10 @@ public class ResetCommandSpec: QuickSpec { MasKit.initialize() } describe("reset command") { - it("updates stuff") { + it("resets the App Store state") { let cmd = ResetCommand() let result = cmd.run(ResetCommand.Options(debug: false)) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/SignInCommandSpec.swift b/Tests/MasKitTests/Commands/SignInCommandSpec.swift index 7d386ce..5f44888 100644 --- a/Tests/MasKitTests/Commands/SignInCommandSpec.swift +++ b/Tests/MasKitTests/Commands/SignInCommandSpec.swift @@ -11,17 +11,18 @@ import Quick @testable import MasKit +// Deprecated test public class SignInCommandSpec: QuickSpec { override public func spec() { beforeSuite { MasKit.initialize() } - describe("signn command") { - it("updates stuff") { + // account command disabled since macOS 10.13 High Sierra https://github.com/mas-cli/mas#%EF%B8%8F-known-issues + xdescribe("signin command") { + xit("signs in") { let cmd = SignInCommand() let result = cmd.run(SignInCommand.Options(username: "", password: "", dialog: false)) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift b/Tests/MasKitTests/Commands/SignOutCommandSpec.swift index 2e13782..6a6bd61 100644 --- a/Tests/MasKitTests/Commands/SignOutCommandSpec.swift +++ b/Tests/MasKitTests/Commands/SignOutCommandSpec.swift @@ -17,11 +17,10 @@ public class SignOutCommandSpec: QuickSpec { MasKit.initialize() } describe("signout command") { - it("updates stuff") { + it("signs out") { let cmd = SignOutCommand() let result = cmd.run(SignOutCommand.Options()) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift b/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift index a41cb1c..8a544d3 100644 --- a/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift +++ b/Tests/MasKitTests/Commands/UpgradeCommandSpec.swift @@ -17,11 +17,10 @@ public class UpgradeCommandSpec: QuickSpec { MasKit.initialize() } describe("upgrade command") { - it("updates stuff") { + it("upgrades stuff") { let cmd = UpgradeCommand() let result = cmd.run(UpgradeCommand.Options(apps: [""])) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Commands/VersionCommandSpec.swift b/Tests/MasKitTests/Commands/VersionCommandSpec.swift index b0fe53a..ba10acc 100644 --- a/Tests/MasKitTests/Commands/VersionCommandSpec.swift +++ b/Tests/MasKitTests/Commands/VersionCommandSpec.swift @@ -20,8 +20,7 @@ public class VersionCommandSpec: QuickSpec { it("displays the current version") { let cmd = VersionCommand() let result = cmd.run(VersionCommand.Options()) - print(result) - // expect(result).to(beSuccess()) + expect(result).to(beSuccess()) } } } diff --git a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift b/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift index f709fa3..5fed9e5 100644 --- a/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift +++ b/Tests/MasKitTests/Controllers/MasStoreSearchSpec.swift @@ -16,6 +16,23 @@ public class MasStoreSearchSpec: QuickSpec { beforeSuite { MasKit.initialize() } + describe("url string") { + it("contains the app name") { + let appName = "myapp" + let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString + expect(urlString) == """ + https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appName)&country=US + """ + } + it("contains the encoded app name") { + let appName = "My App" + let appNameEncoded = "My%20App" + let urlString = MasStoreSearch().searchURL(for: appName, inCountry: "US")?.absoluteString + expect(urlString) == """ + https://itunes.apple.com/search?media=software&entity=macSoftware&term=\(appNameEncoded)&country=US + """ + } + } describe("store") { context("when searched") { it("can find slack") { diff --git a/Tests/MasKitTests/Controllers/StoreSearchSpec.swift b/Tests/MasKitTests/Controllers/StoreSearchSpec.swift deleted file mode 100644 index c0cb6d1..0000000 --- a/Tests/MasKitTests/Controllers/StoreSearchSpec.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// StoreSearchSpec.swift -// MasKitTests -// -// Created by Ben Chatelain on 1/11/19. -// Copyright © 2019 mas-cli. All rights reserved. -// - -import Foundation -import Nimble -import PromiseKit -import Quick - -@testable import MasKit - -/// Protocol minimal implementation -struct StoreSearchForTesting: StoreSearch { - func lookup(app _: Int) -> Promise { - .value(nil) - } - - func search(for _: String) -> Promise<[SearchResult]> { - .value([]) - } -} - -public class StoreSearchSpec: QuickSpec { - override public func spec() { - let storeSearch = StoreSearchForTesting() - let region = Locale.autoupdatingCurrent.regionCode! - - describe("url string") { - it("contains the app name") { - let appName = "myapp" - let urlString = storeSearch.searchURL(for: appName)?.absoluteString - expect(urlString) == "https://itunes.apple.com/search?" - + "media=software&entity=macSoftware&term=\(appName)&country=\(region)" - } - it("contains the encoded app name") { - let appName = "My App" - let appNameEncoded = "My%20App" - let urlString = storeSearch.searchURL(for: appName)?.absoluteString - expect(urlString) == "https://itunes.apple.com/search?" - + "media=software&entity=macSoftware&term=\(appNameEncoded)&country=\(region)" - } - // Find a character that causes addingPercentEncoding(withAllowedCharacters to return nil - xit("is nil when app name cannot be url encoded") { - let appName = "`~!@#$%^&*()_+ 💩" - let urlString = storeSearch.searchURL(for: appName)?.absoluteString - expect(urlString).to(beNil()) - } - } - } -} diff --git a/Tests/MasKitTests/Errors/MASErrorTestCase.swift b/Tests/MasKitTests/Errors/MASErrorTestCase.swift index ed53777..cdefffd 100644 --- a/Tests/MasKitTests/Errors/MASErrorTestCase.swift +++ b/Tests/MasKitTests/Errors/MASErrorTestCase.swift @@ -116,11 +116,6 @@ class MASErrorTestCase: XCTestCase { XCTAssertEqual(error.description, "Uninstall failed") } - func testUrlEncoding() { - error = .urlEncoding - XCTAssertEqual(error.description, "Unable to encode service URL") - } - func testNoData() { error = .noData XCTAssertEqual(error.description, "Service did not return data")