import Testing import Foundation @testable import 康康 // MARK: - Mock 网络层 /// 按 URL 注册完整响应体,startLoading 时按请求的 Range header 自动切片返回(206)或全量(200)。 /// 每个测试用唯一 URL 注册自己的内容 → 测试间不会互相覆盖,无需依赖执行顺序或可见性。 final class MockURLProtocol: URLProtocol, @unchecked Sendable { private static let lock = NSLock() private static var bodies: [String: Data] = [:] static func register(_ url: URL, body: Data) { lock.lock(); defer { lock.unlock() } bodies[url.path] = body } static func reset() { lock.lock(); defer { lock.unlock() } bodies.removeAll() } private static func body(forPath path: String) -> Data? { lock.lock(); defer { lock.unlock() } return bodies[path] } override class func canInit(with request: URLRequest) -> Bool { true } override class func canonicalRequest(for request: URLRequest) -> URLRequest { request } override func startLoading() { guard let url = request.url, let full = Self.body(forPath: url.path) else { client?.urlProtocol(self, didFailWithError: URLError(.fileDoesNotExist)) return } var data = full var status = 200 var headers: [String: String] = [:] if let range = request.value(forHTTPHeaderField: "Range"), let start = Self.parseRangeStart(range), start <= full.count { data = Data(full.suffix(from: start)) status = 206 headers["Content-Range"] = "bytes \(start)-\(full.count - 1)/\(full.count)" } let response = HTTPURLResponse( url: url, statusCode: status, httpVersion: nil, headerFields: headers)! client?.urlProtocol(self, didReceive: response, cacheStoragePolicy: .notAllowed) client?.urlProtocol(self, didLoad: data) client?.urlProtocolDidFinishLoading(self) } override func stopLoading() {} /// "bytes=2-" → 2 private static func parseRangeStart(_ s: String) -> Int? { guard let eq = s.firstIndex(of: "="), let dash = s.firstIndex(of: "-") else { return nil } return Int(s[s.index(after: eq).. URLSessionConfiguration { let config = URLSessionConfiguration.ephemeral config.protocolClasses = [MockURLProtocol.self] return config } private func tempFile() -> URL { FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString, isDirectory: true) .appendingPathComponent("a.bin") } private func uniqueURL() -> URL { URL(string: "https://mock.test/\(UUID().uuidString).bin")! } // MARK: - DownloadState struct DownloadStateTests { @Test func fractionZeroWhenTotalZero() { let s = DownloadState(phase: .idle, receivedBytes: 0, totalBytes: 0, bytesPerSecond: 0) #expect(s.fraction == 0) } @Test func fractionComputed() { let s = DownloadState(phase: .downloading, receivedBytes: 50, totalBytes: 200, bytesPerSecond: 0) #expect(s.fraction == 0.25) } } // MARK: - FileDownloader /// 串行执行:这些测试共享全局 URLProtocol / URLSession 状态,并行会互相干扰。 @Suite(.serialized) struct FileDownloaderTests { @Test func downloadsFileContent() async throws { let url = uniqueURL() MockURLProtocol.register(url, body: Data("hello".utf8)) let dst = tempFile() defer { try? FileManager.default.removeItem(at: dst.deletingLastPathComponent()) } let dl = FileDownloader(configuration: mockConfiguration()) try await dl.download(from: url, to: dst, expectedBytes: 5) #expect(try Data(contentsOf: dst) == Data("hello".utf8)) #expect(!FileManager.default.fileExists(atPath: dst.appendingPathExtension("part").path)) } @Test func resumesFromPartialFile() async throws { let url = uniqueURL() MockURLProtocol.register(url, body: Data("hello".utf8)) let dst = tempFile() defer { try? FileManager.default.removeItem(at: dst.deletingLastPathComponent()) } // 预置已下载的一半,download 应从 offset 2 续传 try FileManager.default.createDirectory( at: dst.deletingLastPathComponent(), withIntermediateDirectories: true) try Data("he".utf8).write(to: dst.appendingPathExtension("part")) let dl = FileDownloader(configuration: mockConfiguration()) try await dl.download(from: url, to: dst, expectedBytes: 5) #expect(try Data(contentsOf: dst) == Data("hello".utf8)) } @Test func throwsOnSizeMismatch() async throws { let url = uniqueURL() MockURLProtocol.register(url, body: Data("hi".utf8)) // 仅 2 字节,期望 5 let dst = tempFile() defer { try? FileManager.default.removeItem(at: dst.deletingLastPathComponent()) } let dl = FileDownloader(configuration: mockConfiguration()) await #expect(throws: (any Error).self) { try await dl.download(from: url, to: dst, expectedBytes: 5) } #expect(!FileManager.default.fileExists(atPath: dst.path)) } }