2
0
Fork 0
mirror of https://github.com/mas-cli/mas synced 2025-03-06 23:57:21 +00:00

Merge pull request from rgoldberg/596-testing

Improve testing
This commit is contained in:
Ross Goldberg 2025-01-01 22:45:46 -05:00 committed by GitHub
commit 441991a791
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
32 changed files with 363 additions and 577 deletions

View file

@ -8,7 +8,6 @@
opt_in_rules:
- all
disabled_rules:
- balanced_xctest_lifecycle
- closure_body_length
- contrasted_opening_brace
- explicit_acl
@ -17,7 +16,6 @@ disabled_rules:
- explicit_type_interface
- file_header
- file_name
- final_test_case
- force_unwrapping
- function_body_length
- inert_defer
@ -26,7 +24,6 @@ disabled_rules:
- no_grouping_extension
- number_separator
- one_declaration_per_file
- prefer_nimble
- prefixed_toplevel_constant
- quick_discouraged_call
- quick_discouraged_pending_test

View file

@ -9,4 +9,5 @@ disabled_rules:
- force_cast
- force_try
- implicitly_unwrapped_optional
- large_tuple
- no_magic_numbers

View file

@ -11,19 +11,15 @@ import Quick
@testable import mas
/// Deprecated test.
public class AccountSpec: QuickSpec {
public final class AccountSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
// account command disabled since macOS 12 Monterey https://github.com/mas-cli/mas#known-issues
describe("Account command") {
it("displays active account") {
expect {
try MAS.Account.parse([]).run()
}
.to(throwError(MASError.notSupported))
describe("account command") {
it("displays not supported warning") {
expect(consequencesOf(try MAS.Account.parse([]).run()))
== (error: MASError.notSupported, stdout: "", stderr: "")
}
}
}

View file

@ -11,17 +11,15 @@ import Quick
@testable import mas
public class HomeSpec: QuickSpec {
public final class HomeSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("home command") {
it("can't find app with unknown ID") {
expect {
try MAS.Home.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
expect(consequencesOf(try MAS.Home.parse(["999"]).run(searcher: MockAppStoreSearcher())))
== (MASError.unknownAppID(999), "", "")
}
}
}

View file

@ -6,23 +6,20 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class InfoSpec: QuickSpec {
public final class InfoSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("Info command") {
it("can't find app with unknown ID") {
expect {
try MAS.Info.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
expect(consequencesOf(try MAS.Info.parse(["999"]).run(searcher: MockAppStoreSearcher())))
== (MASError.unknownAppID(999), "", "")
}
it("displays app details") {
let mockResult = SearchResult(
@ -36,21 +33,25 @@ public class InfoSpec: QuickSpec {
trackViewUrl: "https://awesome.app",
version: "1.0"
)
expect {
try captureStream(stdout) {
expect(
consequencesOf(
try MAS.Info.parse([String(mockResult.trackId)])
.run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]))
}
}
== """
Awesome App 1.0 [$2.00]
By: Awesome Dev
Released: 2019-01-07
Minimum OS: 10.14
Size: 1 KB
From: https://awesome.app
)
)
== (
nil,
"""
Awesome App 1.0 [$2.00]
By: Awesome Dev
Released: 2019-01-07
Minimum OS: 10.14
Size: 1 KB
From: https://awesome.app
"""
""",
""
)
}
}
}

View file

@ -11,17 +11,19 @@ import Quick
@testable import mas
public class InstallSpec: QuickSpec {
public final class InstallSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
xdescribe("install command") {
xit("installs apps") {
expect {
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
it("installs apps") {
expect(
consequencesOf(
try MAS.Install.parse([]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
)
)
== (nil, "", "")
}
}
}

View file

@ -6,25 +6,20 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class ListSpec: QuickSpec {
public final class ListSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("list command") {
it("lists apps") {
expect {
try captureStream(stderr) {
try MAS.List.parse([]).run(appLibrary: MockAppLibrary())
}
}
== "Error: No installed apps found\n"
expect(consequencesOf(try MAS.List.parse([]).run(appLibrary: MockAppLibrary())))
== (nil, "", "Error: No installed apps found\n")
}
}
}

View file

@ -11,7 +11,7 @@ import Quick
@testable import mas
public class LuckySpec: QuickSpec {
public final class LuckySpec: QuickSpec {
override public func spec() {
let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json")
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
@ -20,11 +20,11 @@ public class LuckySpec: QuickSpec {
MAS.initialize()
}
xdescribe("lucky command") {
xit("installs the first app matching a search") {
expect {
try MAS.Lucky.parse(["Slack"]).run(appLibrary: MockAppLibrary(), searcher: searcher)
}
.toNot(throwError())
it("installs the first app matching a search") {
expect(
consequencesOf(try MAS.Lucky.parse(["Slack"]).run(appLibrary: MockAppLibrary(), searcher: searcher))
)
== (nil, "", "")
}
}
}

View file

@ -6,23 +6,20 @@
// Copyright © 2019 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class OpenSpec: QuickSpec {
public final class OpenSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("open command") {
it("can't find app with unknown ID") {
expect {
try MAS.Open.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
expect(consequencesOf(try MAS.Open.parse(["999"]).run(searcher: MockAppStoreSearcher())))
== (MASError.unknownAppID(999), "", "")
}
}
}

View file

@ -12,7 +12,7 @@ import Quick
@testable import mas
public class OutdatedSpec: QuickSpec {
public final class OutdatedSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
@ -32,8 +32,8 @@ public class OutdatedSpec: QuickSpec {
version: "1.28"
)
expect {
try captureStream(stdout) {
expect(
consequencesOf(
try MAS.Outdated.parse([])
.run(
appLibrary: MockAppLibrary(
@ -47,9 +47,9 @@ public class OutdatedSpec: QuickSpec {
),
searcher: MockAppStoreSearcher([mockSearchResult.trackId: mockSearchResult])
)
}
}
== "490461369 Bandwidth+ (1.27 -> 1.28)\n"
)
)
== (nil, "490461369 Bandwidth+ (1.27 -> 1.28)\n", "")
}
}
}

View file

@ -11,17 +11,20 @@ import Quick
@testable import mas
public class PurchaseSpec: QuickSpec {
public final class PurchaseSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
xdescribe("purchase command") {
xit("purchases apps") {
expect {
try MAS.Purchase.parse(["999"]).run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
.toNot(throwError())
it("purchases apps") {
expect(
consequencesOf(
try MAS.Purchase.parse(["999"])
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
)
)
== (nil, "", "")
}
}
}

View file

@ -1,28 +0,0 @@
//
// ResetSpec.swift
// masTests
//
// Created by Ben Chatelain on 2018-12-28.
// Copyright © 2018 mas-cli. All rights reserved.
//
import Nimble
import Quick
@testable import mas
public class ResetSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("reset command") {
it("resets the App Store state") {
expect {
try MAS.Reset.parse([]).run()
}
.toNot(throwError())
}
}
}
}

View file

@ -6,13 +6,12 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class SearchSpec: QuickSpec {
public final class SearchSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
@ -25,19 +24,17 @@ public class SearchSpec: QuickSpec {
trackViewUrl: "mas preview url",
version: "0.0"
)
expect {
try captureStream(stdout) {
expect(
consequencesOf(
try MAS.Search.parse(["slack"])
.run(searcher: MockAppStoreSearcher([mockResult.trackId: mockResult]))
}
}
== " 1111 slack (0.0)\n"
)
)
== (nil, " 1111 slack (0.0)\n", "")
}
it("fails when searching for nonexistent app") {
expect {
try MAS.Search.parse(["nonexistent"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.noSearchResultsFound))
expect(consequencesOf(try MAS.Search.parse(["nonexistent"]).run(searcher: MockAppStoreSearcher())))
== (MASError.noSearchResultsFound, "", "")
}
}
}

View file

@ -12,18 +12,15 @@ import Quick
@testable import mas
/// Deprecated test.
public class SignInSpec: QuickSpec {
public final class SignInSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
// signin command disabled since macOS 10.13 High Sierra: https://github.com/mas-cli/mas#known-issues
describe("signin command") {
it("signs in") {
expect {
try MAS.SignIn.parse(["", ""]).run()
}
.to(throwError(MASError.notSupported))
expect(consequencesOf(try MAS.SignIn.parse(["", ""]).run()))
== (MASError.notSupported, "", "")
}
}
}

View file

@ -11,17 +11,15 @@ import Quick
@testable import mas
public class SignOutSpec: QuickSpec {
public final class SignOutSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("signout command") {
it("signs out") {
expect {
try MAS.SignOut.parse([]).run()
}
.toNot(throwError())
expect(consequencesOf(try MAS.SignOut.parse([]).run()))
== (nil, "", "")
}
}
}

View file

@ -12,65 +12,49 @@ import Quick
@testable import mas
public class UninstallSpec: QuickSpec {
public final class UninstallSpec: QuickSpec {
override public func spec() {
let appID: AppID = 12345
let app = MockSoftwareProduct(
appName: "Some App",
bundleIdentifier: "com.some.app",
bundlePath: "/tmp/Some.app",
bundleVersion: "1.0",
itemIdentifier: NSNumber(value: appID)
)
beforeSuite {
MAS.initialize()
}
xdescribe("uninstall command") {
let appID: AppID = 12345
let app = MockSoftwareProduct(
appName: "Some App",
bundleIdentifier: "com.some.app",
bundlePath: "/tmp/Some.app",
bundleVersion: "1.0",
itemIdentifier: NSNumber(value: appID)
)
context("dry run") {
let uninstall = try! MAS.Uninstall.parse(["--dry-run", String(appID)])
it("can't remove a missing app") {
expect {
try uninstall.run(appLibrary: MockAppLibrary())
}
.to(throwError(MASError.notInstalled(appID: appID)))
expect(consequencesOf(try uninstall.run(appLibrary: MockAppLibrary())))
== (MASError.notInstalled(appID: appID), "", "")
}
it("finds an app") {
expect {
try captureStream(stdout) {
try uninstall.run(appLibrary: MockAppLibrary(app))
}
}
== "==> 'Some App' '/tmp/Some.app'\n==> (not removed, dry run)\n"
expect(consequencesOf(try uninstall.run(appLibrary: MockAppLibrary(app))))
== (nil, "==> 'Some App' '/tmp/Some.app'\n==> (not removed, dry run)\n", "")
}
}
context("wet run") {
let uninstall = try! MAS.Uninstall.parse([String(appID)])
it("can't remove a missing app") {
expect {
try uninstall.run(appLibrary: MockAppLibrary())
}
.to(throwError(MASError.notInstalled(appID: appID)))
expect(consequencesOf(try uninstall.run(appLibrary: MockAppLibrary())))
== (MASError.notInstalled(appID: appID), "", "")
}
it("removes an app") {
expect {
try captureStream(stdout) {
try uninstall.run(appLibrary: MockAppLibrary(app))
}
}
.toNot(throwError())
expect(consequencesOf(try uninstall.run(appLibrary: MockAppLibrary(app))))
== (nil, "", "")
}
it("fails if there is a problem with the trash command") {
var brokenApp = app
brokenApp.bundlePath = "/dev/null"
expect {
try captureStream(stdout) {
try uninstall.run(appLibrary: MockAppLibrary(brokenApp))
}
}
.to(throwError(MASError.uninstallFailed(error: nil)))
expect(consequencesOf(try uninstall.run(appLibrary: MockAppLibrary(brokenApp))))
== (MASError.uninstallFailed(error: nil), "", "")
}
}
}

View file

@ -6,26 +6,25 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class UpgradeSpec: QuickSpec {
public final class UpgradeSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("upgrade command") {
it("finds no upgrades") {
expect {
try captureStream(stderr) {
expect(
consequencesOf(
try MAS.Upgrade.parse([])
.run(appLibrary: MockAppLibrary(), searcher: MockAppStoreSearcher())
}
}
.toNot(throwError())
)
)
== (nil, "", "")
}
}
}

View file

@ -11,17 +11,15 @@ import Quick
@testable import mas
public class VendorSpec: QuickSpec {
public final class VendorSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("vendor command") {
it("can't find app with unknown ID") {
expect {
try MAS.Vendor.parse(["999"]).run(searcher: MockAppStoreSearcher())
}
.to(throwError(MASError.unknownAppID(999)))
expect(consequencesOf(try MAS.Vendor.parse(["999"]).run(searcher: MockAppStoreSearcher())))
== (MASError.unknownAppID(999), "", "")
}
}
}

View file

@ -6,25 +6,20 @@
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import Nimble
import Quick
@testable import mas
public class VersionSpec: QuickSpec {
public final class VersionSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("version command") {
it("displays the current version") {
expect {
try captureStream(stdout) {
try MAS.Version.parse([]).run()
}
}
== "\(Package.version)\n"
expect(consequencesOf(try MAS.Version.parse([]).run()))
== (nil, "\(Package.version)\n", "")
}
}
}

View file

@ -11,33 +11,48 @@ import Quick
@testable import mas
public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
public final class ITunesSearchAppStoreSearcherSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
}
describe("url string") {
it("contains the search term") {
expect {
ITunesSearchAppStoreSearcher()
.searchURL(
for: "myapp",
inRegion: findISORegion(forAlpha2Code: "US")
)?
.absoluteString
}
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp"
expect(
consequencesOf(
ITunesSearchAppStoreSearcher()
.searchURL(
for: "myapp",
inRegion: findISORegion(forAlpha2Code: "US")
)?
.absoluteString
)
)
== (
"https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=myapp",
nil,
"",
""
)
}
it("contains the encoded search term") {
expect {
ITunesSearchAppStoreSearcher()
.searchURL(
for: "My App",
inRegion: findISORegion(forAlpha2Code: "US")
)?
.absoluteString
}
== "https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App"
expect(
consequencesOf(
ITunesSearchAppStoreSearcher()
.searchURL(
for: "My App",
inRegion: findISORegion(forAlpha2Code: "US")
)?
.absoluteString
)
)
== (
// swiftlint:disable:next line_length
"https://itunes.apple.com/search?media=software&entity=desktopSoftware&country=US&term=My%20App",
nil,
"",
""
)
}
}
describe("store") {
@ -46,10 +61,11 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
let networkSession = MockFromFileNetworkSession(responseFile: "search/slack.json")
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
expect {
try searcher.search(for: "slack").wait()
}
.to(haveCount(39))
let consequences = consequencesOf(try searcher.search(for: "slack").wait())
expect(consequences.value).to(haveCount(39))
expect(consequences.error) == nil
expect(consequences.stdout).to(beEmpty())
expect(consequences.stderr).to(beEmpty())
}
}
@ -59,13 +75,12 @@ public class ITunesSearchAppStoreSearcherSpec: QuickSpec {
let networkSession = MockFromFileNetworkSession(responseFile: "lookup/slack.json")
let searcher = ITunesSearchAppStoreSearcher(networkManager: NetworkManager(session: networkSession))
var result: SearchResult?
expect {
result = try searcher.lookup(appID: appID).wait()
}
.toNot(throwError())
let consequences = consequencesOf(try searcher.lookup(appID: appID).wait())
expect(consequences.error) == nil
expect(consequences.stdout).to(beEmpty())
expect(consequences.stderr).to(beEmpty())
guard let result else {
guard let result = consequences.value else {
fatalError("lookup result was nil")
}

View file

@ -1,47 +0,0 @@
//
// SoftwareMapAppLibrarySpec.swift
// masTests
//
// Created by Ben Chatelain on 3/1/20.
// Copyright © 2020 mas-cli. All rights reserved.
//
import Nimble
import Quick
@testable import mas
public class SoftwareMapAppLibrarySpec: QuickSpec {
override public func spec() {
let myApp = MockSoftwareProduct(
appName: "MyApp",
bundleIdentifier: "com.example",
bundlePath: "/Applications/MyApp.app",
bundleVersion: "1.0.0",
itemIdentifier: 1234
)
let apps = [myApp]
let library = SoftwareMapAppLibrary(softwareMap: MockSoftwareMap(products: apps))
beforeSuite {
MAS.initialize()
}
describe("mas app library") {
it("contains all installed apps") {
expect(library.installedApps).to(haveCount(apps.count))
expect(library.installedApps.first!.appName) == myApp.appName
}
}
}
}
// MARK: - MockSoftwareMap
struct MockSoftwareMap: SoftwareMap {
let products: [SoftwareProduct]
func allSoftwareProducts() -> [SoftwareProduct] {
products
}
}

View file

@ -1,92 +0,0 @@
//
// MASErrorTestCase.swift
// masTests
//
// Created by Ben Chatelain on 2/11/18.
// Copyright © 2018 Andrew Naylor. All rights reserved.
//
import Foundation
import XCTest
@testable import mas
class MASErrorTestCase: XCTestCase {
private static let error = NSError(domain: "MAS", code: 999, userInfo: [NSLocalizedDescriptionKey: "foo"])
override func setUp() {
super.setUp()
MAS.initialize()
}
func testNotSignedIn() {
XCTAssertEqual(MASError.notSignedIn.description, "Not signed in")
}
func testSignInFailed() {
XCTAssertEqual(MASError.signInFailed(error: nil).description, "Sign in failed")
}
func testSignInFailedError() {
XCTAssertEqual(MASError.signInFailed(error: Self.error).description, "Sign in failed: foo")
}
func testAlreadySignedIn() {
XCTAssertEqual(
MASError.alreadySignedIn(asAppleID: "person@example.com").description,
"Already signed in as person@example.com"
)
}
func testPurchaseFailed() {
XCTAssertEqual(MASError.purchaseFailed(error: nil).description, "Download request failed")
}
func testPurchaseFailedError() {
XCTAssertEqual(MASError.purchaseFailed(error: Self.error).description, "Download request failed: foo")
}
func testDownloadFailed() {
XCTAssertEqual(MASError.downloadFailed(error: nil).description, "Download failed")
}
func testDownloadFailedError() {
XCTAssertEqual(MASError.downloadFailed(error: Self.error).description, "Download failed: foo")
}
func testNoDownloads() {
XCTAssertEqual(MASError.noDownloads.description, "No downloads began")
}
func testCancelled() {
XCTAssertEqual(MASError.cancelled.description, "Download cancelled")
}
func testSearchFailed() {
XCTAssertEqual(MASError.searchFailed.description, "Search failed")
}
func testNoSearchResultsFound() {
XCTAssertEqual(MASError.noSearchResultsFound.description, "No apps found")
}
func testNoVendorWebsite() {
XCTAssertEqual(MASError.noVendorWebsite.description, "App does not have a vendor website")
}
func testNotInstalled() {
XCTAssertEqual(MASError.notInstalled(appID: 123).description, "No apps installed with app ID 123")
}
func testUninstallFailed() {
XCTAssertEqual(MASError.uninstallFailed(error: nil).description, "Uninstall failed")
}
func testNoData() {
XCTAssertEqual(MASError.noData.description, "Service did not return data")
}
func testJsonParsing() {
XCTAssertEqual(MASError.jsonParsing(data: Data()).description, "Unable to parse response as JSON:\n")
}
}

View file

@ -11,7 +11,7 @@ import Quick
@testable import mas
public class AppListFormatterSpec: QuickSpec {
public final class AppListFormatterSpec: QuickSpec {
override public func spec() {
// static func reference
let format = AppListFormatter.format(products:)
@ -21,7 +21,7 @@ public class AppListFormatterSpec: QuickSpec {
}
describe("app list formatter") {
it("formats nothing as empty string") {
expect(format([])).to(beEmpty())
expect(consequencesOf(format([]))) == ("", nil, "", "")
}
it("can format a single product") {
let product = MockSoftwareProduct(
@ -31,30 +31,32 @@ public class AppListFormatterSpec: QuickSpec {
bundleVersion: "19.2.1",
itemIdentifier: 12345
)
expect(format([product])) == "12345 Awesome App (19.2.1)"
expect(consequencesOf(format([product]))) == ("12345 Awesome App (19.2.1)", nil, "", "")
}
it("can format two products") {
expect(
format(
[
MockSoftwareProduct(
appName: "Awesome App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "19.2.1",
itemIdentifier: 12345
),
MockSoftwareProduct(
appName: "Even Better App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "1.2.0",
itemIdentifier: 67890
),
]
consequencesOf(
format(
[
MockSoftwareProduct(
appName: "Awesome App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "19.2.1",
itemIdentifier: 12345
),
MockSoftwareProduct(
appName: "Even Better App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "1.2.0",
itemIdentifier: 67890
),
]
)
)
)
== "12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)"
== ("12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)", nil, "", "")
}
}
}

View file

@ -11,7 +11,7 @@ import Quick
@testable import mas
public class SearchResultFormatterSpec: QuickSpec {
public final class SearchResultFormatterSpec: QuickSpec {
override public func spec() {
// static func reference
let format = SearchResultFormatter.format(results:includePrice:)
@ -21,7 +21,7 @@ public class SearchResultFormatterSpec: QuickSpec {
}
describe("search results formatter") {
it("formats nothing as empty string") {
expect(format([], false)).to(beEmpty())
expect(consequencesOf(format([], false))) == ("", nil, "", "")
}
it("can format a single result") {
let result = SearchResult(
@ -30,7 +30,7 @@ public class SearchResultFormatterSpec: QuickSpec {
trackName: "Awesome App",
version: "19.2.1"
)
expect(format([result], false)) == " 12345 Awesome App (19.2.1)"
expect(consequencesOf(format([result], false))) == (" 12345 Awesome App (19.2.1)", nil, "", "")
}
it("can format a single result with price") {
let result = SearchResult(
@ -39,51 +39,61 @@ public class SearchResultFormatterSpec: QuickSpec {
trackName: "Awesome App",
version: "19.2.1"
)
expect(format([result], true)) == " 12345 Awesome App (19.2.1) $9.87"
expect(consequencesOf(format([result], true)))
== (" 12345 Awesome App (19.2.1) $9.87", nil, "", "")
}
it("can format a two results") {
expect(
format(
[
SearchResult(
formattedPrice: "$9.87",
trackId: 12345,
trackName: "Awesome App",
version: "19.2.1"
),
SearchResult(
formattedPrice: "$0.01",
trackId: 67890,
trackName: "Even Better App",
version: "1.2.0"
),
],
false
consequencesOf(
format(
[
SearchResult(
formattedPrice: "$9.87",
trackId: 12345,
trackName: "Awesome App",
version: "19.2.1"
),
SearchResult(
formattedPrice: "$0.01",
trackId: 67890,
trackName: "Even Better App",
version: "1.2.0"
),
],
false
)
)
)
== " 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)"
== (" 12345 Awesome App (19.2.1)\n 67890 Even Better App (1.2.0)", nil, "", "")
}
it("can format a two results with prices") {
expect(
format(
[
SearchResult(
formattedPrice: "$9.87",
trackId: 12345,
trackName: "Awesome App",
version: "19.2.1"
),
SearchResult(
formattedPrice: "$0.01",
trackId: 67890,
trackName: "Even Better App",
version: "1.2.0"
),
],
true
consequencesOf(
format(
[
SearchResult(
formattedPrice: "$9.87",
trackId: 12345,
trackName: "Awesome App",
version: "19.2.1"
),
SearchResult(
formattedPrice: "$0.01",
trackId: 67890,
trackName: "Even Better App",
version: "1.2.0"
),
],
true
)
)
)
== " 12345 Awesome App (19.2.1) $9.87\n 67890 Even Better App (1.2.0) $0.01"
== (
" 12345 Awesome App (19.2.1) $9.87\n 67890 Even Better App (1.2.0) $0.01",
nil,
"",
""
)
}
}
}

View file

@ -12,7 +12,7 @@ import Quick
@testable import mas
public class SearchResultListSpec: QuickSpec {
public final class SearchResultListSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
@ -20,15 +20,23 @@ public class SearchResultListSpec: QuickSpec {
describe("search result list") {
it("can parse bbedit") {
expect(
try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/bbedit.json")).resultCount
consequencesOf(
try JSONDecoder()
.decode(SearchResultList.self, from: Data(from: "search/bbedit.json"))
.resultCount
)
)
== 1
== (1, nil, "", "")
}
it("can parse things") {
expect(
try JSONDecoder().decode(SearchResultList.self, from: Data(from: "search/things.json")).resultCount
consequencesOf(
try JSONDecoder()
.decode(SearchResultList.self, from: Data(from: "search/things.json"))
.resultCount
)
)
== 50
== (50, nil, "", "")
}
}
}

View file

@ -12,7 +12,7 @@ import Quick
@testable import mas
public class SearchResultSpec: QuickSpec {
public final class SearchResultSpec: QuickSpec {
override public func spec() {
beforeSuite {
MAS.initialize()
@ -20,11 +20,13 @@ public class SearchResultSpec: QuickSpec {
describe("search result") {
it("can parse things") {
expect(
try JSONDecoder()
.decode(SearchResult.self, from: Data(from: "search/things-that-go-bump.json"))
.trackId
consequencesOf(
try JSONDecoder()
.decode(SearchResult.self, from: Data(from: "search/things-that-go-bump.json"))
.trackId
)
)
== 1_472_954_003
== (1_472_954_003, nil, "", "")
}
}
}

View file

@ -12,36 +12,43 @@ import Quick
@testable import mas
public class SoftwareProductSpec: QuickSpec {
public final class SoftwareProductSpec: QuickSpec {
override public func spec() {
let app = MockSoftwareProduct(
appName: "App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "1.0.0",
itemIdentifier: 111
)
beforeSuite {
MAS.initialize()
}
describe("software product") {
let app = MockSoftwareProduct(
appName: "App",
bundleIdentifier: "",
bundlePath: "",
bundleVersion: "1.0.0",
itemIdentifier: 111
)
let currentApp = SearchResult(version: "1.0.0")
let appUpdate = SearchResult(version: "2.0.0")
let higherOs = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0")
let updateIos = SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0")
it("is not outdated when there is no new version available") {
expect(app.isOutdated(comparedTo: currentApp)) == false
expect(consequencesOf(app.isOutdated(comparedTo: SearchResult(version: "1.0.0"))))
== (false, nil, "", "")
}
it("is outdated when there is a new version available") {
expect(app.isOutdated(comparedTo: appUpdate)) == true
expect(consequencesOf(app.isOutdated(comparedTo: SearchResult(version: "2.0.0"))))
== (true, nil, "", "")
}
it("is not outdated when the new version of mac-software requires a higher OS version") {
expect(app.isOutdated(comparedTo: higherOs)) == false
expect(
consequencesOf(
app.isOutdated(comparedTo: SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0"))
)
)
== (false, nil, "", "")
}
it("is not outdated when the new version of software requires a higher OS version") {
expect(app.isOutdated(comparedTo: updateIos)) == false
expect(
consequencesOf(
app.isOutdated(comparedTo: SearchResult(minimumOsVersion: "99.0.0", version: "3.0.0"))
)
)
== (false, nil, "", "")
}
}
}

View file

@ -1,38 +0,0 @@
//
// MockNetworkSession
// masTests
//
// Created by Ben Chatelain on 11/13/18.
// Copyright © 2018 mas-cli. All rights reserved.
//
import Foundation
import PromiseKit
@testable import mas
/// Mock NetworkSession for testing.
struct MockNetworkSession: NetworkSession {
// Properties that enable us to set exactly what data or error
// we want our mocked URLSession to return for any request.
private let data: Data?
private let error: Error?
init(data: Data) {
self.data = data
error = nil
}
init(error: Error) {
self.error = error
data = nil
}
func loadData(from _: URL) -> Promise<Data> {
guard let data else {
return Promise(error: error ?? MASError.noData)
}
return .value(data)
}
}

View file

@ -1,68 +0,0 @@
//
// NetworkManagerTests.swift
// masTests
//
// Created by Ben Chatelain on 1/5/19.
// Copyright © 2019 mas-cli. All rights reserved.
//
import XCTest
@testable import mas
class NetworkManagerTests: XCTestCase {
override func setUp() {
super.setUp()
MAS.initialize()
}
func testSuccessfulAsyncResponse() throws {
let data = Data([0, 1, 0, 1])
XCTAssertEqual(
try NetworkManager(session: MockNetworkSession(data: data))
.loadData(from: URL(fileURLWithPath: "url"))
.wait(),
data
)
}
func testSuccessfulSyncResponse() throws {
let data = Data([0, 1, 0, 1])
XCTAssertEqual(
try NetworkManager(session: MockNetworkSession(data: data))
.loadData(from: URL(fileURLWithPath: "url"))
.wait(),
data
)
}
func testFailureAsyncResponse() {
XCTAssertThrowsError(
try NetworkManager(session: MockNetworkSession(error: MASError.noData))
.loadData(from: URL(fileURLWithPath: "url"))
.wait()
) { error in
guard let masError = error as? MASError else {
XCTFail("Error is of unexpected type.")
return
}
XCTAssertEqual(masError, MASError.noData)
}
}
func testFailureSyncResponse() {
XCTAssertThrowsError(
try NetworkManager(session: MockNetworkSession(error: MASError.noData))
.loadData(from: URL(fileURLWithPath: "url"))
.wait()
) { error in
guard let error = error as? MASError else {
XCTFail("Error is of unexpected type.")
return
}
XCTAssertEqual(error, MASError.noData)
}
}
}

View file

@ -0,0 +1,93 @@
//
// Consequences.swift
// masTests
//
// Created by Ross Goldberg on 2024-12-29.
// Copyright © 2016 mas-cli. All rights reserved.
//
import Foundation
@testable import mas
func consequencesOf(
streamEncoding: String.Encoding = .utf8,
_ expression: @autoclosure @escaping () throws -> Void
) -> (error: MASError?, stdout: String, stderr: String) {
let consequences = consequences(streamEncoding: streamEncoding, expression)
return (consequences.error, consequences.stdout, consequences.stderr)
}
func consequencesOf<T>(
streamEncoding: String.Encoding = .utf8,
_ expression: @autoclosure @escaping () throws -> T
) -> (value: T?, error: MASError?, stdout: String, stderr: String) {
consequences(streamEncoding: streamEncoding, expression)
}
// periphery:ignore
func consequencesOf(
streamEncoding: String.Encoding = .utf8,
_ body: @escaping () throws -> Void
) -> (error: MASError?, stdout: String, stderr: String) {
let consequences = consequences(streamEncoding: streamEncoding, body)
return (consequences.error, consequences.stdout, consequences.stderr)
}
// periphery:ignore
func consequencesOf<T>(
streamEncoding: String.Encoding = .utf8,
_ body: @escaping () throws -> T
) -> (value: T?, error: MASError?, stdout: String, stderr: String) {
consequences(streamEncoding: streamEncoding, body)
}
private func consequences<T>(
streamEncoding: String.Encoding = .utf8,
_ body: @escaping () throws -> T
) -> (value: T?, error: MASError?, stdout: String, stderr: String) {
let outOriginalFD = fileno(stdout)
let errOriginalFD = fileno(stderr)
let outDuplicateFD = dup(outOriginalFD)
defer {
close(outDuplicateFD)
}
let errDuplicateFD = dup(errOriginalFD)
defer {
close(errDuplicateFD)
}
let outPipe = Pipe()
let errPipe = Pipe()
dup2(outPipe.fileHandleForWriting.fileDescriptor, outOriginalFD)
dup2(errPipe.fileHandleForWriting.fileDescriptor, errOriginalFD)
var value: T?
var thrownError: MASError?
do {
defer {
fflush(stdout)
fflush(stderr)
dup2(outDuplicateFD, outOriginalFD)
dup2(errDuplicateFD, errOriginalFD)
outPipe.fileHandleForWriting.closeFile()
errPipe.fileHandleForWriting.closeFile()
}
value = try body()
} catch let error as MASError {
thrownError = error
} catch {
thrownError = MASError.failed(error: error as NSError)
}
return (
value,
thrownError,
String(data: outPipe.fileHandleForReading.readDataToEndOfFile(), encoding: streamEncoding) ?? "",
String(data: errPipe.fileHandleForReading.readDataToEndOfFile(), encoding: streamEncoding) ?? ""
)
}

View file

@ -1,36 +0,0 @@
//
// Streams.swift
// masTests
//
// Created by Ross Goldberg on 2024-12-29.
// Copyright © 2016 mas-cli. All rights reserved.
//
import Foundation
func captureStream(
_ stream: UnsafeMutablePointer<FILE>,
encoding: String.Encoding = .utf8,
_ block: @escaping () throws -> Void
) rethrows -> String {
let originalFd = fileno(stream)
let duplicateFd = dup(originalFd)
defer {
close(duplicateFd)
}
let pipe = Pipe()
dup2(pipe.fileHandleForWriting.fileDescriptor, originalFd)
do {
defer {
fflush(stream)
dup2(duplicateFd, originalFd)
pipe.fileHandleForWriting.closeFile()
}
try block()
}
return String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: encoding) ?? ""
}

View file

@ -12,5 +12,5 @@ printf $'==> ✅ Testing mas %s\n' "$(script/version)"
script/generate_version_info_for_swift
script -q /dev/null swift test |
script -q /dev/null swift test "${@}" |
(grep -vxE $'Test Suite \'.+\' (?:started|passed) at \\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}\\.\\d{3}\\.?\\r|Test Case \'-\\[.+\\]\' (?:started|passed \\(\\d+\\.\\d+ seconds\\))\\.\\r|\\t Executed \\d+ tests?, with 0 failures \\(0 unexpected\\) in \\d+\\.\\d+ \\(\\d+\\.\\d+\\) seconds\\r' || true)