Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/ProxyControl/ProxyCommandAddressWorkflowTests.swift

257 lines
10 KiB
Swift

import Testing
import XCTest
@testable import ShadowsocksX_NG2
@MainActor
struct LANCommandEnablementTests {
@Test(arguments: [false, true])
func copyRejectsLostIPIncludingAButtonRenderedBeforeNetworkNotification(
notificationDelivered: Bool
) {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
runtime.listenFacts.lan.isEnabled = true
interfaces.interfaces = [FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.0.2.2")])]
let workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaces)
let address = workflow.snapshot.commandAddressPicker.candidates[1]
workflow.selectCommandAddress(address)
interfaces.interfaces = []
if notificationDelivered { interfaces.emitChange() }
#expect(throws: TerminalCommandFailure.addressUnavailable) {
try workflow.prepareTerminalCommandsForCopy(expectedAddress: address.identity)
}
#expect(workflow.snapshot.commandAddressPicker.selected.address == "127.0.0.1")
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
#expect(runtime.agentCommands.isEmpty)
#expect(runtime.systemProxyCommands.isEmpty)
}
@Test func windowsFormatsRequireEnabledLANAndSelectedNetworkIPWithoutRequiringRunningProxy() {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
let workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaces)
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
runtime.listenFacts.lan.isEnabled = true
interfaces.interfaces = [FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.0.2.2")])]
runtime.emitChange()
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
workflow.selectCommandAddress(workflow.snapshot.commandAddressPicker.candidates[1])
runtime.runtimeFacts = ProxyRuntimeFacts(status: .off, isOn: false)
runtime.emitChange()
#expect(
workflow.snapshot.terminalCommandShells == [
.zshBash, .fish, .nushell, .windowsCMD, .windowsPowerShell,
])
runtime.listenFacts.lan.isEnabled = false
runtime.emitChange()
#expect(workflow.snapshot.terminalCommandShells == [.zshBash, .fish, .nushell])
#expect(runtime.agentCommands.isEmpty)
#expect(runtime.systemProxyCommands.isEmpty)
}
@Test func disablingLANRetainsIPv6ModeAndFallsBackFromSelectedNetworkAddress() {
let runtime = FakeProxyRuntime()
let interfaces = FakeLocalInterfaceFacts()
let workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaces)
runtime.listenFacts.lan.isEnabled = true
runtime.listenFacts.lan.mode = .allIPv6Interfaces
interfaces.interfaces = [
FakeLocalInterfaceFacts.wifi(addresses: [.ipv6("2001:db8::1")])
]
runtime.emitChange()
let address = workflow.snapshot.commandAddressPicker.candidates[1]
_ = workflow.selectCommandAddress(address)
#expect(workflow.snapshot.commandAddressPicker.selected.address == "2001:db8::1")
runtime.listenFacts.lan.isEnabled = false
runtime.emitChange()
#expect(runtime.listenFacts.lan.mode == .allIPv6Interfaces)
let picker = workflow.snapshot.commandAddressPicker
#expect(!picker.isVisible)
#expect(picker.candidates.map(\.address) == ["127.0.0.1"])
#expect(picker.selected.address == "127.0.0.1")
#expect(
workflow.snapshot.terminalProxyEnvironmentCommands.zshBash.contains("http://127.0.0.1:11087"))
}
}
/// 首页命令地址选择器 contract。候选派生:启用状态与三种监听方式的地址族
/// 过滤与默认值、严格接口类型过滤、名称回退、排序合并与网络变化刷新。选择
/// 生命周期与命令生成见 `ProxyCommandAddressLifecycleTests`。复用
/// ProxyControlWorkflowTests 的可编程替身。
@MainActor
final class ProxyCommandAddressWorkflowTests: XCTestCase {
private var runtime: FakeProxyRuntime!
private var interfaceFacts: FakeLocalInterfaceFacts!
private var workflow: ProxyControlWorkflow!
override func setUp() async throws {
try await super.setUp()
runtime = FakeProxyRuntime()
interfaceFacts = FakeLocalInterfaceFacts()
workflow = ProxyControlWorkflow(
runtime: runtime, targetFacts: FakeTargetFacts(), interfaceFacts: interfaceFacts)
}
/// 把已保存监听事实切到指定方式并触发重观察。
private func setListenerMode(_ mode: ListenerMode) {
runtime.listenFacts = RuntimeListenFacts(
listenerMode: .disabled, socksPort: 11086, httpPort: 11087)
runtime.listenFacts.lan.isEnabled = mode != .disabled
if let lanMode = LANListenerMode(rawValue: mode.rawValue) {
runtime.listenFacts.lan.mode = lanMode
}
runtime.emitChange()
}
func testLocalhostModeHidesPickerWithLoopbackOnlyCandidates() {
setListenerMode(.disabled)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.168.1.10")]),
FakeLocalInterfaceFacts.loopback(),
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertFalse(picker.isVisible, "仅本机监听方式隐藏下拉框")
XCTAssertEqual(
picker.candidates,
[TerminalCommandAddress(bsdName: "lo0", address: "127.0.0.1", displayName: "lo0")],
"仅本机方式候选只有 IPv4 回环")
XCTAssertEqual(picker.selected.address, "127.0.0.1")
}
func testIPv4ModeFiltersFamiliesAndAllowedInterfaceTypes() {
setListenerMode(.allIPv4Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(
addresses: [.ipv4("192.168.1.10"), .ipv6("2001:db8::1"), .ipv6("fe80::1", linkLocal: true)]
),
FakeLocalInterfaceFacts.ethernet(addresses: [.ipv4("10.0.0.5")]),
FakeLocalInterfaceFacts.vpn(),
FakeLocalInterfaceFacts.virtual(),
FakeLocalInterfaceFacts.loopback(),
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertTrue(picker.isVisible)
XCTAssertEqual(
picker.candidates.map(\.address),
["127.0.0.1", "10.0.0.5", "192.168.1.10"],
"回环置顶,其余按接口名称排序;IPv6、VPN、其他类型与回环事实不进候选")
XCTAssertEqual(picker.selected.address, "127.0.0.1", "默认兼容回环地址")
}
func testDualStackModeIncludesBothLoopbacksAndBothFamilies() {
setListenerMode(.allIPv4AndIPv6Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(
addresses: [
.ipv6("2001:db8::1"), .ipv4("192.168.1.10"), .ipv4("169.254.9.9"),
.ipv6("fe80::1", linkLocal: true),
])
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertEqual(
picker.candidates.map(\.address),
["127.0.0.1", "169.254.9.9", "192.168.1.10", "2001:db8::1"],
"双栈提供两个回环(127.0.0.1 在前)、允许 IPv4 链路本地、排除 IPv6 链路本地,同接口 IPv4 先于 IPv6")
XCTAssertEqual(picker.selected.address, "127.0.0.1")
}
func testTemporaryIPv6ExcludedAndAnnotationsKeptAsSeparateFacts() {
setListenerMode(.allIPv4AndIPv6Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(
addresses: [
.ipv6("240e::8150", flags: [.autoconf, .secured]),
.ipv6("240e::855", flags: [.autoconf, .temporary]),
.ipv6("240e::17", flags: [.dynamic]),
])
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertEqual(
picker.candidates.map(\.address),
["127.0.0.1", "240e::17", "240e::8150"],
"RFC 4941 隐私临时地址被过滤(会轮换);其余按地址稳定排序")
let secured = picker.candidates.first { $0.address == "240e::8150" }
XCTAssertEqual(secured?.displayName, "Wi-Fi")
XCTAssertEqual(secured?.annotation, "autoconf secured", "注记作为独立呈现事实")
XCTAssertEqual(
picker.candidates.first { $0.address == "240e::17" }?.annotation,
"dynamic", "DHCPv6 地址注记 dynamic")
}
func testIPv6OnlyModeDefaultsToV1AndFiltersFamilies() {
setListenerMode(.allIPv6Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(
addresses: [.ipv4("192.168.1.10"), .ipv6("2001:db8::1")])
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertEqual(
picker.candidates.map(\.address), ["127.0.0.1", "2001:db8::1"],
"仅 IPv6 方式默认 ::1 且候选只有 IPv6")
XCTAssertEqual(picker.selected.address, "127.0.0.1")
}
func testDisplayNamesUseLocalizedNameWithBSDNameFallback() {
setListenerMode(.allIPv4Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(localizedName: "Wi-Fi", addresses: [.ipv4("192.168.1.10")]),
FakeLocalInterfaceFacts.ethernet(localizedName: nil, addresses: [.ipv4("10.0.0.5")]),
]
interfaceFacts.emitChange()
XCTAssertEqual(
workflow.snapshot.commandAddressPicker.candidates.map(\.displayName),
["lo0", "en1", "Wi-Fi"],
"名称缺失回退 BSD 名,回环显示 lo0")
}
func testSameInterfaceDuplicateIPMergesAcrossInterfacesKept() {
setListenerMode(.allIPv4Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(
localizedName: "Wi-Fi",
addresses: [.ipv4("192.168.1.10"), .ipv4("192.168.1.10")]),
FakeLocalInterfaceFacts.ethernet(
bsdName: "en2", localizedName: "Ethernet 2", addresses: [.ipv4("192.168.1.10")]),
]
interfaceFacts.emitChange()
let picker = workflow.snapshot.commandAddressPicker
XCTAssertEqual(
picker.candidates.map(\.identity),
[
TerminalCommandAddressIdentity(bsdName: "lo0", address: "127.0.0.1"),
TerminalCommandAddressIdentity(bsdName: "en2", address: "192.168.1.10"),
TerminalCommandAddressIdentity(bsdName: "en0", address: "192.168.1.10"),
],
"同接口重复 IP 合并;不同接口同 IP 保留接口身份")
}
func testInterfaceChangeNotificationRepublishesCandidates() {
setListenerMode(.allIPv4Interfaces)
interfaceFacts.interfaces = [
FakeLocalInterfaceFacts.wifi(addresses: [.ipv4("192.168.1.10")])
]
interfaceFacts.emitChange()
XCTAssertEqual(
workflow.snapshot.commandAddressPicker.candidates.map(\.address),
["127.0.0.1", "192.168.1.10"], "接口事实变化后候选随网络刷新")
}
}