diff --git a/Brief/Updater.swift b/Brief/Updater.swift
index 010cb14..cea0bbb 100644
--- a/Brief/Updater.swift
+++ b/Brief/Updater.swift
@@ -11,7 +11,10 @@ public class Updater: ObservableObject, UpdaterProtocol {
@Published public var update: Release?
- public init(checkOnLaunch: Bool) {
+ private let osVersion: SemVer
+
+ public init(checkOnLaunch: Bool, osVersion: SemVer = SemVer(ProcessInfo.processInfo.operatingSystemVersion)) {
+ self.osVersion = osVersion
if checkOnLaunch {
// Don't do a launch check if the user hasn't seen the setup prompt explaining updater yet.
checkForUpdates()
@@ -25,8 +28,8 @@ public class Updater: ObservableObject, UpdaterProtocol {
public func checkForUpdates() {
URLSession.shared.dataTask(with: Constants.updateURL) { data, _, _ in
guard let data = data else { return }
- guard let release = try? JSONDecoder().decode(Release.self, from: data) else { return }
- self.evaluate(release: release)
+ guard let releases = try? JSONDecoder().decode([Release].self, from: data) else { return }
+ self.evaluate(releases: releases)
}.resume()
}
@@ -42,11 +45,16 @@ public class Updater: ObservableObject, UpdaterProtocol {
extension Updater {
- func evaluate(release: Release) {
+ func evaluate(releases: [Release]) {
+ guard let release = releases
+ .sorted()
+ .reversed()
+ .filter({ !$0.prerelease })
+ .first(where: { $0.minimumOSVersion <= osVersion }) else { return }
guard !userIgnored(release: release) else { return }
guard !release.prerelease else { return }
let latestVersion = SemVer(release.name)
- let currentVersion = SemVer(Bundle.main.infoDictionary!["CFBundleShortVersionString"] as! String)
+ let currentVersion = SemVer(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "0.0.0")
if latestVersion > currentVersion {
DispatchQueue.main.async {
self.update = release
@@ -64,11 +72,11 @@ extension Updater {
}
}
-struct SemVer {
+public struct SemVer {
let versionNumbers: [Int]
- init(_ version: String) {
+ public init(_ version: String) {
// Betas have the format 1.2.3_beta1
let strippedBeta = version.split(separator: "_").first!
var split = strippedBeta.split(separator: ".").compactMap { Int($0) }
@@ -78,11 +86,15 @@ struct SemVer {
versionNumbers = split
}
+ public init(_ version: OperatingSystemVersion) {
+ versionNumbers = [version.majorVersion, version.minorVersion, version.patchVersion]
+ }
+
}
extension SemVer: Comparable {
- static func < (lhs: SemVer, rhs: SemVer) -> Bool {
+ public static func < (lhs: SemVer, rhs: SemVer) -> Bool {
for (latest, current) in zip(lhs.versionNumbers, rhs.versionNumbers) {
if latest < current {
return true
@@ -99,7 +111,7 @@ extension SemVer: Comparable {
extension Updater {
enum Constants {
- static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases/latest")!
+ static let updateURL = URL(string: "https://api.github.com/repos/maxgoedjen/secretive/releases")!
}
}
@@ -128,12 +140,32 @@ extension Release: Identifiable {
}
+extension Release: Comparable {
+
+ public static func < (lhs: Release, rhs: Release) -> Bool {
+ lhs.version < rhs.version
+ }
+
+}
+
extension Release {
public var critical: Bool {
body.contains(Constants.securityContent)
}
+ public var version: SemVer {
+ SemVer(name)
+ }
+
+ public var minimumOSVersion: SemVer {
+ guard let range = body.range(of: "Minimum macOS Version"),
+ let numberStart = body.rangeOfCharacter(from: CharacterSet.decimalDigits, options: [], range: range.upperBound..
SemVer("1.0.0"))
+ }
+
+ func testGreatestSelectedIfOldPatchIsPublishedLater() {
+ // If 2.x.x series has been published, and a patch for 1.x.x is issued
+ // 2.x.x should still be selected if user can run it.
+ let updater = Updater(checkOnLaunch: false, osVersion: SemVer("2.2.3"))
+ let two = Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available! Minimum macOS Version: 2.2.3")
+ let releases = [
+ Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release Minimum macOS Version: 1.2.3"),
+ Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes Minimum macOS Version: 1.2.3"),
+ two,
+ Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"),
+ ]
+
+ let expectation = XCTestExpectation()
+ updater.evaluate(releases: releases)
+ DispatchQueue.main.async {
+ XCTAssert(updater.update == two)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1)
+ }
+
+ func testLatestVersionIsRunnable() {
+ // If the 2.x.x series has been published but the user can't run it
+ // the last version the user can run should be selected.
+ let updater = Updater(checkOnLaunch: false, osVersion: SemVer("1.2.3"))
+ let oneOhTwo = Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3")
+ let releases = [
+ Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release Minimum macOS Version: 1.2.3"),
+ Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes Minimum macOS Version: 1.2.3"),
+ Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available! Minimum macOS Version: 2.2.3"),
+ Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch! Minimum macOS Version: 1.2.3"),
+ ]
+ let expectation = XCTestExpectation()
+ updater.evaluate(releases: releases)
+ DispatchQueue.main.async {
+ XCTAssert(updater.update == oneOhTwo)
+ expectation.fulfill()
+ }
+ wait(for: [expectation], timeout: 1)
+ }
+
+ func testSorting() {
+ let two = Release(name: "2.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "2.0 available!")
+ let releases = [
+ Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release"),
+ Release(name: "1.0.1", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Bug fixes"),
+ two,
+ Release(name: "1.0.2", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Emergency patch!"),
+ ]
+ let sorted = releases.sorted().reversed().first
+ XCTAssert(sorted == two)
+ }
+
+}
diff --git a/BriefTests/BriefTests.swift b/BriefTests/SemVerTests.swift
similarity index 68%
rename from BriefTests/BriefTests.swift
rename to BriefTests/SemVerTests.swift
index 02729fa..f7ee332 100644
--- a/BriefTests/BriefTests.swift
+++ b/BriefTests/SemVerTests.swift
@@ -27,6 +27,21 @@ class SemVerTests: XCTestCase {
XCTAssert(current < new)
}
+ func testRegularParsing() {
+ let current = SemVer("1.0.2")
+ XCTAssert(current.versionNumbers == [1, 0, 2])
+ }
+
+ func testNoPatch() {
+ let current = SemVer("1.1")
+ XCTAssert(current.versionNumbers == [1, 1, 0])
+ }
+
+ func testGarbage() {
+ let current = SemVer("Test")
+ XCTAssert(current.versionNumbers == [0, 0, 0])
+ }
+
func testBeta() {
let current = SemVer("1.0.2")
let new = SemVer("1.1.0_beta1")
diff --git a/Secretive.xcodeproj/project.pbxproj b/Secretive.xcodeproj/project.pbxproj
index cad1ecf..70a3fbf 100644
--- a/Secretive.xcodeproj/project.pbxproj
+++ b/Secretive.xcodeproj/project.pbxproj
@@ -64,7 +64,7 @@
508BF28E25B4F005009EFB7E /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = 508BF28D25B4F005009EFB7E /* InternetAccessPolicy.plist */; };
508BF2AA25B4F1CB009EFB7E /* InternetAccessPolicy.plist in Resources */ = {isa = PBXBuildFile; fileRef = 508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */; };
5091D2BC25183B830049FD9B /* ApplicationDirectoryController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */; };
- 5091D3222519D56D0049FD9B /* BriefTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D3212519D56D0049FD9B /* BriefTests.swift */; };
+ 5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5091D3212519D56D0049FD9B /* SemVerTests.swift */; };
5091D3242519D56D0049FD9B /* Brief.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 506772FB2426F3F400034DED /* Brief.framework */; };
5099A02423FD2AAA0062B6F2 /* CreateSecretView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */; };
5099A02723FE34FA0062B6F2 /* SmartCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A02623FE34FA0062B6F2 /* SmartCard.swift */; };
@@ -75,6 +75,7 @@
5099A07C240242BA0062B6F2 /* AgentTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A07B240242BA0062B6F2 /* AgentTests.swift */; };
5099A07E240242BA0062B6F2 /* SecretAgentKit.h in Headers */ = {isa = PBXBuildFile; fileRef = 5099A06E240242BA0062B6F2 /* SecretAgentKit.h */; settings = {ATTRIBUTES = (Public, ); }; };
5099A08A240242C20062B6F2 /* SSHAgentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */; };
+ 509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */; };
50A3B79124026B7600D209EA /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79024026B7600D209EA /* Assets.xcassets */; };
50A3B79424026B7600D209EA /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79324026B7600D209EA /* Preview Assets.xcassets */; };
50A3B79724026B7600D209EA /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 50A3B79524026B7600D209EA /* Main.storyboard */; };
@@ -281,7 +282,7 @@
508BF29425B4F140009EFB7E /* InternetAccessPolicy.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; name = InternetAccessPolicy.plist; path = SecretAgent/InternetAccessPolicy.plist; sourceTree = SOURCE_ROOT; };
5091D2BB25183B830049FD9B /* ApplicationDirectoryController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationDirectoryController.swift; sourceTree = ""; };
5091D31F2519D56D0049FD9B /* BriefTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BriefTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
- 5091D3212519D56D0049FD9B /* BriefTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BriefTests.swift; sourceTree = ""; };
+ 5091D3212519D56D0049FD9B /* SemVerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemVerTests.swift; sourceTree = ""; };
5091D3232519D56D0049FD9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = ""; };
5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = ""; };
@@ -295,6 +296,7 @@
5099A07B240242BA0062B6F2 /* AgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentTests.swift; sourceTree = ""; };
5099A07D240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentProtocol.swift; sourceTree = ""; };
+ 509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseParsingTests.swift; sourceTree = ""; };
50A3B78A24026B7500D209EA /* SecretAgent.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = SecretAgent.app; sourceTree = BUILT_PRODUCTS_DIR; };
50A3B79024026B7600D209EA /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; };
@@ -567,7 +569,8 @@
5091D3202519D56D0049FD9B /* BriefTests */ = {
isa = PBXGroup;
children = (
- 5091D3212519D56D0049FD9B /* BriefTests.swift */,
+ 5091D3212519D56D0049FD9B /* SemVerTests.swift */,
+ 509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */,
5091D3232519D56D0049FD9B /* Info.plist */,
);
path = BriefTests;
@@ -1077,7 +1080,8 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
- 5091D3222519D56D0049FD9B /* BriefTests.swift in Sources */,
+ 509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */,
+ 5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};