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 { 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"#) } }