mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
501 lines
22 KiB
Swift
501 lines
22 KiB
Swift
import Foundation
|
|
import Testing
|
|
@testable import LegadoCore
|
|
|
|
private struct JavaScriptNetworkMock: JavaScriptNetworkLoading {
|
|
func load(_ request: URLRequest) throws -> HTTPResponse {
|
|
#expect(request.url?.absoluteString == "https://js-network.example.test/api?page=2")
|
|
#expect(request.httpMethod == "POST")
|
|
#expect(String(data: request.httpBody ?? Data(), encoding: .utf8) == #"{"id":7}"#)
|
|
#expect(request.value(forHTTPHeaderField: "X-Source") == "source")
|
|
#expect(request.value(forHTTPHeaderField: "X-Extra") == "extra")
|
|
let url = try #require(request.url)
|
|
return .init(
|
|
data: Data(#"{"ok":true}"#.utf8),
|
|
statusCode: 201,
|
|
headers: ["X-Reply": "yes"],
|
|
finalURL: url
|
|
)
|
|
}
|
|
}
|
|
|
|
private final class RetryingLegacyJavaScriptNetworkMock: JavaScriptNetworkLoading, @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var count = 0
|
|
|
|
func load(_ request: URLRequest) throws -> HTTPResponse {
|
|
lock.lock()
|
|
count += 1
|
|
let current = count
|
|
lock.unlock()
|
|
if current == 1 {
|
|
return .init(
|
|
data: Data("temporary".utf8),
|
|
statusCode: 503,
|
|
headers: [:],
|
|
finalURL: try #require(request.url)
|
|
)
|
|
}
|
|
#expect(request.value(forHTTPHeaderField: "Cookie")?.contains("explicit=yes") == true)
|
|
let encoding = try #require(HTTPTextDecoder.encoding(named: "gb18030"))
|
|
return .init(
|
|
data: try #require("旧编码辅助请求".data(using: encoding)),
|
|
statusCode: 200,
|
|
headers: ["Set-Cookie": "session=js-ok; Path=/"],
|
|
finalURL: try #require(request.url)
|
|
)
|
|
}
|
|
|
|
func calls() -> Int {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return count
|
|
}
|
|
}
|
|
|
|
private struct JavaScriptAjaxAllMock: JavaScriptNetworkLoading {
|
|
func load(_ request: URLRequest) throws -> HTTPResponse {
|
|
let url = try #require(request.url)
|
|
return .init(
|
|
data: Data("body:\(url.lastPathComponent)".utf8),
|
|
statusCode: 200,
|
|
headers: [:],
|
|
finalURL: url
|
|
)
|
|
}
|
|
}
|
|
|
|
private final class JavaScriptDirectRequestMock: JavaScriptRedirectNetworkLoading, @unchecked Sendable {
|
|
private let lock = NSLock()
|
|
private var recorded: [(URLRequest, Bool)] = []
|
|
|
|
func load(_ request: URLRequest) throws -> HTTPResponse {
|
|
try load(request, followRedirects: true)
|
|
}
|
|
|
|
func load(_ request: URLRequest, followRedirects: Bool) throws -> HTTPResponse {
|
|
lock.lock()
|
|
recorded.append((request, followRedirects))
|
|
lock.unlock()
|
|
let url = try #require(request.url)
|
|
return .init(
|
|
data: Data("reply-\(request.httpMethod ?? "")".utf8),
|
|
statusCode: request.httpMethod == "GET" ? 302 : 200,
|
|
headers: ["X-Reply": "direct"],
|
|
finalURL: url
|
|
)
|
|
}
|
|
|
|
func calls() -> [(URLRequest, Bool)] {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return recorded
|
|
}
|
|
}
|
|
|
|
struct JavaScriptRuleEvaluatorTests {
|
|
@Test func decryptsSourceContentUsingRhinoPackageImportsAndAESByteArrays() throws {
|
|
// OpenSSL AES-128-CBC/PKCS7 fixture, with a 16-byte IV prepended.
|
|
let script = #"""
|
|
var javaImport = new JavaImporter();
|
|
javaImport.importPackage(Packages.java.lang, Packages.javax.crypto.spec,
|
|
Packages.javax.crypto, Packages.java.util);
|
|
with (javaImport) {
|
|
var encrypted = Base64.getDecoder().decode('ZmVkY2JhOTg3NjU0MzIxMLgd3vmXCW/KPKUrbea1zRjjTRawCuSDRVQflh/mHcYAv/gm12GQKUL0NhitscZMdw==');
|
|
var key = SecretKeySpec(String('0123456789abcdef').getBytes(), 'AES');
|
|
var iv = IvParameterSpec(Arrays.copyOfRange(encrypted, 0, 16));
|
|
var cipher = Cipher.getInstance('AES/CBC/PKCS5Padding');
|
|
cipher.init(Cipher.DECRYPT_MODE, key, iv);
|
|
var decoded = String(cipher.doFinal(Arrays.copyOfRange(encrypted, 16, encrypted.length)));
|
|
cipher.init(Cipher.ENCRYPT_MODE, key, iv);
|
|
var encoded = Base64.getEncoder().encodeToString(cipher.doFinal(decoded.getBytes()));
|
|
decoded + '|' + encoded;
|
|
}
|
|
"""#
|
|
let value = try JavaScriptRuleEvaluator().evaluate(script: script, result: .null, executionContext: .init())
|
|
#expect(value.value.stringValue == "第一段正文\n第二段正文。|uB3e+ZcJb8o8pStt5rXNGONNFrAK5INFVB+WH+YdxgC/+CbXYZApQvQ2GK2xxkx3")
|
|
}
|
|
|
|
@Test func javaCipherRejectsInvalidInputsAndUnsupportedPackageImports() throws {
|
|
for script in [
|
|
"var i = new JavaImporter(); i.importPackage(Packages.java.io);",
|
|
"Packages.javax.crypto.Cipher.getInstance('DES/ECB/NoPadding');",
|
|
"var i = new JavaImporter(Packages.java.lang, Packages.javax.crypto.spec, Packages.javax.crypto); with(i) { var c = Cipher.getInstance('AES/CBC/PKCS5Padding'); c.init(2, SecretKeySpec(String('short').getBytes(), 'AES'), IvParameterSpec(String('0123456789abcdef').getBytes())); c.doFinal([1, 2, 3]); }"
|
|
] {
|
|
#expect(throws: RuleEngineError.self) {
|
|
try JavaScriptRuleEvaluator().evaluate(script: script, result: .null, executionContext: .init())
|
|
}
|
|
}
|
|
}
|
|
|
|
@Test func importedAESHelperDeclaredInsideWithRetainsJavaStringByteDecoding() throws {
|
|
let script = #"""
|
|
var javaImport = new JavaImporter();
|
|
javaImport.importPackage(Packages.java.lang, Packages.javax.crypto.spec,
|
|
Packages.javax.crypto, Packages.java.util);
|
|
with (javaImport) {
|
|
function decode(content) {
|
|
var bytes = Base64.getDecoder().decode(String(content));
|
|
var key = SecretKeySpec(String('0123456789abcdef').getBytes(), 'AES');
|
|
var iv = IvParameterSpec(Arrays.copyOfRange(bytes, 0, 16));
|
|
var cipher = Cipher.getInstance('AES/CBC/PKCS5Padding');
|
|
cipher.init(2, key, iv);
|
|
return String(cipher.doFinal(Arrays.copyOfRange(bytes, 16, bytes.length)));
|
|
}
|
|
}
|
|
decode('ZmVkY2JhOTg3NjU0MzIxMLgd3vmXCW/KPKUrbea1zRjjTRawCuSDRVQflh/mHcYAv/gm12GQKUL0NhitscZMdw==');
|
|
"""#
|
|
let value = try JavaScriptRuleEvaluator().evaluate(script: script, result: .null, executionContext: .init())
|
|
#expect(value.value.stringValue == "第一段正文\n第二段正文。")
|
|
}
|
|
|
|
@Test func supportsBundledHttpTTSJavaImporterHmacSHA1Script() throws {
|
|
let script = #"""
|
|
var aly = new JavaImporter(
|
|
Packages.javax.crypto.Mac,
|
|
Packages.javax.crypto.spec.SecretKeySpec,
|
|
Packages.javax.xml.bind.DatatypeConverter,
|
|
Packages.java.net.URLEncoder,
|
|
Packages.java.lang.String,
|
|
Packages.android.util.Base64
|
|
);
|
|
with (aly) {
|
|
var mac = Mac.getInstance('HmacSHA1');
|
|
mac.init(new SecretKeySpec(String('key').getBytes('UTF-8'), 'HmacSHA1'));
|
|
var signature = Base64.encodeToString(
|
|
mac.doFinal(String('The quick brown fox jumps over the lazy dog').getBytes('UTF-8')),
|
|
Base64.NO_WRAP
|
|
);
|
|
signature + ':' + URLEncoder.encode('a b*~中', 'UTF-8');
|
|
}
|
|
"""#
|
|
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: script,
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "3nybhbi3iqa8ino29wqQcBydtNk=:a+b*%7E%E4%B8%AD")
|
|
}
|
|
|
|
@Test func javaImporterDoesNotExposeArbitraryNativeClasses() throws {
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "typeof Packages.java.io + ':' + typeof Packages.java.lang.Runtime + ':' + Object.isFrozen(Packages)",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "undefined:undefined:true")
|
|
}
|
|
|
|
@Test func exposesDollarAsCurrentJSONForLegadoInlineExpressions() throws {
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "$.book_id",
|
|
result: .object(["book_id": .string("42")]),
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "42")
|
|
}
|
|
|
|
@Test func exposesJavaGetStringRuleBridge() throws {
|
|
let evaluator = JavaScriptRuleEvaluator()
|
|
let html = #"<article><a href="/chapter/1">第一章</a></article>"#
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "java.getString('a@text', result) + ':' + java.getString('a@href', result, true)",
|
|
result: .string(html),
|
|
executionContext: .init(baseURL: "https://reader.example/book/")
|
|
)
|
|
let directJSON = try evaluator.evaluate(
|
|
script: "java.getString('name', result) + ':' + java.getString('$.id', result)",
|
|
result: .object(["name": .string("书名"), "id": .number(42)]),
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "第一章:https://reader.example/chapter/1")
|
|
#expect(directJSON.value.stringValue == "书名:42")
|
|
}
|
|
|
|
@Test func jsonParseKeepsPlainTextResponsesButStillRejectsBrokenJSON() throws {
|
|
let evaluator = JavaScriptRuleEvaluator()
|
|
let plain = try evaluator.evaluate(
|
|
script: "JSON.parse(result)",
|
|
result: .string("The server is temporarily unavailable"),
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(plain.value.stringValue == "The server is temporarily unavailable")
|
|
#expect(throws: RuleEngineError.self) {
|
|
_ = try evaluator.evaluate(
|
|
script: "JSON.parse(result)",
|
|
result: .string(#"{"broken":}"#),
|
|
executionContext: .init()
|
|
)
|
|
}
|
|
}
|
|
|
|
@Test func directJavaGetResolvesRelativeURLAndAppliesSourceHeaders() throws {
|
|
var source = BookSource(bookSourceUrl: "https://direct.example.test/base/", bookSourceName: "相对请求")
|
|
source.header = #"{"X-Source":"yes"}"#
|
|
let loader = JavaScriptDirectRequestMock()
|
|
let evaluator = JavaScriptRuleEvaluator(networkLoader: loader)
|
|
|
|
_ = try evaluator.evaluate(
|
|
script: "java.get('/chapter/1').body()",
|
|
result: .null,
|
|
executionContext: .init(baseURL: source.bookSourceUrl, source: source)
|
|
)
|
|
|
|
let call = try #require(loader.calls().first)
|
|
#expect(call.0.url?.absoluteString == "https://direct.example.test/chapter/1")
|
|
#expect(call.0.value(forHTTPHeaderField: "X-Source") == "yes")
|
|
#expect(call.0.value(forHTTPHeaderField: "User-Agent") != nil)
|
|
}
|
|
|
|
@Test func loadsSharedSourceJavaScriptLibrary() throws {
|
|
var source = BookSource(bookSourceUrl: "https://js-lib.example.test", bookSourceName: "共享脚本源")
|
|
source.jsLib = "function decorate(value) { return 'lib:' + value; }"
|
|
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "decorate(result)",
|
|
result: .string("ok"),
|
|
executionContext: .init(source: source)
|
|
)
|
|
|
|
#expect(value.value.stringValue == "lib:ok")
|
|
}
|
|
@Test func providesAjaxAndConnectWithLegadoURLOptions() throws {
|
|
var source = BookSource(bookSourceUrl: "https://js-network.example.test", bookSourceName: "JS 网络源")
|
|
source.header = #"{"X-Source":"source"}"#
|
|
let evaluator = JavaScriptRuleEvaluator(networkLoader: JavaScriptNetworkMock())
|
|
let url = #"/api?page=2,{"method":"POST","body":{"id":7},"headers":{"X-Extra":"extra"}}"#
|
|
|
|
let ajax = try evaluator.evaluate(
|
|
script: "java.ajax(result)",
|
|
result: .string(url),
|
|
executionContext: .init(baseURL: source.bookSourceUrl, source: source)
|
|
)
|
|
let connect = try evaluator.evaluate(
|
|
script: "var r=java.connect(result); r.body + ':' + r.code + ':' + r.headers['X-Reply']",
|
|
result: .string(url),
|
|
executionContext: .init(baseURL: source.bookSourceUrl, source: source)
|
|
)
|
|
|
|
#expect(ajax.value.stringValue == #"{"ok":true}"#)
|
|
#expect(connect.value.stringValue == #"{"ok":true}:201:yes"#)
|
|
}
|
|
@Test func ajaxUsesCharsetHTTPRetryCookieMergeAndBodyJavaScript() throws {
|
|
let host = "https://legacy-js.example.test"
|
|
LegadoCookieStore.shared.removeCookies(for: host)
|
|
defer { LegadoCookieStore.shared.removeCookies(for: host) }
|
|
LegadoCookieStore.shared.setCookie("stored=one; Path=/", for: host)
|
|
var source = BookSource(bookSourceUrl: host, bookSourceName: "旧编码 JS 源")
|
|
source.enabledCookieJar = true
|
|
let loader = RetryingLegacyJavaScriptNetworkMock()
|
|
let evaluator = JavaScriptRuleEvaluator(networkLoader: loader)
|
|
let rule = #"/api, {"charset":"gb18030","retry":1,"headers":{"Cookie":"explicit=yes"},"bodyJs":"result.replace('辅助', '内部')"}"#
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "java.ajax(result) + ':' + java.getCookie('\(host)', 'session')",
|
|
result: .string(rule),
|
|
executionContext: .init(baseURL: host, source: source)
|
|
)
|
|
|
|
#expect(value.value.stringValue == "旧编码内部请求:js-ok")
|
|
#expect(loader.calls() == 2)
|
|
}
|
|
@Test func readsAndWritesSharedCookies() throws {
|
|
let host = "https://cookie-js-bridge.test"
|
|
LegadoCookieStore.shared.removeCookies(for: host)
|
|
defer { LegadoCookieStore.shared.removeCookies(for: host) }
|
|
let evaluator = JavaScriptRuleEvaluator()
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "java.setCookie('\(host)', 'token=abc; Path=/'); java.getCookie('\(host)', 'token') + ':' + java.getCookie('\(host)').indexOf('token=abc')",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "abc:0")
|
|
#expect(LegadoCookieStore.shared.cookieHeader(for: host).contains("token=abc"))
|
|
}
|
|
@Test func exposesGlobalCookieStore() throws {
|
|
// Use a separate registrable domain because Swift Testing runs these
|
|
// cookie-store tests concurrently.
|
|
let host = "https://global-cookie.test"
|
|
LegadoCookieStore.shared.removeCookies(for: host)
|
|
defer { LegadoCookieStore.shared.removeCookies(for: host) }
|
|
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "cookie.setCookie('\(host)', 'token=abc; Path=/'); var map=cookie.cookieToMap(cookie.getCookie('\(host)')); var header=cookie.mapToCookie(map); cookie.getKey('\(host)', 'token') + ':' + header",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "abc:token=abc")
|
|
|
|
let removed = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "cookie.removeCookie('\(host)'); cookie.getCookie('\(host)')",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
#expect(removed.value.stringValue.isEmpty)
|
|
}
|
|
@Test func ajaxAllReturnsResponseObjectsInInputOrder() throws {
|
|
let evaluator = JavaScriptRuleEvaluator(networkLoader: JavaScriptAjaxAllMock())
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "var rows=java.ajaxAll(['https://ajax-all.test/a','https://ajax-all.test/b'], false); rows[0].body + '|' + rows[1].url",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "body:a|https://ajax-all.test/b")
|
|
}
|
|
@Test func getHeadPostExposeJsoupStyleResponseAndDisableRedirects() throws {
|
|
let loader = JavaScriptDirectRequestMock()
|
|
let evaluator = JavaScriptRuleEvaluator(networkLoader: loader)
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "var g=java.get('https://direct.test/get', {'X-Test':'yes'}, 5000); var h=java.head('https://direct.test/head', {}); var p=java.post('https://direct.test/post', 'raw=正文', {'Content-Type':'text/plain'}); g.statusCode() + ':' + g.header('x-reply') + ':' + h.body() + ':' + p.body()",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
let calls = loader.calls()
|
|
#expect(value.value.stringValue == "302:direct:reply-HEAD:reply-POST")
|
|
#expect(calls.count == 3)
|
|
#expect(calls.allSatisfy { !$0.1 })
|
|
#expect(calls[0].0.value(forHTTPHeaderField: "X-Test") == "yes")
|
|
#expect(calls[1].0.httpBody == nil)
|
|
#expect(String(decoding: calls[2].0.httpBody ?? Data(), as: UTF8.self) == "raw=正文")
|
|
}
|
|
@Test func executesPureJavaScriptAndJSONPathPipeline() async throws {
|
|
let engine = RuleEngine()
|
|
let root = try JSONValue.decode(data: Data(#"{"id":42,"name":"测试"}"#.utf8))
|
|
|
|
let pure = await engine.evaluate("@js:result.name + '-JS'", in: root)
|
|
#expect(pure.firstString == "测试-JS")
|
|
#expect(pure.diagnostics.isEmpty)
|
|
|
|
let pipeline = await engine.evaluate(
|
|
"$.id@js:java.put('bookId', result); '/book/' + result",
|
|
in: root
|
|
)
|
|
#expect(pipeline.firstString == "/book/42")
|
|
#expect(pipeline.variables["bookId"] == "42")
|
|
#expect(pipeline.diagnostics.isEmpty)
|
|
}
|
|
|
|
@Test func carriesVariablesAndProvidesLegadoHelpers() async {
|
|
let engine = RuleEngine()
|
|
let context = RuleExecutionContext(variables: ["bookId": "1001"])
|
|
|
|
let result = await engine.evaluate(
|
|
"@js:java.md5Encode(java.get('bookId')).length + ':' + java.get('bookId')",
|
|
in: .null,
|
|
context: context
|
|
)
|
|
|
|
#expect(result.firstString == "32:1001")
|
|
#expect(result.variables["bookId"] == "1001")
|
|
}
|
|
|
|
@Test func providesByteHexTimeAndUUIDHelpers() throws {
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "java.bytesToStr(java.strToBytes('正文')) + ':' + java.hexDecodeToString(java.hexEncodeToString('A')) + ':' + java.timeFormatUTC(0, 'yyyy-MM-dd HH:mm', 0) + ':' + java.randomUUID().length",
|
|
result: .null,
|
|
executionContext: .init()
|
|
)
|
|
|
|
#expect(value.value.stringValue == "正文:A:1970-01-01 00:00:36")
|
|
}
|
|
|
|
@Test func exposesSourceTagBase64BytesAndStableInstallationIdentifier() throws {
|
|
let source = BookSource(bookSourceUrl: "https://bridge-source.test", bookSourceName: "桥接源")
|
|
let evaluator = JavaScriptRuleEvaluator()
|
|
|
|
let value = try evaluator.evaluate(
|
|
script: "java.getSource().bookSourceName + ':' + java.getTag() + ':' + java.bytesToStr(java.base64DecodeToByteArray('b2s=')) + ':' + (java.androidId() === java.androidId())",
|
|
result: .null,
|
|
executionContext: .init(source: source)
|
|
)
|
|
|
|
#expect(value.value.stringValue == "桥接源:https://bridge-source.test:ok:true")
|
|
}
|
|
|
|
@Test func exposesEntityPropertiesAndFormStyleEncodeURI() throws {
|
|
var book = Book(bookUrl: "https://book.test/1", name: "书名", author: "作者")
|
|
book.kind = "玄幻"
|
|
let chapter = BookChapter(url: "/chapter/1", title: "第一章", bookUrl: book.bookUrl, index: 3)
|
|
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "book.name + ':' + book.kind + ':' + chapter.title + ':' + chapter.index + ':' + java.encodeURI('a b&中')",
|
|
result: .null,
|
|
executionContext: .init(book: book, chapter: chapter)
|
|
)
|
|
|
|
#expect(value.value.stringValue == "书名:玄幻:第一章:3:a+b%26%E4%B8%AD")
|
|
}
|
|
|
|
@Test func keepsJavaSpecialVariablesAndSourceVariablesInSeparateScopes() throws {
|
|
let source = BookSource(bookSourceUrl: "https://variable-source.test", bookSourceName: "变量源")
|
|
let book = Book(bookUrl: "https://book.test/variable", name: "变量书")
|
|
let chapter = BookChapter(url: "/one", title: "变量章", bookUrl: book.bookUrl, index: 0)
|
|
let evaluator = JavaScriptRuleEvaluator()
|
|
|
|
let first = try evaluator.evaluate(
|
|
script: "source.put('token','persisted'); java.get('bookName') + ':' + java.get('title')",
|
|
result: .null,
|
|
executionContext: .init(source: source, book: book, chapter: chapter)
|
|
)
|
|
let second = try evaluator.evaluate(
|
|
script: "source.get('token')",
|
|
result: .null,
|
|
executionContext: .init(source: source)
|
|
)
|
|
|
|
#expect(first.value.stringValue == "变量书:变量章")
|
|
#expect(second.value.stringValue == "persisted")
|
|
}
|
|
|
|
@Test func resolvesDynamicLenientAndLoginHeadersByPriority() throws {
|
|
var source = BookSource(bookSourceUrl: "https://header-source.test", bookSourceName: "Header源")
|
|
source.header = #"@js:"{'X-Static':'base','X-Dynamic':'js'}""#
|
|
BookSourceRuntimeStore.shared.put(
|
|
sourceID: source.id,
|
|
key: "loginHeader",
|
|
value: #"{"X-Static":"login","Cookie":"token=ok"}"#
|
|
)
|
|
|
|
let headers = try BookSourceHeaderResolver().resolve(source)
|
|
|
|
#expect(headers["X-Dynamic"] == "js")
|
|
#expect(headers["X-Static"] == "login")
|
|
#expect(headers["Cookie"] == "token=ok")
|
|
}
|
|
|
|
@Test func exposesSourceLoginInfoMapAndHeaderMap() throws {
|
|
var source = BookSource(bookSourceUrl: "httpTts:99", bookSourceName: "在线朗读")
|
|
source.header = #"{"X-Base":"one"}"#
|
|
BookSourceRuntimeStore.shared.put(sourceID: source.id, key: "loginHeader", value: #"{"X-Login":"two"}"#)
|
|
|
|
let value = try JavaScriptRuleEvaluator().evaluate(
|
|
script: "source.putLoginInfo(JSON.stringify({token:'abc'})); source.getLoginInfoMap().get('token') + ':' + source.getHeaderMap(true)['X-Login'] + ':' + source.getKey()",
|
|
result: .null,
|
|
executionContext: .init(source: source)
|
|
)
|
|
|
|
#expect(value.value.stringValue == "abc:two:httpTts:99")
|
|
}
|
|
|
|
@Test func rejectsObviousUnboundedScripts() async {
|
|
let result = await RuleEngine().evaluate("@js:while(true){}", in: .null)
|
|
|
|
#expect(result.values.isEmpty)
|
|
#expect(result.diagnostics.contains { $0.code == "rule.resource_limit" })
|
|
}
|
|
}
|