mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-14 10:13:54 +00:00
Implements issue #71 with ADR-0020 (supersedes ADR-0019's endpoint-signature cleanup, startup cleanup, and health-gate cleanup). - New ShadowsocksX-NG2SystemProxyHelper target: app-bundled root LaunchDaemon (SMAppService.daemon, on-demand via MachServices) that mechanically applies a closed typed configuration or executes an unconditional clear. Clients are accepted by code-signing bundle identifier only; requests run serially on a shared queue (last-writer-wins). - SystemProxyConfiguration is now the closed typed apply payload (Codable over JSON XPC): explicit SOCKS/HTTP/HTTPS enable + endpoints, PAC and auto-discovery disabled with URL/JS removed, explicit simple-hostname exclusion, complete ExceptionsList. Unmodeled Proxies keys are preserved and equivalent values skip the commit. - Cleanup is a side effect of a persisted system-proxy intent on-to-off transition only: the switch itself, or the agent-off cascade (persist both intents off, clear, then stop listeners; failed cascade persistence records no transition and clears nothing). Launch with the switch off, health-gate closure, invalid targets, and passive events never clear. Failed clears are surfaced without deferred retry. - Endpoint-signature store, ownership migration, and direct Authorization Services writes are removed; old signature files stay inert. Helper unavailability or pending approval keeps an enabled intent pending and surfaces the Login Items approval path (HomeView button + menu label).
133 lines
4.7 KiB
Swift
133 lines
4.7 KiB
Swift
import XCTest
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
/// helper XPC 契约测试(issue #71):typed payload 往返、引擎应答编码与
|
||
/// 串行 last-writer 语义,全部用确定性替身,不触真实 XPC 与 SystemConfiguration。
|
||
final class SystemProxyHelperContractTests: XCTestCase {
|
||
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"])
|
||
|
||
/// 并发闭包与断言之间的线程安全值交接。
|
||
private final class LockedArray<Element>: @unchecked Sendable {
|
||
private let lock = NSLock()
|
||
private var items: [Element] = []
|
||
|
||
func append(_ item: Element) {
|
||
lock.lock()
|
||
items.append(item)
|
||
lock.unlock()
|
||
}
|
||
|
||
var all: [Element] {
|
||
lock.lock()
|
||
defer { lock.unlock() }
|
||
return items
|
||
}
|
||
}
|
||
|
||
func testConfigurationPayloadRoundTripsThroughJSON() throws {
|
||
let payload = try SystemProxyHelperWire.encodeConfiguration(configuration)
|
||
let decoded = try SystemProxyHelperWire.decodeConfiguration(payload)
|
||
XCTAssertEqual(decoded, configuration)
|
||
}
|
||
|
||
func testEngineAnswersApplyWithTypedOutcome() throws {
|
||
let requested = try SystemProxyHelperWire.encodeConfiguration(configuration)
|
||
let observed = LockedArray<SystemProxyHelperEngine.Request>()
|
||
let response = SystemProxyHelperEngine.response(
|
||
request: .success(.apply(configuration)),
|
||
perform: { request in
|
||
observed.append(request)
|
||
return .applied(.written)
|
||
})
|
||
|
||
XCTAssertEqual(observed.all, [.apply(configuration)], "helper 收到 closed typed 配置")
|
||
XCTAssertEqual(response, .applied(.written))
|
||
XCTAssertEqual(
|
||
try SystemProxyHelperWire.decodeConfiguration(requested), configuration,
|
||
"payload 与配置一致")
|
||
}
|
||
|
||
func testEngineAnswersClearWithTypedOutcome() {
|
||
let response = SystemProxyHelperEngine.response(
|
||
request: .success(.clear), perform: { _ in .cleared })
|
||
XCTAssertEqual(response, .cleared)
|
||
}
|
||
|
||
func testEngineEncodesTypedFailuresAndInvalidPayloads() throws {
|
||
let failure = SystemProxyHelperEngine.response(
|
||
request: .failure(SystemProxyError.commitFailed("busy")),
|
||
perform: { _ in .cleared })
|
||
XCTAssertEqual(failure, .failure(.commitFailed("busy")))
|
||
|
||
let genericFailure = SystemProxyHelperEngine.response(
|
||
request: .failure(ProxySettingsStoreError.corrupt(detail: "bad")),
|
||
perform: { _ in .cleared })
|
||
XCTAssertEqual(
|
||
genericFailure, .failure(.helperUnavailable("corrupt(detail: \"bad\")")),
|
||
"非 SystemProxyError 归并为 helperUnavailable")
|
||
|
||
XCTAssertThrowsError(
|
||
try SystemProxyHelperWire.decodeConfiguration(Data("not json".utf8))
|
||
) { error in
|
||
guard case SystemProxyError.invalidRequest = error else {
|
||
return XCTFail("无效 payload 应为 invalidRequest,实际 \(error)")
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 串行 last-writer(issue #71 AC34):注入的假写入器在共享串行队列上按
|
||
/// 接受顺序执行,最后接受的请求决定最终结果。
|
||
func testEngineProcessesRequestsSeriallyWithLastWriterWins() async throws {
|
||
let recorder = LockedArray<String>()
|
||
let queue = DispatchQueue(label: "test.system-proxy-helper.serial")
|
||
let engine = SystemProxyHelperEngine(
|
||
perform: { request in
|
||
switch request {
|
||
case .apply(let applied):
|
||
recorder.append("apply:\(applied.socks.port)")
|
||
case .clear:
|
||
recorder.append("clear")
|
||
}
|
||
return .applied(.written)
|
||
},
|
||
queue: queue)
|
||
|
||
let first = try SystemProxyHelperWire.encodeConfiguration(configuration)
|
||
let second = try SystemProxyHelperWire.encodeConfiguration(
|
||
SystemProxyConfiguration(
|
||
socks: .init(host: "127.0.0.1", port: 2086),
|
||
http: .init(host: "127.0.0.1", port: 2087),
|
||
https: .init(host: "127.0.0.1", port: 2087),
|
||
exceptions: ["localhost"]))
|
||
|
||
let replies = LockedArray<Data>()
|
||
let replyExpectation = expectation(description: "replies")
|
||
replyExpectation.expectedFulfillmentCount = 3
|
||
engine.apply(first) {
|
||
replies.append($0)
|
||
replyExpectation.fulfill()
|
||
}
|
||
engine.clear {
|
||
replies.append($0)
|
||
replyExpectation.fulfill()
|
||
}
|
||
engine.apply(second) {
|
||
replies.append($0)
|
||
replyExpectation.fulfill()
|
||
}
|
||
await fulfillment(of: [replyExpectation], timeout: 5)
|
||
|
||
XCTAssertEqual(recorder.all, ["apply:1086", "clear", "apply:2086"], "逐个串行执行")
|
||
XCTAssertEqual(replies.all.count, 3)
|
||
for reply in replies.all {
|
||
XCTAssertEqual(
|
||
try SystemProxyHelperWire.decode(reply), .applied(.written), "每个请求都有 typed 应答")
|
||
}
|
||
}
|
||
}
|