Files
legado-ios/Tests/LegadoCoreTests/LegadoURLBuilderTests.swift
T

180 lines
8.3 KiB
Swift

import Foundation
import Testing
@testable import LegadoCore
struct LegadoURLBuilderTests {
@Test func canDisableCookieHandlingPerSourceRequest() throws {
let prepared = try LegadoURLBuilder().prepare("https://example.test/books")
#expect(prepared.urlRequest(handleCookies: false).httpShouldHandleCookies == false)
#expect(prepared.urlRequest().httpShouldHandleCookies)
}
private let builder = LegadoURLBuilder()
@Test func relativeRequestOptionsAndHeaderCasingSurviveResolution() throws {
let base = try #require(URL(string: "https://example.test/book/1"))
let resolved = LegadoURLBuilder.resolve(#"../chapter,{"method":"POST","headers":{"referer":"custom"},"body":"id=1"}"#, against: base)
let request = try builder.prepare(resolved, defaultHeaders: ["Referer": "old"])
#expect(request.url.absoluteString == "https://example.test/chapter")
#expect(request.method == .post)
#expect(request.headers.filter { $0.key.lowercased() == "referer" }.count == 1)
#expect(request.headers["referer"] == "custom")
#expect(String(decoding: try #require(request.body), as: UTF8.self) == "id=1")
}
@Test func rendersKeywordPageAndRequestOptions() throws {
let template = """
/search?q={{key}}&page=<0,1,2>, {"method":"POST","headers":{"X-Test":"yes"},"body":"token={{token}}","retry":2}
"""
let prepared = try builder.prepare(
template,
context: .init(key: "三体", page: 2, variables: ["token": "abc"]),
baseURL: URL(string: "https://example.test/base/"),
defaultHeaders: ["User-Agent": "Legado-iOS"]
)
#expect(prepared.url.absoluteString.contains("q=%E4%B8%89%E4%BD%93"))
#expect(prepared.url.absoluteString.contains("page=1"))
#expect(prepared.method == .post)
#expect(prepared.headers["X-Test"] == "yes")
#expect(prepared.headers["User-Agent"] == "Legado-iOS")
#expect(String(decoding: try #require(prepared.body), as: UTF8.self) == "token=abc")
#expect(prepared.retry == 2)
}
@Test func evaluatesInlineJavaScriptWithSearchBindings() throws {
let prepared = try builder.prepare(
"https://example.test/{{key.toUpperCase()}}?page={{page + 1}}",
context: .init(key: "swift", page: 2)
)
#expect(prepared.url.absoluteString == "https://example.test/SWIFT?page=3")
}
@Test func evaluatesWholeURLAndOptionJavaScriptWhileCarryingVariables() throws {
let wholeScript = try builder.prepare(
"@js:'/search?q=' + key",
context: .init(key: "book", page: 4),
baseURL: URL(string: "https://example.test")
)
let prepared = try builder.prepare(
#"/search?q=book, {"js":"java.put('signature', 's-' + page); result + '&sig=' + java.get('signature')"}"#,
context: .init(key: "book", page: 4),
baseURL: URL(string: "https://example.test")
)
#expect(wholeScript.url.absoluteString == "https://example.test/search?q=book")
#expect(prepared.url.absoluteString == "https://example.test/search?q=book&sig=s-4")
#expect(prepared.variables["signature"] == "s-4")
}
@Test func supportsLegacySearchTokensAndJSONObjectBody() throws {
let prepared = try builder.prepare(
"https://example.test/search, {\"method\":\"POST\",\"body\":{\"title\":\"searchKey\",\"page\":{{searchPage}}}}",
context: .init(key: "三体", page: 3)
)
#expect(prepared.method == .post)
let body = try #require(prepared.body)
let json = try JSONValue.decode(data: body)
#expect(json["title"]?.stringValue == "三体")
#expect(json["page"]?.stringValue == "3")
}
@Test func encodesLegacyCharsetQueryAndFormBody() throws {
let query = try builder.prepare(
#"https://example.test/search?q={{key}}, {"charset":"gb18030"}"#,
context: .init(key: "旧城")
)
let form = try builder.prepare(
#"https://example.test/search, {"method":"POST","charset":"gb18030","body":"q={{key}}&tag=中文 空格"}"#,
context: .init(key: "旧城")
)
#expect(query.url.absoluteString.contains("q=%BE%C9%B3%C7"))
#expect(String(decoding: try #require(form.body), as: UTF8.self) == "q=%BE%C9%B3%C7&tag=%D6%D0%CE%C4+%BF%D5%B8%F1")
#expect(form.headers["Content-Type"] == "application/x-www-form-urlencoded; charset=gb18030")
}
@Test func parsesGsonLenientStyleOptions() throws {
let prepared = try builder.prepare(
#"https://example.test/search?q={{key}}, {'method':'POST', body:'key={{key}}', charset:'utf-8',}"#,
context: .init(key: "三体")
)
#expect(prepared.method == .post)
#expect(prepared.charset == "utf-8")
#expect(String(decoding: try #require(prepared.body), as: UTF8.self) == "key=%E4%B8%89%E4%BD%93")
}
@Test func keepsJsonInQueryWhenLaterSplitParses() throws {
let prepared = try builder.prepare(
#"https://example.test/search?data={"q":"{{key}}"}, {"method":"POST","body":"key={{key}}"}"#,
context: .init(key: "剑来")
)
#expect(prepared.method == .post)
#expect(prepared.url.absoluteString.contains("data="))
#expect(String(decoding: try #require(prepared.body), as: UTF8.self) == "key=%E5%89%91%E6%9D%A5")
}
@Test func appliesDefaultBrowserUserAgentWhenMissing() throws {
let prepared = try builder.prepare("https://example.test/books")
#expect(prepared.headers["User-Agent"] == LegadoURLBuilder.defaultUserAgent)
#expect(prepared.headers["User-Agent"]?.contains("Chrome/128.0.0.0") == true)
let explicit = try builder.prepare(
"https://example.test/books",
defaultHeaders: ["User-Agent": "Custom/1.0"]
)
#expect(explicit.headers["User-Agent"] == "Custom/1.0")
}
@Test func nullUserAgentSuppressesDefaultHeader() throws {
let prepared = try builder.prepare(
"https://example.test/books",
defaultHeaders: ["user-agent": "null"]
)
#expect(prepared.headers.keys.allSatisfy { $0.caseInsensitiveCompare("User-Agent") != .orderedSame })
#expect(prepared.urlRequest().value(forHTTPHeaderField: "User-Agent") == nil)
}
@Test func preservesTypeDnsServerAndProxyOptions() throws {
let prepared = try builder.prepare(
#"https://example.test/file, {"type":"image","dnsIp":"1.1.1.1,8.8.8.8","serverID":12}"#,
defaultHeaders: ["proxy": "http://127.0.0.1:7890", "X-Test": "yes"]
)
#expect(prepared.responseType == "image")
#expect(prepared.dnsIP == "1.1.1.1,8.8.8.8")
#expect(prepared.serverID == 12)
#expect(prepared.proxy == "http://127.0.0.1:7890")
#expect(prepared.headers["proxy"] == nil)
#expect(prepared.headers["X-Test"] == "yes")
}
@Test func rendersPostFormAndIgnoresBodyForGetAndHead() throws {
let post = try builder.prepare(
#"https://example.test/search, {"method":"POST","body":"q={{key}}&space=空 格"}"#,
context: .init(key: "三体")
)
let emptyPost = try builder.prepare(#"https://example.test/search, {"method":"POST"}"#)
let get = try builder.prepare(#"https://example.test/search, {"body":"ignored=1"}"#)
let head = try builder.prepare(#"https://example.test/search, {"method":"HEAD","body":"ignored=1"}"#)
#expect(String(decoding: try #require(post.body), as: UTF8.self) == "q=%E4%B8%89%E4%BD%93&space=%E7%A9%BA+%E6%A0%BC")
#expect(emptyPost.body == Data())
#expect(post.headers["Content-Type"] == "application/x-www-form-urlencoded; charset=utf-8")
#expect(get.urlRequest().httpBody == nil)
#expect(head.urlRequest().httpBody == nil)
}
@Test func explicitContentTypeKeepsRawPostBody() throws {
let prepared = try builder.prepare(
#"https://example.test/raw, {"method":"POST","headers":{"content-type":"text/plain; charset=utf-8"},"body":"原始 空格"}"#
)
#expect(String(decoding: try #require(prepared.body), as: UTF8.self) == "原始 空格")
#expect(prepared.headers["content-type"] == "text/plain; charset=utf-8")
}
}