Files
ShadowsocksX-NG/ShadowsocksX-NG2/Tests/Settings/SettingsWorkflowInterfaceTests.swift
Qiu YuzhouandDeepSeek Harness 28c6e82555 test: drop cases whose injected failure can never fire
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>
2026-10-21 01:09:28 +08:00

257 lines
11 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
/// 设置项编辑器的 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
}
}