Files
legado-ios/Tests/LegadoCoreTests/LocalFileServiceTests.swift

110 lines
6.3 KiB
Swift

import Foundation
import Testing
@testable import LegadoCore
struct LocalFileServiceTests {
private func root() throws -> URL {
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
return url
}
@Test func importsSameNamedFilesWithoutOverwritingAndIgnoresSymlinks() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let source = root.appendingPathComponent("书.txt")
try "第一章\n正文".write(to: source, atomically: true, encoding: .utf8)
let directory = root.appendingPathComponent("books", isDirectory: true)
let service = LocalFileService(directory: directory)
let first = try await service.importFile(from: source)
let second = try await service.importFile(from: source)
try FileManager.default.createSymbolicLink(at: directory.appendingPathComponent("link.txt"), withDestinationURL: source)
let files = try await service.list()
#expect(files.count == 2)
#expect(Set(files.map { $0.url.lastPathComponent }) == ["书.txt", "书 (2).txt"])
#expect(try Data(contentsOf: first) == Data(contentsOf: second))
}
@Test func rejectsEscapesSymlinksAndOverwriteDuringRename() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let directory = root.appendingPathComponent("books", isDirectory: true)
let service = LocalFileService(directory: directory)
let source = root.appendingPathComponent("source.txt")
try "正文".write(to: source, atomically: true, encoding: .utf8)
let file = try await service.importFile(from: source)
let second = try await service.importFile(from: source)
await #expect(throws: LocalFileError.invalidName) { try await service.rename(file, to: "../escape") }
await #expect(throws: LocalFileError.alreadyExists) { try await service.rename(file, to: second.lastPathComponent) }
await #expect(throws: LocalFileError.outsideLibrary) { try await service.stageRemoval(source) }
let link = directory.appendingPathComponent("link.txt")
try FileManager.default.createSymbolicLink(at: link, withDestinationURL: source)
await #expect(throws: LocalFileError.outsideLibrary) { try await service.stageRemoval(link) }
#expect(FileManager.default.fileExists(atPath: source.path))
#expect(FileManager.default.fileExists(atPath: file.path))
}
@Test func stagedDeletionCanRestoreOriginalBytes() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let source = root.appendingPathComponent("书.txt")
let data = Data([0xff, 0xfe, 0x61, 0x00])
try data.write(to: source)
let service = LocalFileService(directory: root)
let staged = try #require(try await service.stageRemoval(source))
#expect(!FileManager.default.fileExists(atPath: source.path))
#expect(try await service.list().isEmpty)
try await service.restore(staged)
#expect(try Data(contentsOf: source) == data)
let final = try #require(try await service.stageRemoval(source))
try await service.finishRemoval(final)
#expect(!FileManager.default.fileExists(atPath: final.staged.path))
}
@Test func originalExportPreservesEncodingAndNeverMovesSource() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let source = root.appendingPathComponent("书.txt")
let data = Data([0xff, 0xfe, 0x61, 0x00])
try data.write(to: source)
let exporter = BookFileExporter(directory: root.appendingPathComponent("exports"))
let result = try await exporter.originalFile(source, filename: "../书.txt")
#expect(try Data(contentsOf: result.url) == data)
#expect(try Data(contentsOf: source) == data)
#expect(result.url.lastPathComponent == ".._书.txt")
try await exporter.discard(result)
#expect(!FileManager.default.fileExists(atPath: result.url.path))
#expect(FileManager.default.fileExists(atPath: source.path))
}
@Test func cachedExportUsesDirectoryOrderAndMarksMissingChapters() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let cache = ChapterContentCache(directory: root.appendingPathComponent("cache"))
let book = Book(bookUrl: "book", name: "书", author: "作者")
let chapters = (0..<3).map { BookChapter(url: "c\($0)", title: "第\($0 + 1)章", bookUrl: "book", index: $0) }
try await cache.save(.init(bookURL: "book", chapterURL: "c2", content: "第三章正文", supplementalContent: "补充"))
try await cache.save(.init(bookURL: "book", chapterURL: "c0", content: "第一章正文"))
let exporter = BookFileExporter(directory: root.appendingPathComponent("exports"), cache: cache)
let result = try await exporter.cachedBook(book, chapters: chapters.reversed())
let text = try String(contentsOf: result.url, encoding: .utf8)
#expect(result.cachedChapters == 2)
#expect(result.totalChapters == 3)
#expect(result.isPartial)
#expect(text.contains("第2章\n\n【本章尚未缓存】"))
#expect(text.contains("第三章正文\n补充"))
#expect(try #require(text.range(of: "第一章正文")).lowerBound < #require(text.range(of: "第三章正文")).lowerBound)
}
@Test func emptyCacheCannotProduceMisleadingEmptyBook() async throws {
let root = try root()
defer { try? FileManager.default.removeItem(at: root) }
let exports = root.appendingPathComponent("exports")
let exporter = BookFileExporter(directory: exports, cache: ChapterContentCache(directory: root.appendingPathComponent("cache")))
let book = Book(bookUrl: "book", name: "书")
let chapter = BookChapter(url: "c", title: "第一章", bookUrl: "book", index: 0)
await #expect(throws: LocalFileError.emptyCache) { try await exporter.cachedBook(book, chapters: [chapter]) }
#expect(try FileManager.default.contentsOfDirectory(atPath: exports.path).isEmpty)
}
}