// // CloudTransferManagerTests.swift // suixinkanTests // import Foundation import XCTest @testable import suixinkan @MainActor /// 云盘传输管理测试。 final class CloudTransferManagerTests: XCTestCase { func testUploadRegistersFileAndRemovesCompletedTask() async throws { let store = MemoryTransferStore() let uploader = FakeCloudUploader(url: "https://cdn/a.jpg") let api = TransferMockCloudDriveAPI() let manager = CloudTransferManager( store: store, uploader: uploader, api: api, downloader: FakeDownloader(), photoSaver: FakePhotoSaver(), scenicIdProvider: { 10 } ) let fileURL = try makeTempFile(name: "a.jpg") var completedFolder: Int? manager.onUploadCompleted = { completedFolder = $0 } manager.addUploadTask(localFileURL: fileURL, fileName: "a.jpg", fileType: 2, fileSize: 3, parentFolderId: 6) try await waitUntil { manager.uploadTasks.isEmpty } XCTAssertEqual(api.uploadCalls.first?.parentFolderId, 6) XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/a.jpg") XCTAssertEqual(completedFolder, 6) XCTAssertTrue(store.saved.last?.allSatisfy { $0.kind != .upload } == true) } func testUploadFailureKeepsFailedTask() async throws { let uploader = FakeCloudUploader(error: TestError.failed) let manager = CloudTransferManager( store: MemoryTransferStore(), uploader: uploader, api: TransferMockCloudDriveAPI(), downloader: FakeDownloader(), photoSaver: FakePhotoSaver(), scenicIdProvider: { 10 } ) let fileURL = try makeTempFile(name: "a.jpg") manager.addUploadTask(localFileURL: fileURL, fileName: "a.jpg", fileType: 2, fileSize: 3, parentFolderId: 0) try await waitUntil { manager.uploadTasks.first?.status == .failed } XCTAssertEqual(manager.uploadTasks.first?.status, .failed) } func testDownloadCompletesAndCanCancel() async throws { let downloader = FakeDownloader() let saver = FakePhotoSaver() let manager = CloudTransferManager( store: MemoryTransferStore(), uploader: FakeCloudUploader(url: "unused"), api: TransferMockCloudDriveAPI(), downloader: downloader, photoSaver: saver, scenicIdProvider: { 10 } ) manager.addDownloadTask(fileURL: "https://cdn/a.jpg", fileName: "a.jpg", fileType: 2) try await waitUntil { manager.downloadTasks.first?.status == .completed } XCTAssertEqual(downloader.downloadCalls.first?.remoteURL, "https://cdn/a.jpg") XCTAssertEqual(saver.saved.count, 1) XCTAssertEqual(manager.downloadTasks.first?.progress, 100) let id = try XCTUnwrap(manager.downloadTasks.first?.id) manager.cancelDownload(id: id) XCTAssertTrue(manager.downloadTasks.isEmpty) } private func makeTempFile(name: String) throws -> URL { let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + name) try Data([1, 2, 3]).write(to: url) return url } private func waitUntil( timeout: TimeInterval = 2, condition: @escaping () -> Bool ) async throws { let deadline = Date().addingTimeInterval(timeout) while !condition() { if Date() > deadline { XCTFail("Timed out waiting for condition") return } try await Task.sleep(nanoseconds: 20_000_000) } } } private final class MemoryTransferStore: CloudTransferTaskStoring { var tasks: [CloudTransferTask] = [] var saved: [[CloudTransferTask]] = [] func loadTasks() -> [CloudTransferTask] { tasks } func saveTasks(_ tasks: [CloudTransferTask]) { self.tasks = tasks saved.append(tasks) } } @MainActor private final class FakeCloudUploader: CloudDriveUploading { let url: String let error: Error? init(url: String = "", error: Error? = nil) { self.url = url self.error = error } func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String { if let error { throw error } onProgress(50) onProgress(100) return url } } private final class FakeDownloader: CloudFileDownloading { var downloadCalls: [(remoteURL: String, fileName: String)] = [] func downloadFile(remoteURL: String, fileName: String, onProgress: @escaping @Sendable (Int) -> Void) async throws -> URL { downloadCalls.append((remoteURL, fileName)) onProgress(40) let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName) try Data([1]).write(to: url) onProgress(100) return url } } private final class FakePhotoSaver: CloudPhotoSaving { var saved: [(fileURL: URL, fileType: Int)] = [] func saveToPhotoLibrary(fileURL: URL, fileType: Int) async throws { saved.append((fileURL, fileType)) } } @MainActor private final class TransferMockCloudDriveAPI: CloudDriveServing { var uploadCalls: [(parentFolderId: Int, fileUrl: String, fileName: String)] = [] func cloudFileList(parentFolderId: Int, name: String, type: Int, orderBy: Int, page: Int, pageSize: Int) async throws -> CloudFileListResponse { CloudFileListResponse() } func createCloudFolder(parentFolderId: Int, name: String) async throws {} func uploadCloudFile(parentFolderId: Int, fileUrl: String, fileName: String) async throws { uploadCalls.append((parentFolderId, fileUrl, fileName)) } func deleteCloudFiles(_ request: CloudFileDeleteRequest) async throws {} func moveCloudFiles(_ request: CloudFileMoveRequest) async throws {} func checkUploadPermission() async throws -> CloudUploadPermissionResponse { CloudUploadPermissionResponse(canUpload: true, reason: "") } func modifyCloudFolderName(fileId: Int, name: String) async throws {} func modifyCloudFileName(fileId: Int, fileName: String) async throws {} } private enum TestError: LocalizedError { case failed var errorDescription: String? { "failed" } }