mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
117 lines
7.4 KiB
Swift
117 lines
7.4 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import LegadoCore
|
|
|
|
struct RuleEditingCompatibilityTests {
|
|
@Test func unknownFieldsSurviveImportFieldEditingAndRepositoryRoundTrip() async throws {
|
|
let input = #"{"bookSourceUrl":"https://roundtrip.test","bookSourceName":"扩展源","futureOption":{"token":"keep"},"ruleSearch":"{\"bookList\":\"$.books\",\"name\":\"$.name\",\"bookUrl\":\"$.url\",\"futureRule\":{\"x\":true}}"}"#
|
|
let original = try #require(BookSourceImporter().decode(Data(input.utf8)).first)
|
|
var document = try BookSourceEditorDocument(source: original)
|
|
document.set(.string("已编辑"), at: "bookSourceName")
|
|
document.set(.string("$.title"), at: "ruleSearch.name")
|
|
let edited = try document.source()
|
|
let repository = InMemoryBookSourceRepository()
|
|
try await repository.save([edited])
|
|
let restored = try #require(await repository.load().first)
|
|
let json = try JSONValue.decode(data: JSONEncoder().encode(restored))
|
|
#expect(json["futureOption"]?["token"] == .string("keep"))
|
|
#expect(json["ruleSearch"]?["futureRule"]?["x"] == .bool(true))
|
|
#expect(restored.ruleSearch?.name == "$.title")
|
|
#expect(restored.bookSourceName == "已编辑")
|
|
#expect(json["additionalFields"] == nil)
|
|
}
|
|
|
|
@Test func clearingKnownRuleDoesNotRestoreOldValueFromPreservedFields() throws {
|
|
let source = try JSONDecoder().decode(BookSource.self, from: Data(#"{"bookSourceUrl":"https://test","bookSourceName":"书源","ruleContent":{"content":"article@text","future":42}}"#.utf8))
|
|
var document = try BookSourceEditorDocument(source: source)
|
|
document.set(nil, at: "ruleContent.content")
|
|
let json = try JSONValue.decode(data: JSONEncoder().encode(document.source()))
|
|
#expect(json["ruleContent"]?["content"] == nil)
|
|
#expect(json["ruleContent"]?["future"] == .number(42))
|
|
}
|
|
|
|
@Test func emptyRuleBlocksAndExplicitCookieNullRoundTrip() throws {
|
|
let original = try JSONDecoder().decode(BookSource.self, from: Data(#"{"bookSourceUrl":"https://test","bookSourceName":"空规则","ruleExplore":{},"enabledCookieJar":null}"#.utf8))
|
|
let restored = try JSONDecoder().decode(BookSource.self, from: JSONEncoder().encode(original))
|
|
#expect(restored.ruleExplore != nil)
|
|
#expect(restored.enabledCookieJar == nil)
|
|
#expect(restored == original)
|
|
}
|
|
|
|
@Test func partialInspectionReportsMalformedEntriesAndKeepsValidSiblings() throws {
|
|
let input = #"[{"bookSourceUrl":"https://good","bookSourceName":"有效"},{"bookSourceUrl":"https://bad","bookSourceName":"错误","ruleSearch":{"name":12}},17,{"bookSourceName":"缺少地址"}]"#
|
|
let inspected = try BookSourceImporter().inspect(Data(input.utf8))
|
|
#expect(inspected.sources.map(\.bookSourceName) == ["有效"])
|
|
#expect(inspected.rejected.count == 3)
|
|
#expect(throws: BookSourceImportError.self) { try BookSourceImporter().decode(Data(input.utf8)) }
|
|
}
|
|
|
|
@Test func editorRetainsRawInvalidJSONAndRejectsIncompleteSourceAtSave() throws {
|
|
#expect(throws: (any Error).self) { try BookSourceEditorDocument(json: "{") }
|
|
#expect(throws: BookSourceImportError.self) { try BookSourceEditorDocument(json: "[]") }
|
|
let draft = try BookSourceEditorDocument(json: #"{"bookSourceName":"未完成"}"#)
|
|
#expect(throws: BookSourceImportError.self) { try draft.source() }
|
|
}
|
|
|
|
@Test func importInputAcceptsQRJSONDeepLinksAndWithoutUA() throws {
|
|
let deep = "legado://import/bookSource?src=https%3A%2F%2Fexample.test%2Fsources.json%23requestWithoutUA"
|
|
let parsed = try RuleImportInput.parse(deep)
|
|
#expect(parsed == .remote(URL(string: "https://example.test/sources.json")!, withoutUserAgent: true))
|
|
#expect(parsed.request()?.value(forHTTPHeaderField: "User-Agent") == "null")
|
|
#expect(try RuleImportInput.parse("[合集](https://example.test/source.json)") == .remote(URL(string: "https://example.test/source.json")!, withoutUserAgent: false))
|
|
#expect(try RuleImportInput.parse(#"{"bookSourceName":"测试"}"#) == .json(#"{"bookSourceName":"测试"}"#))
|
|
#expect(throws: BookSourceImportError.self) { try RuleImportInput.parse("legado://import/replaceRule?src=https://example.test/rules") }
|
|
#expect(throws: BookSourceImportError.self) { try RuleImportInput.parse("file:///etc/passwd") }
|
|
#expect(throws: BookSourceImportError.self) { try RuleImportInput.parse("https://") }
|
|
}
|
|
|
|
@Test func purificationTestMatchesReaderScopesAndCaptures() throws {
|
|
var rule = ReplaceRule(id: 1, name: "替换", pattern: "第([0-9]+)章", replacement: "Chapter $1")
|
|
rule.scopeTitle = true
|
|
rule.scopeContent = true
|
|
rule.scope = "示例书;https://test"
|
|
let test = try ReplaceRuleTester().test(rule: rule, title: "第12章", content: "第34章 正文", bookName: "示例书", sourceURL: "https://test")
|
|
#expect(test.report.title == "Chapter 12")
|
|
#expect(test.report.content == "Chapter 34 正文")
|
|
#expect(test.report.effectiveRuleIDs == [1])
|
|
rule.excludeScope = "示例书"
|
|
let excluded = try ReplaceRuleTester().test(rule: rule, title: "第12章", content: "第34章 正文", bookName: "示例书", sourceURL: "https://test")
|
|
#expect(excluded.report.title == "第12章")
|
|
#expect(excluded.report.effectiveRuleIDs.isEmpty)
|
|
}
|
|
|
|
@Test func purificationLiteralReplacementKeepsDollarSignsAndWhitespace() throws {
|
|
var rule = ReplaceRule(id: 2, name: "字面", pattern: "[广告]", replacement: "$1")
|
|
rule.isRegex = false
|
|
let result = try ReplaceRuleTester().test(rule: rule, title: "标题", content: " 正文\n[广告]\n", bookName: "", sourceURL: "")
|
|
#expect(result.report.content == " 正文\n$1\n")
|
|
}
|
|
|
|
@Test func invalidPurificationRegexReportsErrorInsteadOfLiteralFallback() throws {
|
|
let rule = ReplaceRule(id: 3, name: "损坏", pattern: "[")
|
|
#expect(throws: ReplaceRuleImportError.self) { try ReplaceRuleValidator.validate(rule) }
|
|
let book = Book(bookUrl: "test", name: "书", origin: "test")
|
|
let chapter = BookChapter(url: "chapter", title: "标题", bookUrl: "test", index: 0)
|
|
let report = ContentSanitizer().process(book: book, chapter: chapter, content: "[原文", rules: [rule])
|
|
#expect(report.content == "[原文")
|
|
#expect(report.diagnostics.first?.code == "rule.malformed")
|
|
}
|
|
|
|
@Test func regexDeadlineInterruptsBacktrackingAndDoesNotReturnPartialReplacement() {
|
|
let input = String(repeating: "a", count: 100) + "!"
|
|
#expect(throws: RuleEngineError.self) {
|
|
try RegexEvaluator().replacingStrict(pattern: "(a|aa)+$", in: input, with: "changed", timeoutMilliseconds: 1)
|
|
}
|
|
}
|
|
|
|
@Test func purificationResourceLimitsPreserveInputAndReportDiagnostic() throws {
|
|
let rule = ReplaceRule(id: 3, name: "嵌套", pattern: "(a+)+$", replacement: "changed")
|
|
let result = try ReplaceRuleTester().test(rule: rule, title: "标题", content: "aaaa!", bookName: "书", sourceURL: "test")
|
|
#expect(result.report.content == "aaaa!")
|
|
#expect(result.report.diagnostics.first?.code == "rule.resource_limit")
|
|
let regex = RegexEvaluator(limits: .init(maximumMatches: 2))
|
|
#expect(throws: RuleEngineError.self) { try regex.replacingStrict(pattern: "a", in: "aaa", with: "b") }
|
|
#expect(try regex.replacingStrict(pattern: "a", in: "aa", with: "b") == "bb")
|
|
}
|
|
}
|