178 lines
6.4 KiB
Swift
178 lines
6.4 KiB
Swift
import XCTest
|
|
@testable import MacSyncShared
|
|
|
|
/// Minimal Stats / Error fixtures used to instantiate the generic base.
|
|
private struct TestStats: Sendable, Equatable {
|
|
var count: Int = 0
|
|
}
|
|
|
|
private enum TestError: Sendable, Equatable {
|
|
case none
|
|
case authDenied
|
|
case backend(String)
|
|
}
|
|
|
|
@MainActor
|
|
private final class TestSyncManager: BaseSyncManager<TestStats, TestError> {
|
|
var performSyncCalls: Int = 0
|
|
var authRequests: Int = 0
|
|
var authorized: Bool = true
|
|
var authorizationGrantedOnRequest: Bool = true
|
|
|
|
var onPerform: (@MainActor () async -> Void)?
|
|
|
|
init(persistenceKey: String, interval: TimeInterval = 60, authorized: Bool = true) {
|
|
self.authorized = authorized
|
|
super.init(
|
|
initialStats: TestStats(),
|
|
noError: .none,
|
|
persistenceKey: persistenceKey,
|
|
timerInterval: interval
|
|
)
|
|
}
|
|
|
|
override func performSync() async {
|
|
performSyncCalls += 1
|
|
stats.count += 1
|
|
if let onPerform { await onPerform() }
|
|
}
|
|
|
|
override func requestAuthorization() async -> Bool {
|
|
authRequests += 1
|
|
if authorizationGrantedOnRequest {
|
|
authorized = true
|
|
}
|
|
return authorized
|
|
}
|
|
|
|
override func isAuthorized() async -> Bool { authorized }
|
|
|
|
override func onAuthorizationDenied() {
|
|
syncError = .authDenied
|
|
}
|
|
}
|
|
|
|
final class BaseSyncManagerTests: XCTestCase {
|
|
override func setUp() async throws {
|
|
// Clear any UserDefaults watermarks from prior runs so tests are
|
|
// hermetic. Each test uses a unique persistenceKey.
|
|
try await super.setUp()
|
|
}
|
|
|
|
@MainActor
|
|
func test_syncNow_runsCycleAndAdvancesWatermark() async {
|
|
let key = "test.base.\(UUID().uuidString)"
|
|
defer {
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
|
|
}
|
|
|
|
let manager = TestSyncManager(persistenceKey: key)
|
|
XCTAssertNil(manager.lastSync)
|
|
XCTAssertNil(manager.lastSyncCompletedAt)
|
|
|
|
manager.syncNow()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
XCTAssertEqual(manager.performSyncCalls, 1)
|
|
XCTAssertFalse(manager.isSyncing)
|
|
XCTAssertNotNil(manager.lastSync)
|
|
XCTAssertNotNil(manager.lastSyncCompletedAt)
|
|
XCTAssertEqual(manager.stats.count, 1)
|
|
}
|
|
|
|
@MainActor
|
|
func test_syncNow_skipsWhenAlreadyRunning() async {
|
|
let key = "test.base.\(UUID().uuidString)"
|
|
defer {
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
|
|
}
|
|
|
|
let manager = TestSyncManager(persistenceKey: key)
|
|
manager.onPerform = {
|
|
try? await Task.sleep(nanoseconds: 50_000_000)
|
|
}
|
|
|
|
manager.syncNow()
|
|
manager.syncNow()
|
|
manager.syncNow()
|
|
try? await Task.sleep(nanoseconds: 200_000_000)
|
|
|
|
XCTAssertEqual(manager.performSyncCalls, 1, "concurrent syncNow calls must coalesce")
|
|
}
|
|
|
|
@MainActor
|
|
func test_authorizationDenied_setsErrorAndSkipsPerform() async {
|
|
let key = "test.base.\(UUID().uuidString)"
|
|
defer {
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
|
|
}
|
|
|
|
let manager = TestSyncManager(persistenceKey: key, authorized: false)
|
|
manager.authorizationGrantedOnRequest = false
|
|
|
|
manager.syncNow()
|
|
try? await Task.sleep(nanoseconds: 100_000_000)
|
|
|
|
XCTAssertEqual(manager.performSyncCalls, 0)
|
|
XCTAssertEqual(manager.syncError, .authDenied)
|
|
XCTAssertNil(manager.lastSync, "denied auth must not advance watermark")
|
|
}
|
|
|
|
@MainActor
|
|
func test_setLastSync_persistsAndClears() async {
|
|
let key = "test.base.\(UUID().uuidString)"
|
|
defer {
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
|
|
}
|
|
|
|
let manager = TestSyncManager(persistenceKey: key)
|
|
let date = Date(timeIntervalSince1970: 1_700_000_000)
|
|
manager.setLastSync(date)
|
|
XCTAssertEqual(manager.lastSync, date)
|
|
XCTAssertEqual(UserDefaults.standard.object(forKey: "\(key).lastSync") as? Date, date)
|
|
|
|
manager.setLastSync(nil)
|
|
XCTAssertNil(manager.lastSync)
|
|
XCTAssertNil(UserDefaults.standard.object(forKey: "\(key).lastSync"))
|
|
}
|
|
|
|
@MainActor
|
|
func test_initRehydratesWatermarkFromUserDefaults() async {
|
|
let key = "test.base.\(UUID().uuidString)"
|
|
let date = Date(timeIntervalSince1970: 1_650_000_000)
|
|
UserDefaults.standard.set(date, forKey: "\(key).lastSync")
|
|
UserDefaults.standard.set(date, forKey: "\(key).lastSyncCompletedAt")
|
|
defer {
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSync")
|
|
UserDefaults.standard.removeObject(forKey: "\(key).lastSyncCompletedAt")
|
|
}
|
|
|
|
let manager = TestSyncManager(persistenceKey: key)
|
|
XCTAssertEqual(manager.lastSync, date)
|
|
XCTAssertEqual(manager.lastSyncCompletedAt, date)
|
|
}
|
|
}
|
|
|
|
final class SyncConnectionErrorHeuristicTests: XCTestCase {
|
|
func test_recognisesConnectionishKeywords() {
|
|
struct Boom: LocalizedError { let errorDescription: String? }
|
|
XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Network is unreachable")))
|
|
XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "The connection timed out")))
|
|
XCTAssertTrue(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Could not connect to host")))
|
|
XCTAssertFalse(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "Invalid JSON payload")))
|
|
XCTAssertFalse(SyncConnectionErrorHeuristic.isConnectionError(Boom(errorDescription: "401 Unauthorized")))
|
|
}
|
|
}
|
|
|
|
final class AppleScriptEscapeTests: XCTestCase {
|
|
func test_escapesBackslashesAndQuotes() {
|
|
XCTAssertEqual(AppleScriptEscape.quote("hello"), "hello")
|
|
XCTAssertEqual(AppleScriptEscape.quote(#"say "hi""#), #"say \"hi\""#)
|
|
XCTAssertEqual(AppleScriptEscape.quote(#"C:\path"#), #"C:\\path"#)
|
|
XCTAssertEqual(AppleScriptEscape.quote("line1\nline2"), #"line1\nline2"#)
|
|
}
|
|
}
|