import Foundation import Testing @testable import LegadoCore private struct BookFlowMockTransport: HTTPTransport { let pages: [String: String] func data(for request: URLRequest) async throws -> HTTPResponse { let url = try #require(request.url) let body = try #require(pages[url.absoluteString], "缺少 mock 页面:\(url.absoluteString)") return .init(data: Data(body.utf8), statusCode: 200, headers: [:], finalURL: url) } } struct NetworkBookServiceTests { @Test func loadsDetailPaginatedTocAndPaginatedContent() async throws { let transport = BookFlowMockTransport(pages: [ "https://reader.example.test/book/1": """

迁移实战

作者甲

一本测试书

目录
""", "https://reader.example.test/book/1/toc/1": """ """, "https://reader.example.test/book/1/toc/2": """ """, "https://reader.example.test/chapter/1": """

第一章 新标题

第一段

""", "https://reader.example.test/chapter/1?page=2": """

第二段

""" ]) var source = BookSource( bookSourceUrl: "https://reader.example.test", bookSourceName: "完整链路源" ) var info = BookInfoRule() info.`init` = "@CSS:main.book" info.name = "@CSS:h1@text" info.author = "@CSS:.author@text" info.intro = "@CSS:.intro@text" info.coverUrl = "@CSS:img.cover@src" info.tocUrl = "@CSS:a.toc@href" source.ruleBookInfo = info var toc = TocRule() toc.chapterList = "@CSS:li.chapter" toc.chapterName = "@CSS:a@text" toc.chapterUrl = "@CSS:a@href" toc.nextTocUrl = "@CSS:a.next@href" source.ruleToc = toc var content = ContentRule() content.content = "@CSS:article p@text" content.title = "@CSS:h1@text" content.nextContentUrl = "@CSS:a.next@href" source.ruleContent = content let searchBook = SearchBook( bookURL: "https://reader.example.test/book/1", origin: source.bookSourceUrl, originName: source.bookSourceName, type: 0, name: "搜索结果名" ) let service = NetworkBookService(transport: transport) let detail = await service.loadDetail(searchBook: searchBook, source: source) #expect(detail.book.name == "迁移实战") #expect(detail.book.author == "作者甲") #expect(detail.book.coverUrl == "https://reader.example.test/cover.jpg") #expect(detail.book.tocUrl == "https://reader.example.test/book/1/toc/1") #expect(detail.diagnostics.isEmpty) let tocReport = await service.loadChapters(book: detail.book, source: source) #expect(tocReport.chapters.map(\.title) == ["第一章", "第二章"]) #expect(tocReport.chapters.map(\.index) == [0, 1]) #expect(tocReport.pagesVisited == 2) #expect(tocReport.diagnostics.isEmpty) let chapter = try #require(tocReport.chapters.first) let contentReport = await service.loadContent( book: detail.book, chapter: chapter, source: source, nextChapterURL: tocReport.chapters[1].url ) #expect(contentReport.title == "第一章 新标题") #expect(contentReport.content == "第一段\n第二段") #expect(contentReport.pagesVisited == 2) #expect(contentReport.diagnostics.isEmpty) } @Test func returnsDiagnosticsForEmptyContentAndStopsTocLoops() async throws { let transport = BookFlowMockTransport(pages: [ "https://reader.example.test/toc": """
  • 空章
  • """, "https://reader.example.test/chapter/empty": "
    " ]) var source = BookSource(bookSourceUrl: "https://reader.example.test", bookSourceName: "诊断源") var toc = TocRule() toc.chapterList = "@CSS:li.chapter" toc.chapterName = "@CSS:a@text" toc.chapterUrl = "@CSS:a@href" toc.nextTocUrl = "@CSS:a.next@href" source.ruleToc = toc var content = ContentRule() content.content = "@CSS:article@text" source.ruleContent = content var book = Book( bookUrl: "https://reader.example.test/book/empty", name: "空书", origin: source.bookSourceUrl ) book.tocUrl = "https://reader.example.test/toc" let service = NetworkBookService(transport: transport) let tocReport = await service.loadChapters(book: book, source: source) #expect(tocReport.pagesVisited == 1) #expect(tocReport.chapters.count == 1) let chapter = try #require(tocReport.chapters.first) let contentReport = await service.loadContent(book: book, chapter: chapter, source: source) #expect(contentReport.content.isEmpty) #expect(contentReport.diagnostics.contains { $0.code == "content.empty" }) } @Test func carriesBookVariablesIntoJavaScriptChapterRules() async throws { let transport = BookFlowMockTransport(pages: [ "https://audio.example.test/toc": #"{"content":{"content":[{"id":2002,"chapterTitle":"第一集"}]}}"# ]) var source = BookSource(bookSourceUrl: "https://audio.example.test", bookSourceName: "变量源") var toc = TocRule() toc.chapterList = "$.content.content" toc.chapterName = "$.chapterTitle" toc.chapterUrl = "$.id@js:java.put('chapterId', result); '/audio?bookId=' + java.get('bookId') + '&chapterId=' + result" source.ruleToc = toc var book = Book( bookUrl: "https://audio.example.test/book/1001", name: "听书", origin: source.bookSourceUrl ) book.tocUrl = "https://audio.example.test/toc" book.variable = #"{"bookId":"1001"}"# let report = await NetworkBookService(transport: transport).loadChapters(book: book, source: source) let chapter = try #require(report.chapters.first) #expect(chapter.url == "https://audio.example.test/audio?bookId=1001&chapterId=2002") let variables = try JSONDecoder().decode( [String: String].self, from: Data(try #require(chapter.variable).utf8) ) #expect(variables["bookId"] == "1001") #expect(variables["chapterId"] == "2002") #expect(report.diagnostics.isEmpty) } }