368 lines
13 KiB
Swift
368 lines
13 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
|
|
func testUploadPauseThenResumeCompletes() async throws {
|
|
let uploader = PausingThenSucceedCloudUploader(url: "https://cdn/resumed.jpg")
|
|
let api = TransferMockCloudDriveAPI()
|
|
let manager = CloudTransferManager(
|
|
store: MemoryTransferStore(),
|
|
uploader: uploader,
|
|
api: api,
|
|
downloader: FakeDownloader(),
|
|
photoSaver: FakePhotoSaver(),
|
|
scenicIdProvider: { 10 }
|
|
)
|
|
let fileURL = try makeTempFile(name: "resume.jpg")
|
|
|
|
manager.addUploadTask(localFileURL: fileURL, fileName: "resume.jpg", fileType: 2, fileSize: 3, parentFolderId: 7)
|
|
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
|
|
try await waitUntil { uploader.attempts == 1 }
|
|
manager.pauseUpload(id: id)
|
|
try await waitUntil { manager.uploadTasks.first?.status == .paused }
|
|
|
|
manager.resumeUpload(id: id)
|
|
try await waitUntil { manager.uploadTasks.isEmpty }
|
|
|
|
XCTAssertEqual(uploader.attempts, 2)
|
|
XCTAssertEqual(api.uploadCalls.first?.parentFolderId, 7)
|
|
XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/resumed.jpg")
|
|
}
|
|
|
|
func testFailedUploadCanRetry() async throws {
|
|
let uploader = FailOnceCloudUploader(url: "https://cdn/retried.jpg")
|
|
let api = TransferMockCloudDriveAPI()
|
|
let manager = CloudTransferManager(
|
|
store: MemoryTransferStore(),
|
|
uploader: uploader,
|
|
api: api,
|
|
downloader: FakeDownloader(),
|
|
photoSaver: FakePhotoSaver(),
|
|
scenicIdProvider: { 10 }
|
|
)
|
|
let fileURL = try makeTempFile(name: "retry.jpg")
|
|
|
|
manager.addUploadTask(localFileURL: fileURL, fileName: "retry.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
|
|
try await waitUntil { manager.uploadTasks.first?.status == .failed }
|
|
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
|
|
|
|
manager.resumeUpload(id: id)
|
|
try await waitUntil { manager.uploadTasks.isEmpty }
|
|
|
|
XCTAssertEqual(uploader.attempts, 2)
|
|
XCTAssertEqual(api.uploadCalls.first?.fileUrl, "https://cdn/retried.jpg")
|
|
}
|
|
|
|
func testUploadCancelRemovesTaskAndStagedFile() async throws {
|
|
let uploader = PausingThenSucceedCloudUploader(url: "unused")
|
|
let manager = CloudTransferManager(
|
|
store: MemoryTransferStore(),
|
|
uploader: uploader,
|
|
api: TransferMockCloudDriveAPI(),
|
|
downloader: FakeDownloader(),
|
|
photoSaver: FakePhotoSaver(),
|
|
scenicIdProvider: { 10 }
|
|
)
|
|
let fileURL = try makeTempFile(name: "cancel.jpg")
|
|
|
|
manager.addUploadTask(localFileURL: fileURL, fileName: "cancel.jpg", fileType: 2, fileSize: 3, parentFolderId: 0)
|
|
let id = try XCTUnwrap(manager.uploadTasks.first?.id)
|
|
manager.cancelUpload(id: id)
|
|
|
|
XCTAssertTrue(manager.uploadTasks.isEmpty)
|
|
XCTAssertFalse(FileManager.default.fileExists(atPath: fileURL.path))
|
|
}
|
|
|
|
func testDownloadPauseThenResumeCompletes() async throws {
|
|
let downloader = PausingThenSucceedDownloader()
|
|
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/resume.jpg", fileName: "resume.jpg", fileType: 2)
|
|
let id = try XCTUnwrap(manager.downloadTasks.first?.id)
|
|
try await waitUntil { await downloader.attemptCount() == 1 }
|
|
manager.pauseDownload(id: id)
|
|
try await waitUntil { manager.downloadTasks.first?.status == .paused }
|
|
|
|
manager.resumeDownload(id: id)
|
|
try await waitUntil { manager.downloadTasks.first?.status == .completed }
|
|
|
|
let attemptCount = await downloader.attemptCount()
|
|
XCTAssertEqual(attemptCount, 2)
|
|
XCTAssertEqual(saver.saved.count, 1)
|
|
XCTAssertEqual(manager.downloadTasks.first?.progress, 100)
|
|
}
|
|
|
|
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 func waitUntil(
|
|
timeout: TimeInterval = 2,
|
|
condition: @escaping () async -> Bool
|
|
) async throws {
|
|
let deadline = Date().addingTimeInterval(timeout)
|
|
while !(await 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
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class PausingThenSucceedCloudUploader: CloudDriveUploading {
|
|
let url: String
|
|
private(set) var attempts = 0
|
|
|
|
init(url: String) {
|
|
self.url = url
|
|
}
|
|
|
|
func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
|
attempts += 1
|
|
if attempts == 1 {
|
|
try await Task.sleep(nanoseconds: 5_000_000_000)
|
|
}
|
|
onProgress(100)
|
|
return url
|
|
}
|
|
}
|
|
|
|
@MainActor
|
|
private final class FailOnceCloudUploader: CloudDriveUploading {
|
|
let url: String
|
|
private(set) var attempts = 0
|
|
|
|
init(url: String) {
|
|
self.url = url
|
|
}
|
|
|
|
func uploadCloudDriveFile(data: Data, fileName: String, fileType: Int, scenicId: Int, onProgress: @escaping (Int) -> Void) async throws -> String {
|
|
attempts += 1
|
|
if attempts == 1 {
|
|
throw TestError.failed
|
|
}
|
|
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 PausingThenSucceedDownloader: CloudFileDownloading {
|
|
private let counter = DownloadAttemptCounter()
|
|
|
|
func downloadFile(remoteURL: String, fileName: String, onProgress: @escaping @Sendable (Int) -> Void) async throws -> URL {
|
|
let attempt = await counter.next()
|
|
if attempt == 1 {
|
|
try await Task.sleep(nanoseconds: 5_000_000_000)
|
|
}
|
|
onProgress(100)
|
|
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + "_" + fileName)
|
|
try Data([1]).write(to: url)
|
|
return url
|
|
}
|
|
|
|
func attemptCount() async -> Int {
|
|
await counter.current()
|
|
}
|
|
}
|
|
|
|
private actor DownloadAttemptCounter {
|
|
private var value = 0
|
|
|
|
func next() -> Int {
|
|
value += 1
|
|
return value
|
|
}
|
|
|
|
func current() -> Int {
|
|
value
|
|
}
|
|
}
|
|
|
|
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" }
|
|
}
|