mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 11:14:51 +00:00
139 lines
6.3 KiB
Swift
139 lines
6.3 KiB
Swift
import Foundation
|
|
import Testing
|
|
import XCTest
|
|
|
|
@testable import ShadowsocksX_NG2
|
|
|
|
/// 规则领域模型:动作与匹配条件独立于
|
|
/// PAC、ACL 文本和系统代理设置。
|
|
final class ProxyRuleTests: XCTestCase {
|
|
// MARK: - 动作与匹配
|
|
|
|
func testActionsAreProxyAndDirect() throws {
|
|
XCTAssertEqual(RuleAction.proxy.rawValue, "proxy")
|
|
XCTAssertEqual(RuleAction.direct.rawValue, "direct")
|
|
}
|
|
|
|
func testMatchKindsCoverDomainAndCIDR() throws {
|
|
let exact = try RuleMatch(domainExact: "api.example.com")
|
|
let suffix = try RuleMatch(domainSuffix: "example.com")
|
|
// swiftlint:disable:next identifier_name
|
|
let v4 = try RuleMatch(ipv4CIDR: "203.0.113.0/24")
|
|
// swiftlint:disable:next identifier_name
|
|
let v6 = try RuleMatch(ipv6CIDR: "2001:db8::/32")
|
|
|
|
XCTAssertEqual(exact.pattern, .domainExact("api.example.com"))
|
|
XCTAssertEqual(suffix.pattern, .domainSuffix("example.com"))
|
|
XCTAssertEqual(v4.pattern, .ipv4CIDR("203.0.113.0/24"))
|
|
XCTAssertEqual(v6.pattern, .ipv6CIDR("2001:db8::/32"))
|
|
}
|
|
|
|
func testDomainExactRejectsSuffixSemantics() throws {
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: "example.com."))
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: ".example.com"))
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: "*.example.com"))
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: ""))
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: "exa mple.com"))
|
|
XCTAssertThrowsError(try RuleMatch(domainExact: "例子.com"))
|
|
}
|
|
|
|
func testDomainSuffixNormalizesAndRejectsInvalid() throws {
|
|
XCTAssertEqual(try RuleMatch(domainSuffix: "Example.COM").pattern, .domainSuffix("example.com"))
|
|
XCTAssertEqual(
|
|
try RuleMatch(domainSuffix: ".example.com").pattern, .domainSuffix("example.com"))
|
|
XCTAssertThrowsError(try RuleMatch(domainSuffix: "example.com."))
|
|
XCTAssertThrowsError(try RuleMatch(domainSuffix: "*.example.com"))
|
|
XCTAssertThrowsError(try RuleMatch(domainSuffix: ""))
|
|
XCTAssertThrowsError(try RuleMatch(domainSuffix: "localhost"))
|
|
XCTAssertThrowsError(try RuleMatch(domainSuffix: "com"), "单标签后缀不允许,避免过宽匹配")
|
|
}
|
|
|
|
func testIPv4CIDRNormalizesHostAddressesAndRejectsInvalid() throws {
|
|
XCTAssertEqual(try RuleMatch(ipv4CIDR: "203.0.113.7").pattern, .ipv4CIDR("203.0.113.7/32"))
|
|
XCTAssertEqual(try RuleMatch(ipv4CIDR: "203.0.113.0/24").pattern, .ipv4CIDR("203.0.113.0/24"))
|
|
// 主机位非零的前缀按网络地址规范化。
|
|
XCTAssertEqual(try RuleMatch(ipv4CIDR: "203.0.113.15/24").pattern, .ipv4CIDR("203.0.113.0/24"))
|
|
XCTAssertThrowsError(try RuleMatch(ipv4CIDR: "203.0.113.0/33"))
|
|
XCTAssertThrowsError(try RuleMatch(ipv4CIDR: "203.0.113.256/24"))
|
|
XCTAssertThrowsError(try RuleMatch(ipv4CIDR: "2001:db8::/32"), "IPv6 不得进入 IPv4 匹配")
|
|
XCTAssertThrowsError(try RuleMatch(ipv4CIDR: "not-an-ip"))
|
|
}
|
|
|
|
func testIPv6CIDRNormalizesAndRejectsInvalid() throws {
|
|
XCTAssertEqual(try RuleMatch(ipv6CIDR: "2001:db8::/32").pattern, .ipv6CIDR("2001:db8::/32"))
|
|
XCTAssertEqual(
|
|
try RuleMatch(ipv6CIDR: "2001:DB8:0:0:0:0:0:0/32").pattern, .ipv6CIDR("2001:db8::/32"))
|
|
XCTAssertThrowsError(try RuleMatch(ipv6CIDR: "2001:db8::/129"))
|
|
XCTAssertThrowsError(try RuleMatch(ipv6CIDR: "203.0.113.0/24"), "IPv4 不得进入 IPv6 匹配")
|
|
XCTAssertThrowsError(try RuleMatch(ipv6CIDR: "gggg::/32"))
|
|
}
|
|
|
|
// MARK: - 规则序列化
|
|
|
|
func testCustomRuleStorageKeepsFlatActionAndMatch() throws {
|
|
let rule = ProxyRule(action: .direct, match: try RuleMatch(domainSuffix: "example.com"))
|
|
let document = CustomRuleDocument(rules: [CustomRule(action: rule.action, match: rule.match)])
|
|
let data = try CustomRuleStore.encode(document)
|
|
let json = try XCTUnwrap(JSONSerialization.jsonObject(with: data) as? [String: Any])
|
|
let entry = try XCTUnwrap((json["rules"] as? [[String: Any]])?.first)
|
|
XCTAssertEqual(Set(entry.keys), ["id", "action", "match"])
|
|
XCTAssertEqual(try CustomRuleStore.decode(data).rules.first?.rule, rule)
|
|
}
|
|
}
|
|
|
|
struct NormalizedRuleValueTests {
|
|
private struct HistoricalCase {
|
|
let match: RuleMatch
|
|
let token: String
|
|
let acl: String
|
|
}
|
|
|
|
@Test func equivalentInputsShareIdentityTokensAndACL() throws {
|
|
let first = ProxyRule(action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.15/24"))
|
|
let second = ProxyRule(action: .direct, match: try RuleMatch(ipv4CIDR: "203.0.113.0/24"))
|
|
#expect(first == second)
|
|
#expect(Set([first, second]).count == 1)
|
|
#expect(first.contentToken == "direct|ipv4CIDR(\"203.0.113.0/24\")")
|
|
#expect(first.aclLine == "203.0.113.0/24")
|
|
#expect(first.match.pattern == .ipv4CIDR("203.0.113.0/24"))
|
|
let mapped = ProxyRule(
|
|
action: .direct, match: try RuleMatch(ipv6CIDR: "::ffff:203.0.113.0/120"))
|
|
#expect(first != mapped)
|
|
}
|
|
|
|
@Test func historicalTokensAndACLRenderingSurviveRepresentationChange() throws {
|
|
let cases = [
|
|
HistoricalCase(
|
|
match: try RuleMatch(domainExact: " API.Example.COM "),
|
|
token: "proxy|domainExact(\"api.example.com\")", acl: "|api.example.com"),
|
|
HistoricalCase(
|
|
match: try RuleMatch(domainSuffix: ".Example.COM"),
|
|
token: "proxy|domainSuffix(\"example.com\")", acl: "||example.com"),
|
|
HistoricalCase(
|
|
match: try RuleMatch(ipv6CIDR: "2001:DB8::8/32"),
|
|
token: "proxy|ipv6CIDR(\"2001:db8::/32\")", acl: "2001:db8::/32"),
|
|
]
|
|
for item in cases {
|
|
let rule = ProxyRule(action: .proxy, match: item.match)
|
|
#expect(rule.contentToken == item.token)
|
|
#expect(rule.aclLine == item.acl)
|
|
}
|
|
}
|
|
|
|
@Test func fixedSuffixesRoundTripThroughSnapshotButRemainUnavailableToCustomRules() throws {
|
|
let fixed = [RuleMatch.localSuffix, .localhostSuffix]
|
|
let rules = fixed.map { ProxyRule(action: .direct, match: $0) }
|
|
let snapshot = rulesFixture(.geolocationCN, rules: rules)
|
|
let decoded = try RuleSnapshotStore.decode(RuleSnapshotStore.encode(snapshot))
|
|
#expect(decoded == snapshot)
|
|
for match in fixed {
|
|
let custom = CustomRule(action: .direct, match: match)
|
|
let data = try CustomRuleStore.encode(CustomRuleDocument(rules: [custom]))
|
|
#expect(throws: (any Error).self) { try CustomRuleStore.decode(data) }
|
|
}
|
|
for value in ["local", "localhost", "com"] {
|
|
#expect(throws: RuleMatchError.self) { try RuleMatch(nationalDomainSuffix: value) }
|
|
}
|
|
}
|
|
}
|