mirror of
				https://github.com/maxgoedjen/secretive.git
				synced 2025-10-29 14:30:56 +00:00 
			
		
		
		
	Improve updater (#203)
This commit is contained in:
		
							parent
							
								
									4105c1d6f6
								
							
						
					
					
						commit
						698a69a034
					
				| @ -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..<body.endIndex) else { return SemVer("11.0.0") } | ||||
|         let numbersEnd = body.rangeOfCharacter(from: CharacterSet.whitespacesAndNewlines, options: [], range: numberStart.upperBound..<body.endIndex)?.lowerBound ?? body.endIndex | ||||
|         let version = numberStart.lowerBound..<numbersEnd | ||||
|         return SemVer(String(body[version])) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| extension Release { | ||||
|  | ||||
							
								
								
									
										104
									
								
								BriefTests/ReleaseParsingTests.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								BriefTests/ReleaseParsingTests.swift
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,104 @@ | ||||
| import XCTest | ||||
| @testable import Brief | ||||
| 
 | ||||
| class ReleaseParsingTests: XCTestCase { | ||||
| 
 | ||||
|     func testNonCritical() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Initial release") | ||||
|         XCTAssert(release.critical == false) | ||||
|     } | ||||
| 
 | ||||
|     func testCritical() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") | ||||
|         XCTAssert(release.critical == true) | ||||
|     } | ||||
| 
 | ||||
|     func testOSMissing() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update") | ||||
|         XCTAssert(release.minimumOSVersion == SemVer("11.0.0")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSPresentWithContentBelow() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update ##Minimum macOS Version\n1.2.3\nBuild info") | ||||
|         XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSPresentAtEnd() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") | ||||
|         XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSWithMacOSPrefix() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: macOS 1.2.3") | ||||
|         XCTAssert(release.minimumOSVersion == SemVer("1.2.3")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSGreaterThanMinimum() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") | ||||
|         XCTAssert(release.minimumOSVersion < SemVer("11.0.0")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSEqualToMinimum() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 11.2.3") | ||||
|         XCTAssert(release.minimumOSVersion <= SemVer("11.2.3")) | ||||
|     } | ||||
| 
 | ||||
|     func testOSLessThanMinimum() { | ||||
|         let release = Release(name: "1.0.0", prerelease: false, html_url: URL(string: "https://example.com")!, body: "Critical Security Update Minimum macOS Version: 1.2.3") | ||||
|         XCTAssert(release.minimumOSVersion > 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) | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| @ -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") | ||||
| @ -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 */; }; | ||||
| @ -280,7 +281,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 = "<group>"; }; | ||||
| 		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 = "<group>"; }; | ||||
| 		5091D3212519D56D0049FD9B /* SemVerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SemVerTests.swift; sourceTree = "<group>"; }; | ||||
| 		5091D3232519D56D0049FD9B /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||
| 		5099A02323FD2AAA0062B6F2 /* CreateSecretView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateSecretView.swift; sourceTree = "<group>"; }; | ||||
| 		5099A02623FE34FA0062B6F2 /* SmartCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartCard.swift; sourceTree = "<group>"; }; | ||||
| @ -294,6 +295,7 @@ | ||||
| 		5099A07B240242BA0062B6F2 /* AgentTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AgentTests.swift; sourceTree = "<group>"; }; | ||||
| 		5099A07D240242BA0062B6F2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; | ||||
| 		5099A089240242C20062B6F2 /* SSHAgentProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSHAgentProtocol.swift; sourceTree = "<group>"; }; | ||||
| 		509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReleaseParsingTests.swift; sourceTree = "<group>"; }; | ||||
| 		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 = "<group>"; }; | ||||
| 		50A3B79324026B7600D209EA /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = "<group>"; }; | ||||
| @ -565,7 +567,8 @@ | ||||
| 		5091D3202519D56D0049FD9B /* BriefTests */ = { | ||||
| 			isa = PBXGroup; | ||||
| 			children = ( | ||||
| 				5091D3212519D56D0049FD9B /* BriefTests.swift */, | ||||
| 				5091D3212519D56D0049FD9B /* SemVerTests.swift */, | ||||
| 				509FA3B525B53C49005E2535 /* ReleaseParsingTests.swift */, | ||||
| 				5091D3232519D56D0049FD9B /* Info.plist */, | ||||
| 			); | ||||
| 			path = BriefTests; | ||||
| @ -1073,7 +1076,8 @@ | ||||
| 			isa = PBXSourcesBuildPhase; | ||||
| 			buildActionMask = 2147483647; | ||||
| 			files = ( | ||||
| 				5091D3222519D56D0049FD9B /* BriefTests.swift in Sources */, | ||||
| 				509FA3B625B53C49005E2535 /* ReleaseParsingTests.swift in Sources */, | ||||
| 				5091D3222519D56D0049FD9B /* SemVerTests.swift in Sources */, | ||||
| 			); | ||||
| 			runOnlyForDeploymentPostprocessing = 0; | ||||
| 		}; | ||||
|  | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user