Files
crossdesk/apps/ios/tests/app_update_checker_test.py
T

101 lines
4.3 KiB
Python

"""Run the production event-driven update checker with a deterministic transport."""
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
@unittest.skipUnless(sys.platform == "darwin", "requires the Apple Swift runtime")
class AppUpdateCheckerTest(unittest.TestCase):
def test_visit_session_end_failures_and_lifecycle(self):
source = Path(__file__).resolve().parents[1] / "CrossDeskMobile/AppUpdateChecker.swift"
with tempfile.TemporaryDirectory(prefix="crossdesk-app-update-tests-") as directory:
harness = Path(directory) / "Tests.swift"
harness.write_text(r'''
import Foundation
func expect(_ condition: @autoclosure () -> Bool, _ message: String) {
if !condition() { fatalError(message) }
}
@MainActor
final class Fixture {
var replies: [(Data?) -> Void] = []
var cancellations = 0
var time = Date(timeIntervalSince1970: 0)
lazy var checker = AppUpdateChecker(currentVersion: "0.0.1",
bundleIdentifier: "com.chunkdash.crossdesk", appStoreID: "6828859211", systemVersion: "17.0",
fetch: { completion in
self.replies.append(completion)
return { self.cancellations += 1 }
}, now: { self.time })
func reply(_ value: String?) {
replies.last!(value.map { version in
try! JSONSerialization.data(withJSONObject: ["resultCount": 1, "results": [[
"wrapperType": "software", "bundleId": "com.chunkdash.crossdesk",
"trackId": 6828859211, "version": version, "minimumOsVersion": "16.0"
]]])
})
}
}
@main
enum Tests {
@MainActor static func main() {
let f = Fixture()
f.checker.checkNow()
expect(f.replies.isEmpty, "No checks without consent and foreground")
f.checker.setEnabled(true)
f.checker.checkNow()
f.checker.setEnabled(true)
expect(f.replies.count == 1, "Deduplicate concurrent checks")
f.reply("0.0.2")
expect(f.checker.updateAvailable && f.checker.status == .available, "Show badge")
f.checker.setEnabled(true)
expect(f.replies.count == 1, "Duplicate active callbacks do not recheck after completion")
f.checker.setEnabled(false)
f.time.addTimeInterval(6 * 60 * 60)
f.checker.setEnabled(true)
expect(f.replies.count == 2, "A new visit checks after the automatic cooldown")
f.reply(nil)
expect(f.checker.status == .failed && f.checker.updateAvailable,
"Failed checks retain discovered update")
f.checker.setEnabled(true)
expect(f.replies.count == 2, "A failure does not restart the check")
// The About action explicitly bypasses the automatic cooldown.
f.checker.checkNow()
f.checker.checkNow()
expect(f.replies.count == 3, "Explicit events check again without a cooldown and deduplicate")
f.reply("0.0.1")
expect(!f.checker.updateAvailable && f.checker.status == .upToDate,
"Successful current-version check clears badge")
f.checker.checkNow()
let stale = f.replies.last!
f.checker.setEnabled(false)
expect(f.cancellations == 1, "Background/revocation cancels request")
stale(Data("9.0.0".utf8))
expect(!f.checker.updateAvailable, "Ignore cancelled response")
f.time.addTimeInterval(6 * 60 * 60)
f.checker.setEnabled(true)
stale(Data("9.0.0".utf8))
expect(f.checker.status == .checking, "Ignore old response during new request")
f.reply("0.0.2")
f.checker.setEnabled(false)
let beforeForeground = f.replies.count
f.checker.checkNow()
expect(f.replies.count == beforeForeground, "Session endings in background do not check")
f.time.addTimeInterval(6 * 60 * 60)
f.checker.setEnabled(true)
expect(f.replies.count == beforeForeground + 1, "Check once on return from background")
expect(f.checker.updateAvailable, "Retain badge while rechecking")
f.checker.setEnabled(false)
}
}
''')
binary = Path(directory) / "app-update-tests"
subprocess.run(["xcrun", "swiftc", str(source), str(harness), "-o", str(binary)], check=True)
subprocess.run([str(binary)], check=True)
if __name__ == "__main__":
unittest.main()