import XCTest import Photos @testable import NASBackup @MainActor final class BackupEngineTests: XCTestCase { private func makeAssets(count: Int) -> [PhotoAsset] { (0.. NASConnection { NASConnection(host: "192.168.1.10", nasProtocol: .smb, username: "user", password: "pass", remotePath: "/backup") } func test_backup_uploadsAllAssets() async throws { let mock = MockNASService() let photos = MockPhotoLibraryService(assets: makeAssets(count: 3)) let engine = BackupEngine.testInstance(transfer: mock, photos: photos) let result = try await engine.run(connection: makeConnection(), filter: BackupFilter()) XCTAssertEqual(result.uploadedCount, 3) XCTAssertEqual(result.failedCount, 0) XCTAssertEqual(mock.uploadCallCount, 3) } func test_backup_skipsExistingFiles_alwaysHardcoded() async throws { let mock = MockNASService() mock.fileExistsAnswer = true let photos = MockPhotoLibraryService(assets: makeAssets(count: 2)) let engine = BackupEngine.testInstance(transfer: mock, photos: photos) let result = try await engine.run(connection: makeConnection(), filter: BackupFilter()) XCTAssertEqual(result.skippedCount, 2) XCTAssertEqual(result.uploadedCount, 0) } func test_backup_handlesUploadFailure_gracefully() async throws { let mock = MockNASService() mock.shouldFail = true let photos = MockPhotoLibraryService(assets: makeAssets(count: 2)) let engine = BackupEngine.testInstance(transfer: mock, photos: photos) let result = try await engine.run(connection: makeConnection(), filter: BackupFilter()) XCTAssertEqual(result.failedCount, 2) XCTAssertEqual(result.uploadedCount, 0) } func test_backup_emitsProgressUpdates() async throws { let mock = MockNASService() let photos = MockPhotoLibraryService(assets: makeAssets(count: 5)) let engine = BackupEngine.testInstance(transfer: mock, photos: photos) var progressEvents = 0 let cancellable = engine.$job.sink { job in if case .running = job.status { progressEvents += 1 } } defer { cancellable.cancel() } _ = try await engine.run(connection: makeConnection(), filter: BackupFilter()) XCTAssertGreaterThan(progressEvents, 0, "Expected progress events during backup") } func test_backup_canBePaused_andResumed() async throws { let mock = MockNASService() mock.uploadDelay = 0.05 let photos = MockPhotoLibraryService(assets: makeAssets(count: 4)) let engine = BackupEngine.testInstance(transfer: mock, photos: photos) let task = Task { try await engine.run(connection: makeConnection(), filter: BackupFilter()) } try await Task.sleep(nanoseconds: 70_000_000) engine.pause() let statusAfterPause = engine.job.status engine.resume() let result = try await task.value if case .paused = statusAfterPause { } else { XCTFail("Expected .paused status, got \(statusAfterPause)") } XCTAssertEqual(result.uploadedCount, 4) } } // MARK: - MockPhotoLibraryService final class MockPhotoLibraryService: PhotoLibraryProtocol { var authorizationStatus: PHAuthorizationStatus { .authorized } private let assets: [PhotoAsset] init(assets: [PhotoAsset]) { self.assets = assets } func requestAuthorization() async -> PHAuthorizationStatus { .authorized } func fetchAssets(filter: BackupFilter) -> [PhotoAsset] { assets } func exportAsset(_ asset: PhotoAsset) async throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent(asset.filename) try Data("mock-data".utf8).write(to: url) return url } }