Files

255 lines
12 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
@testable import ShadowsocksX_NG2
/// System proxy mode mapping and declarative planning are pure/testable seams;
/// no test in this file writes the host's real SystemConfiguration state.
/// Closed typed 配置覆盖 HTTPS/PAC/自动发现/简单主机名与例外列表,
/// 无条件清理计划与 helper XPC 契约(含序列化 last-writer)都在此验证。
final class SystemProxyTests: XCTestCase {
func testSupportedModesProjectTheSameLocalEndpoints() {
let document = ProxyRuntimeFixture.makeDocument(
listenerMode: .allIPv4Interfaces, localPort: 2086)
let expected = SystemProxyConfiguration(
socks: .init(host: "127.0.0.1", port: 2086),
http: .init(host: "127.0.0.1", port: ProxyListeningConfiguration.defaultHTTPPort),
https: .init(host: "127.0.0.1", port: ProxyListeningConfiguration.defaultHTTPPort),
exceptions: FixedLocalProxyRanges.systemProxyExceptions)
XCTAssertEqual(
ProxyMode.rule.systemProxyConfiguration(for: document),
expected)
XCTAssertEqual(
ProxyMode.global.systemProxyConfiguration(for: document),
expected,
"全局模式的系统例外使用固定本地范围,与 ACL 安全策略一致")
}
func testIPv6LANModeKeepsSystemProxyOnLocalIPv4Loopback() {
let document = SslocalRuntimeDocument(
servers: [], listen: ProxyListeningConfiguration(listenerMode: .allIPv6Interfaces))
let configuration = ProxyMode.rule.systemProxyConfiguration(for: document)
XCTAssertEqual(configuration.socks.host, "127.0.0.1")
XCTAssertEqual(configuration.http.host, "127.0.0.1")
XCTAssertEqual(configuration.https.host, "127.0.0.1", "HTTPS 显式赋值,与 HTTP 同端点")
}
func testMissingHTTPInboundIsRejectedInsteadOfPointingAtAWildcardPort() throws {
var document = SslocalRuntimeDocument(servers: [], listen: ProxyListeningConfiguration())
document.locals.removeAll { $0.inboundProtocol == "http" }
XCTAssertFalse(document.isWellFormed, "每组必须同时提供 SOCKS 与 HTTP")
}
func testModeProjectionCarriesClosedTypedPolicyDefaults() {
let document = ProxyRuntimeFixture.makeDocument(localPort: 2086)
let configuration = ProxyMode.rule.systemProxyConfiguration(
for: document, exceptions: ["example.com"])
XCTAssertTrue(configuration.pacEnabled == false, "PAC 显式关闭")
XCTAssertTrue(configuration.autoDiscoveryEnabled == false, "自动发现显式关闭")
XCTAssertTrue(configuration.excludeSimpleHostnames, "简单主机名排除由 GUI 拥有,默认开")
XCTAssertEqual(
configuration.exceptions,
FixedLocalProxyRanges.systemProxyExceptions(including: ["example.com"]),
"例外列表 = 固定本地范围 + 用户补充项,全量给出")
}
// MARK: - Property-list projection
private let configuration = SystemProxyConfiguration(
socks: .init(host: "127.0.0.1", port: 1086),
http: .init(host: "127.0.0.1", port: 1087),
https: .init(host: "127.0.0.1", port: 1087),
exceptions: ["localhost", "127.0.0.1"])
func testSystemConfigurationProjectionEnablesSOCKSHTTPAndHTTPS() {
let original: [String: Any] = [
SystemProxyPropertyList.httpEnabled: 1,
SystemProxyPropertyList.httpsEnabled: 1,
SystemProxyPropertyList.socksEnabled: 1,
SystemProxyPropertyList.pacEnabled: 1,
SystemProxyPropertyList.pacURL: "http://127.0.0.1:1089/v1/proxy.pac",
"ExceptionsList": ["stale.example"],
]
let applied = SystemProxyPropertyList.applying(configuration, to: original)
XCTAssertEqual(applied[SystemProxyPropertyList.pacEnabled] as? Int, 0)
XCTAssertNil(applied[SystemProxyPropertyList.pacURL])
XCTAssertNil(applied[SystemProxyPropertyList.pacJavaScript])
XCTAssertEqual(applied[SystemProxyPropertyList.socksEnabled] as? Int, 1)
XCTAssertEqual(applied[SystemProxyPropertyList.socksProxy] as? String, "127.0.0.1")
XCTAssertEqual(applied[SystemProxyPropertyList.socksPort] as? Int, 1086)
XCTAssertEqual(applied[SystemProxyPropertyList.httpEnabled] as? Int, 1)
XCTAssertEqual(applied[SystemProxyPropertyList.httpProxy] as? String, "127.0.0.1")
XCTAssertEqual(applied[SystemProxyPropertyList.httpPort] as? Int, 1087)
XCTAssertEqual(applied[SystemProxyPropertyList.httpsEnabled] as? Int, 1)
XCTAssertEqual(applied[SystemProxyPropertyList.httpsProxy] as? String, "127.0.0.1")
XCTAssertEqual(applied[SystemProxyPropertyList.httpsPort] as? Int, 1087)
XCTAssertEqual(applied[SystemProxyPropertyList.excludeSimpleHostnames] as? Int, 1)
XCTAssertEqual(applied[SystemProxyPropertyList.autoDiscoveryEnabled] as? Int, 0)
XCTAssertEqual(
applied[SystemProxyPropertyList.exceptionsList] as? [String],
["localhost", "127.0.0.1"],
"例外列表全量覆盖,不保留旧值")
}
/// 显式简单主机名排除值:GUI 拥有该策略(当前恒开),helper 只翻译。
func testSimpleHostnameExclusionFollowsExplicitFlag() {
let withFlagOn = SystemProxyPropertyList.applying(configuration, to: [:])
XCTAssertEqual(withFlagOn[SystemProxyPropertyList.excludeSimpleHostnames] as? Int, 1)
var flagOff = configuration
flagOff.excludeSimpleHostnames = false
let withFlagOff = SystemProxyPropertyList.applying(
flagOff, to: [SystemProxyPropertyList.excludeSimpleHostnames: 1])
XCTAssertEqual(withFlagOff[SystemProxyPropertyList.excludeSimpleHostnames] as? Int, 0)
}
/// 空例外列表也是显式值:写入空数组而非保留旧列表。
func testEmptyExceptionListIsWrittenExplicitly() {
var withoutExceptions = configuration
withoutExceptions.exceptions = []
let applied = SystemProxyPropertyList.applying(
withoutExceptions, to: ["ExceptionsList": ["stale.example"]])
XCTAssertEqual(applied[SystemProxyPropertyList.exceptionsList] as? [String], [])
}
/// 未建模键保留:typed 字段集之外的 Proxies 键不被擦除。
func testUnmodeledKeysArePreservedOnApply() {
let original: [String: Any] = [
"ThirdPartyKey": "third-party value",
SystemProxyPropertyList.socksEnabled: 0,
]
let applied = SystemProxyPropertyList.applying(configuration, to: original)
XCTAssertEqual(applied["ThirdPartyKey"] as? String, "third-party value")
}
/// PAC 显式关闭时移除 URL 与 JavaScript 残值。
func testDisabledPACRemovesURLAndJavaScriptValues() {
let original: [String: Any] = [
SystemProxyPropertyList.pacEnabled: 1,
SystemProxyPropertyList.pacURL: "http://proxy.example/proxy.pac",
SystemProxyPropertyList.pacJavaScript: "function FindProxyForURL() {}",
]
let applied = SystemProxyPropertyList.applying(configuration, to: original)
XCTAssertEqual(applied[SystemProxyPropertyList.pacEnabled] as? Int, 0)
XCTAssertNil(applied[SystemProxyPropertyList.pacURL])
XCTAssertNil(applied[SystemProxyPropertyList.pacJavaScript])
}
// MARK: - Declarative planner
private func serialized(_ dictionary: [String: Any]) throws -> Data {
try PropertyListSerialization.data(fromPropertyList: dictionary, format: .binary, options: 0)
}
/// 字典等价:断言「写出什么」,不与生产的语义归一(semanticallyNormalized)混用。
private func plistsEqual(_ lhs: Data?, _ rhs: Data?) -> Bool {
guard let lhs, let rhs else { return lhs == nil && rhs == nil }
guard
let left = try? PropertyListSerialization.propertyList(from: lhs, options: [], format: nil)
as? NSDictionary,
let right = try? PropertyListSerialization.propertyList(from: rhs, options: [], format: nil)
as? NSDictionary
else { return lhs == rhs }
return left.isEqual(right)
}
private func service(
_ id: String, location: String = "location-1", configuration data: Data?
) -> SystemProxyServiceState {
SystemProxyServiceState(
identifier: SystemProxyServiceIdentifier(locationID: location, serviceID: id),
configuration: data)
}
func testApplyPlannerWritesEveryActiveServiceToDesiredValues() throws {
let applied = try serialized(SystemProxyPropertyList.applying(configuration, to: [:]))
let foreign = try serialized(["HTTPEnable": 1, "HTTPProxy": "proxy.example"])
let plan = try SystemProxyPlanner.makeApplyPlan(
services: [
service("service-1", configuration: applied),
service("service-2", configuration: foreign),
], configuration: configuration)
XCTAssertEqual(plan.writes.map(\.identifier.serviceID), ["service-2"], "等值服务零写入")
XCTAssertEqual(plan.outcome, .written)
let projected = try XCTUnwrap(plan.writes.first?.configuration)
XCTAssertTrue(plistsEqual(projected, applied))
}
func testEquivalentDisabledFlagsAndExceptionOrderProduceNoDifference() throws {
var values = SystemProxyPropertyList.applying(configuration, to: ["Unmanaged": "keep"])
values.removeValue(forKey: "ProxyAutoConfigEnable")
values.removeValue(forKey: "ProxyAutoDiscoveryEnable")
values["ExceptionsList"] = ["127.0.0.1", "localhost", "localhost"]
let plan = try SystemProxyPlanner.makeApplyPlan(
services: [service("wifi", configuration: serialized(values))], configuration: configuration)
XCTAssertTrue(plan.writes.isEmpty, "系统语义等价,不报告差异也不写入")
}
func testApplyPlannerIsIdempotentWhenAllServicesMatch() throws {
let applied = try serialized(SystemProxyPropertyList.applying(configuration, to: [:]))
let plan = try SystemProxyPlanner.makeApplyPlan(
services: [service("service-1", configuration: applied)], configuration: configuration)
XCTAssertTrue(plan.writes.isEmpty)
XCTAssertEqual(plan.outcome, .unchanged, "重试不产生无谓的 SC 写入(issue #71 AC31)")
}
/// 清理计划无条件:凡持有 Proxies 实体的服务(跨位置、不校验端点或来源)
/// 都整字典移除。
func testCleanupPlannerClearsWholeDictionaryEverywhereUnconditionally() throws {
let ng2Values = try serialized(SystemProxyPropertyList.applying(configuration, to: [:]))
let foreignValues = try serialized([
"HTTPEnable": 1, "HTTPProxy": "proxy.example", "HTTPPort": 9090,
])
let services = [
service("same-id", location: "location-a", configuration: ng2Values),
service("same-id", location: "location-b", configuration: foreignValues),
service("bare-service", location: "location-b", configuration: nil),
]
let plan = SystemProxyPlanner.makeClearPlan(services: services)
XCTAssertEqual(
Set(plan.writes.map(\.identifier)),
[services[0].identifier, services[1].identifier],
"不校验来源:NG2 与其他应用写入的服务都被清空")
XCTAssertTrue(plan.writes.allSatisfy { $0.configuration == nil }, "清除完整 Proxies 字典")
XCTAssertFalse(
plan.writes.contains { $0.identifier == services[2].identifier },
"没有 Proxies 实体的服务无需写入")
}
// MARK: - 注册清单漂移指纹
/// launchd 沿用注册提交时的 job 定义快照,清单变更须注销重注才生效;
/// 指纹检测决定是否重注。
func testRegistrationStampDriftDetection() {
let data = Data("plist-content".utf8)
XCTAssertTrue(
SystemProxyHelperRegistrationStamp.drifted(plistData: data, storedStamp: nil),
"无指纹(首次带检测代码启动)视为漂移")
XCTAssertTrue(
SystemProxyHelperRegistrationStamp.drifted(plistData: data, storedStamp: "outdated"),
"清单内容变更视为漂移")
XCTAssertFalse(
SystemProxyHelperRegistrationStamp.drifted(
plistData: data, storedStamp: SystemProxyHelperRegistrationStamp.stamp(data)),
"指纹一致不漂移")
XCTAssertFalse(
SystemProxyHelperRegistrationStamp.drifted(plistData: nil, storedStamp: nil),
"清单不可读无基准,不判漂移")
XCTAssertNotEqual(
SystemProxyHelperRegistrationStamp.stamp(Data("a".utf8)),
SystemProxyHelperRegistrationStamp.stamp(Data("b".utf8)),
"不同清单内容指纹不同")
}
}