mirror of
https://github.com/shadowsocks/ShadowsocksX-NG.git
synced 2026-11-08 16:34:50 +00:00
Four listening-file failure cases inject InMemoryProxyListeningStore.saveError and assert saved == nil, which the fixture guarantees because save throws before assigning; the only production save call is updateListening and none of these paths reaches it. Their behavioral content is covered by sibling tests, so keep only the three assertions that were unique there (agent intent persistence, runtime stopped after the off cascade, no commit in flight) and move them into those siblings. Same pattern in testProxyExceptionsIgnoreListeningPersistenceFailure: the path calls only the non-throwing updateProxyExceptions, so its updateError injection is inert and the rest duplicates the sibling case. The plugin diagnostic test fed an unproducible fixture (user-sourced facts never carry a version, since managedInfo exists only for managed sources); assert the reachable property, the source label in the report row. FakeSettingsSaveError and the now-unused saveError seam go with them. Co-Authored-By: DeepSeek Harness <noreply@deepseek.com>
257 lines
11 KiB
Swift
257 lines
11 KiB
Swift
import XCTest
|
||
|
||
@testable import ShadowsocksX_NG2
|
||
|
||
/// 设置项编辑器的 UI-facing interface 测试;写入缝与占用探测均注入替身,
|
||
/// 不触碰真实偏好文件、钥匙串与系统端口。
|
||
@MainActor
|
||
final class SettingsWorkflowInterfaceTests: XCTestCase {
|
||
var committing: FakeSettingsCommitter!
|
||
var probe: FakeOccupancyProbe!
|
||
|
||
override func setUp() async throws {
|
||
try await super.setUp()
|
||
committing = FakeSettingsCommitter()
|
||
probe = FakeOccupancyProbe()
|
||
}
|
||
|
||
func makeWorkflow() -> SettingsWorkflow {
|
||
SettingsWorkflow(
|
||
committing: committing, occupancyProbe: probe)
|
||
}
|
||
|
||
func testPortEditorMapsValidationIssuesToTheAffectedFields() {
|
||
let workflow = makeWorkflow()
|
||
let invalid = SettingsPortDraft(socksPort: 0, httpPort: 11_087)
|
||
|
||
XCTAssertEqual(
|
||
workflow.portFieldState(for: .socks, editorDraft: invalid).issues,
|
||
[.port(.socks, error: .portOutOfRange(endpoint: .socks, port: 0))])
|
||
XCTAssertTrue(workflow.portFieldState(for: .http, editorDraft: invalid).issues.isEmpty)
|
||
|
||
let duplicate = SettingsPortDraft(socksPort: 11_086, httpPort: 11_086)
|
||
let duplicateIssue = SettingsFieldIssue.port(
|
||
.socks, error: .duplicatePort(endpoint: .socks, otherEndpoint: .http, port: 11_086))
|
||
XCTAssertEqual(
|
||
workflow.portFieldState(for: .socks, editorDraft: duplicate).issues,
|
||
[
|
||
duplicateIssue
|
||
])
|
||
XCTAssertEqual(
|
||
workflow.portFieldState(for: .http, editorDraft: duplicate).issues,
|
||
[.port(.http, error: .duplicatePort(endpoint: .socks, otherEndpoint: .http, port: 11_086))])
|
||
}
|
||
|
||
func testPortEditorReportsFreeOccupiedAndUnknownFacts() async {
|
||
probe = FakeOccupancyProbe(occupiedPorts: [11_087])
|
||
let workflow = makeWorkflow()
|
||
let draft = workflow.beginPortSettingsEditing()
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
XCTAssertEqual(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy, .free)
|
||
XCTAssertEqual(
|
||
workflow.portFieldState(for: .http, editorDraft: draft).occupancy,
|
||
.occupied(occupier: "other-app"))
|
||
|
||
probe = FakeOccupancyProbe(unknownPorts: [11_086])
|
||
let unknownWorkflow = makeWorkflow()
|
||
let unknownDraft = unknownWorkflow.beginPortSettingsEditing()
|
||
unknownWorkflow.refreshPortEditorOccupancy(for: unknownDraft)
|
||
await waitUntil(
|
||
unknownWorkflow.portFieldState(for: .socks, editorDraft: unknownDraft).occupancy != nil)
|
||
XCTAssertEqual(
|
||
unknownWorkflow.portFieldState(for: .socks, editorDraft: unknownDraft).occupancy,
|
||
.unknown(detail: "无法判定"))
|
||
}
|
||
|
||
func testPortEditorProbeUsesTheCompleteCommittedListenIdentity() async {
|
||
committing.committedSettings.listen.listenerMode = .allIPv4Interfaces
|
||
let workflow = makeWorkflow()
|
||
let draft = workflow.beginPortSettingsEditing()
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
let expected = RuntimeListenFacts(listen: committing.committedSettings.listen)
|
||
|
||
await waitUntil(probe.requests.contains { $0.listen == expected })
|
||
|
||
XCTAssertEqual(probe.requests.first(where: { $0.listen == expected })?.endpoint, .socks)
|
||
XCTAssertEqual(probe.requests.first(where: { $0.listen == expected })?.bindAddress, "127.0.0.1")
|
||
}
|
||
|
||
func testRuntimePortExceptionRequiresVerifiedOwnership() async {
|
||
probe = FakeOccupancyProbe(occupiedPorts: [11_086], occupierProcessIDs: [123])
|
||
committing.isProxyRunning = true
|
||
committing.runtimeListenerProcessID = 123
|
||
let workflow = makeWorkflow()
|
||
let draft = workflow.beginPortSettingsEditing()
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
XCTAssertTrue(workflow.portFieldState(for: .socks, editorDraft: draft).isRuntimePortException)
|
||
XCTAssertFalse(workflow.portFieldState(for: .socks, editorDraft: draft).canSuggestFreePort)
|
||
XCTAssertTrue(workflow.canSavePortSettings(draft))
|
||
|
||
committing.runtimeListenerProcessID = 456
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
XCTAssertFalse(workflow.portFieldState(for: .socks, editorDraft: draft).isRuntimePortException)
|
||
XCTAssertFalse(workflow.canSavePortSettings(draft))
|
||
}
|
||
|
||
func testChangedPortWithKnownExternalOccupancyBlocksPortSave() async {
|
||
probe = FakeOccupancyProbe(occupiedPorts: [12_086])
|
||
let workflow = makeWorkflow()
|
||
let draft = SettingsPortDraft(socksPort: 12_086, httpPort: 12_087)
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
XCTAssertTrue(workflow.portFieldState(for: .socks, editorDraft: draft).canSuggestFreePort)
|
||
XCTAssertFalse(workflow.canSavePortSettings(draft))
|
||
let outcome = await workflow.savePortSettings(draft)
|
||
XCTAssertEqual(outcome, .rejected(.occupied([.socks])))
|
||
XCTAssertTrue(committing.updateCalls.isEmpty)
|
||
}
|
||
|
||
func testUnknownPortOccupancyAllowsPortSave() async {
|
||
probe = FakeOccupancyProbe(unknownPorts: [12_086])
|
||
let workflow = makeWorkflow()
|
||
let draft = SettingsPortDraft(socksPort: 12_086, httpPort: 12_087)
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
XCTAssertTrue(workflow.canSavePortSettings(draft))
|
||
let outcome = await workflow.savePortSettings(draft)
|
||
XCTAssertEqual(outcome, .persisted)
|
||
XCTAssertEqual(committing.committedSettings.listen.socksPort, 12_086)
|
||
}
|
||
|
||
func testPortSuggestionReturnsACandidateWithoutSavingIt() async {
|
||
probe = FakeOccupancyProbe(occupiedPorts: [12_086])
|
||
let workflow = makeWorkflow()
|
||
let draft = SettingsPortDraft(socksPort: 12_086, httpPort: 12_087)
|
||
workflow.refreshPortEditorOccupancy(for: draft)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: draft).occupancy != nil)
|
||
|
||
let outcome = await workflow.suggestFreePort(for: .socks, from: draft)
|
||
|
||
XCTAssertEqual(outcome, .suggestedPort(port: .socks, value: 32_768))
|
||
XCTAssertTrue(committing.updateCalls.isEmpty)
|
||
XCTAssertEqual(committing.committedSettings.listen.socksPort, 11_086)
|
||
}
|
||
|
||
func testPortSettingsSaveChangesOnlyTheCommittedPorts() async {
|
||
let original = committedSettingsForItemSaveTests()
|
||
committing.committedSettings = original
|
||
let workflow = makeWorkflow()
|
||
let edited = SettingsPortDraft(socksPort: 12_096, httpPort: 12_097)
|
||
workflow.refreshPortEditorOccupancy(for: edited)
|
||
await waitUntil(workflow.canSavePortSettings(edited))
|
||
|
||
let outcome = await workflow.savePortSettings(edited)
|
||
|
||
var expected = original
|
||
expected.listen.socksPort = edited.socksPort
|
||
expected.listen.httpPort = edited.httpPort
|
||
XCTAssertEqual(outcome, .persisted)
|
||
XCTAssertEqual(committing.updateCalls, [expected])
|
||
XCTAssertEqual(committing.committedSettings, expected)
|
||
}
|
||
|
||
/// store 是唯一强制点:探测门禁通过后,非法端口由 store 拒绝并映射回字段 issue,
|
||
/// 不写入、不留提交锁。探测未完成时保存命令按占用门禁拒绝,不触达 store。
|
||
func testPortSaveRejectsInvalidPortsWithoutWriting() async {
|
||
let workflow = makeWorkflow()
|
||
let invalid = SettingsPortDraft(socksPort: 0, httpPort: 11_087)
|
||
workflow.refreshPortEditorOccupancy(for: invalid)
|
||
await waitUntil(workflow.portFieldState(for: .socks, editorDraft: invalid).occupancy != nil)
|
||
|
||
let outcome = await workflow.savePortSettings(invalid)
|
||
XCTAssertEqual(
|
||
outcome,
|
||
.rejected(
|
||
.validation([
|
||
.port(.socks, error: .portOutOfRange(endpoint: .socks, port: 0))
|
||
])))
|
||
XCTAssertFalse(workflow.isCommitting)
|
||
XCTAssertTrue(committing.updateCalls.isEmpty)
|
||
}
|
||
|
||
func testProxyExceptionsSaveChangesOnlyTheCommittedExceptions() async {
|
||
let original = committedSettingsForItemSaveTests()
|
||
committing.committedSettings = original
|
||
let workflow = makeWorkflow()
|
||
let edited = "localhost\nexample.com"
|
||
|
||
let outcome = await workflow.saveProxyExceptions(edited)
|
||
|
||
var expected = original
|
||
expected.proxyExceptions = edited
|
||
XCTAssertEqual(outcome, .persisted)
|
||
XCTAssertEqual(committing.updateCalls, [expected])
|
||
XCTAssertEqual(committing.committedSettings, expected)
|
||
XCTAssertNil(workflow.lastFailure)
|
||
XCTAssertFalse(workflow.isCommitting)
|
||
}
|
||
|
||
func testItemSavesSerializeWhilePersistenceIsInProgress() async {
|
||
committing.updateGate = AsyncGate()
|
||
let workflow = makeWorkflow()
|
||
let first = Task { await workflow.saveProxyExceptions("localhost") }
|
||
await waitUntil(workflow.isCommitting)
|
||
|
||
let second = await workflow.savePortSettings(
|
||
SettingsPortDraft(socksPort: 11_086, httpPort: 11_087))
|
||
committing.updateGate?.release()
|
||
let firstOutcome = await first.value
|
||
|
||
XCTAssertEqual(second, .rejected(.inProgress))
|
||
XCTAssertEqual(firstOutcome, .persisted)
|
||
XCTAssertEqual(committing.updateCalls.count, 1)
|
||
}
|
||
|
||
func testEqualProxyExceptionsDoNotWrite() async {
|
||
let workflow = makeWorkflow()
|
||
committing.committedSettings.proxyExceptions = "localhost"
|
||
|
||
let outcome = await workflow.saveProxyExceptions("localhost")
|
||
XCTAssertEqual(outcome, .persisted)
|
||
XCTAssertTrue(committing.updateCalls.isEmpty)
|
||
}
|
||
|
||
func testStalePortOccupancyCannotOverrideTheNewerEditorDraft() async {
|
||
let gated = GatedOccupancyProbe(
|
||
gatedAnswer: .occupied(
|
||
PortOccupancyFacts(
|
||
occupier: "stale", occupiedFamilies: [.ipv4], verifiedFamilies: [.ipv4])),
|
||
passThroughAnswer: .free)
|
||
let workflow = SettingsWorkflow(committing: committing, occupancyProbe: gated)
|
||
let firstDraft = workflow.beginPortSettingsEditing()
|
||
workflow.refreshPortEditorOccupancy(for: firstDraft)
|
||
await waitUntil(gated.entered > 0)
|
||
|
||
let laterDraft = SettingsPortDraft(socksPort: 30_086, httpPort: 11_087)
|
||
gated.passThrough(.free)
|
||
workflow.refreshPortEditorOccupancy(for: laterDraft)
|
||
await waitUntil(
|
||
workflow.portFieldState(for: .socks, editorDraft: laterDraft).occupancy == .free)
|
||
|
||
gated.releaseGatedCalls()
|
||
try? await Task.sleep(nanoseconds: 50_000_000)
|
||
XCTAssertEqual(
|
||
workflow.portFieldState(for: .socks, editorDraft: laterDraft).occupancy, .free)
|
||
}
|
||
|
||
func committedSettingsForItemSaveTests() -> ProxyRuntimeSettings {
|
||
var settings = ProxyRuntimeSettings()
|
||
settings.listen.listenerMode = .allIPv6Interfaces
|
||
settings.listen.socksPort = 12_086
|
||
settings.listen.httpPort = 12_087
|
||
settings.proxyExceptions = "existing.example"
|
||
settings.preferredMode = .global
|
||
settings.agentEnabled = true
|
||
settings.systemProxyEnabled = true
|
||
return settings
|
||
}
|
||
}
|