mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
111 lines
5.9 KiB
Swift
111 lines
5.9 KiB
Swift
import Foundation
|
|
import Testing
|
|
import LegadoCore
|
|
@testable import LegadoAppModels
|
|
|
|
@MainActor
|
|
struct LocalFileManagementTests {
|
|
private func fixture(fails: Bool = false) async throws -> (URL, Book, BookLibraryModel) {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString, isDirectory: true)
|
|
try FileManager.default.createDirectory(at: root, withIntermediateDirectories: true)
|
|
let file = root.appendingPathComponent("原文件.txt")
|
|
try "第一章 开始\n正文".write(to: file, atomically: true, encoding: .utf8)
|
|
var book = Book(bookUrl: file.absoluteString, name: "原书名", origin: "local")
|
|
book.originName = file.lastPathComponent
|
|
book.durChapterIndex = 2
|
|
book.durChapterTitle = "第三章"
|
|
book.durChapterPos = 42
|
|
book.group = 8
|
|
let repository: any BookRepository = fails ? FileOperationFailingRepository(books: [book]) : InMemoryBookRepository(books: [book])
|
|
let model = BookLibraryModel(repository: repository, localDirectory: root, chapterRepository: InMemoryBookChapterRepository())
|
|
await model.load()
|
|
return (root, book, model)
|
|
}
|
|
|
|
@Test func renamingFileRetargetsShelfAndPreservesTitleAndReadingState() async throws {
|
|
let (root, original, model) = try await fixture()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
try await model.renameLocalFile(at: URL(string: original.bookUrl)!, to: "新文件")
|
|
let book = try #require(model.books.first)
|
|
#expect(book.bookUrl == root.appendingPathComponent("新文件.txt").absoluteString)
|
|
#expect(book.originName == "新文件.txt")
|
|
#expect(book.name == original.name)
|
|
#expect(book.durChapterIndex == 2 && book.durChapterPos == 42 && book.group == 8)
|
|
#expect(FileManager.default.fileExists(atPath: URL(string: book.bookUrl)!.path))
|
|
#expect(!FileManager.default.fileExists(atPath: URL(string: original.bookUrl)!.path))
|
|
}
|
|
|
|
@Test func failedShelfSaveRestoresRenamedFileAndShelf() async throws {
|
|
let (root, book, model) = try await fixture(fails: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
await #expect(throws: (any Error).self) { try await model.renameLocalFile(at: URL(string: book.bookUrl)!, to: "新文件") }
|
|
#expect(model.books == [book])
|
|
#expect(FileManager.default.fileExists(atPath: URL(string: book.bookUrl)!.path))
|
|
#expect(!FileManager.default.fileExists(atPath: root.appendingPathComponent("新文件.txt").path))
|
|
}
|
|
|
|
@Test func failedShelfSaveRestoresDeletedFileBytes() async throws {
|
|
let (root, book, model) = try await fixture(fails: true)
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let file = URL(string: book.bookUrl)!
|
|
let data = try Data(contentsOf: file)
|
|
await #expect(throws: (any Error).self) { try await model.deleteLocalFile(at: file) }
|
|
#expect(model.books == [book])
|
|
#expect(try Data(contentsOf: file) == data)
|
|
}
|
|
|
|
@Test func deletingFileAlsoRemovesShelfReference() async throws {
|
|
let (root, book, model) = try await fixture()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let file = URL(string: book.bookUrl)!
|
|
try await model.deleteLocalFile(at: file)
|
|
#expect(model.books.isEmpty)
|
|
#expect(!FileManager.default.fileExists(atPath: file.path))
|
|
#expect(try await model.localFiles.list().isEmpty)
|
|
}
|
|
|
|
@Test func mixedImportKeepsSuccessfulFilesAndReportsUnsupportedOnes() async throws {
|
|
let (root, original, model) = try await fixture()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let source = root.appendingPathComponent("原文件.txt")
|
|
let unsupported = root.appendingPathComponent("unsupported.pdf")
|
|
try Data("pdf".utf8).write(to: unsupported)
|
|
await model.importLocalFiles([source, unsupported, source])
|
|
#expect(model.books.count == 3)
|
|
#expect(model.books.first == original)
|
|
#expect(model.errorMessage?.contains("unsupported.pdf") == true)
|
|
#expect(try await model.localFiles.list().count == 3)
|
|
}
|
|
|
|
@Test func missingFileFromOldContainerCanBeRemovedFromShelf() async throws {
|
|
let root = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
|
|
let missing = root.appendingPathComponent("old-container/missing.txt")
|
|
let book = Book(bookUrl: missing.absoluteString, name: "丢失的书", origin: "local")
|
|
let model = BookLibraryModel(repository: InMemoryBookRepository(books: [book]), localDirectory: root.appendingPathComponent("current-container"))
|
|
await model.load()
|
|
try await model.deleteLocalFile(at: missing)
|
|
#expect(model.books.isEmpty)
|
|
#expect(!FileManager.default.fileExists(atPath: root.path))
|
|
}
|
|
|
|
@Test func existingFileOutsideLibraryCannotBeDeletedThroughShelf() async throws {
|
|
let (root, _, _) = try await fixture()
|
|
defer { try? FileManager.default.removeItem(at: root) }
|
|
let external = root.appendingPathComponent("原文件.txt")
|
|
let bytes = try Data(contentsOf: external)
|
|
let book = Book(bookUrl: external.absoluteString, name: "外部文件", origin: "local")
|
|
let model = BookLibraryModel(repository: InMemoryBookRepository(books: [book]), localDirectory: root.appendingPathComponent("managed"))
|
|
await model.load()
|
|
await #expect(throws: LocalFileError.outsideLibrary) { try await model.deleteLocalFile(at: external) }
|
|
#expect(model.books == [book])
|
|
#expect(try Data(contentsOf: external) == bytes)
|
|
}
|
|
}
|
|
|
|
private actor FileOperationFailingRepository: BookRepository {
|
|
let books: [Book]
|
|
init(books: [Book]) { self.books = books }
|
|
func load() -> [Book] { books }
|
|
func save(_ books: [Book]) throws { throw CocoaError(.fileWriteOutOfSpace) }
|
|
}
|