macsync/@packages/shared/Tests/MacSyncSharedTests/BaseSyncManagerTests.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"#)
}
}