mirror of
https://github.com/mas-cli/mas
synced 2024-11-22 03:23:08 +00:00
Merge pull request #505 from mas-cli/search
🖥️ Only search, outdated and update macOS apps
This commit is contained in:
commit
21ed1e641f
22 changed files with 216 additions and 199 deletions
|
@ -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
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -84,6 +84,6 @@ public struct OutdatedOptions: OptionsProtocol {
|
|||
|
||||
public static func evaluate(_ mode: CommandMode) -> Result<OutdatedOptions, CommandantError<MASError>> {
|
||||
create
|
||||
<*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps")
|
||||
<*> mode <| Switch(flag: nil, key: "verbose", usage: "Show warnings about apps")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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<Int>()
|
||||
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<SearchResult?> {
|
||||
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<SearchResult?> 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<Void>? 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<Version?> {
|
||||
// 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<Version?> {
|
||||
firstly {
|
||||
networkManager.loadData(from: pageUrl)
|
||||
}.map { data in
|
||||
let html = String(decoding: data, as: UTF8.self)
|
||||
let fullRange = NSRange(html.startIndex..<html.endIndex, in: html)
|
||||
guard let match = MasStoreSearch.versionExpression.firstMatch(in: html, range: fullRange),
|
||||
let range = Range(match.range(at: 1), in: html),
|
||||
let version = Version(tolerant: html[range])
|
||||
guard let capture = MasStoreSearch.appVersionExpression.firstMatch(in: html)?.captures[0],
|
||||
let version = Version(tolerant: capture)
|
||||
else {
|
||||
throw MASError.noData
|
||||
return nil
|
||||
}
|
||||
|
||||
return version
|
||||
}.recover { _ in
|
||||
.value(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,25 +15,31 @@ protocol StoreSearch {
|
|||
func search(for appName: String) -> 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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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) ?? ""
|
||||
|
|
40
Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift
Normal file
40
Sources/MasKit/ExternalCommands/SysCtlSystemCommand.swift
Normal file
|
@ -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"
|
||||
}()
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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") {
|
||||
|
|
|
@ -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<SearchResult?> {
|
||||
.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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
|
|
Loading…
Reference in a new issue