Experte-Tab: Port-Konflikt-Pruefung wie im Einrichten-Assistenten
Per explizitem Nutzerwunsch die "Port freimachen?"-Abfrage samt allen Warnungen aus dem LAN-Schritt des Wizards auch auf den Experte-Tab angewendet, ausgeloest durch echten Live-Fall (ether4 wurde manuell ueber Expert als eigenes Netz angelegt, blieb dabei unbemerkt Bridge- Mitglied - zwei DHCP-Server im selben Broadcast-Domain). PortConflictWarningView aus LanStepView.swift in Features/Shared/ extrahiert (jetzt von Wizard UND Experte-Tab genutzt), neuer immediateApply-Parameter fuer die kontextabhaengige Abschluss-Meldung (Wizard: erst bei "Jetzt anwenden"; Experte: sofort bei "Anlegen"/ "Speichern"). Neue PortConflict.resolutionCommandsIncludingBridgeDetach() - der Experte-Tab hat anders als der Wizard keinen separaten, automatischen Bridge-Detach-Schritt, muss die Bridge-Entfernung also selbst mit auflisten. ExpertViewModel bekommt dieselbe Race-sichere Generation-Zaehler-Logik wie SetupViewModel (bugs.md #1), angewendet auf das .interfacePick-Feld des jeweils offenen Schemas. Speichern-Button gesperrt bis Konflikt bestaetigt oder Port gewechselt. 6 neue Regressionstests. Build + alle 111 Unit-Tests gruen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -77,4 +77,58 @@ final class ExpertViewModelTests: XCTestCase {
|
||||
XCTAssertNil(viewModel.pendingCommand?.arguments["distance"])
|
||||
XCTAssertEqual(viewModel.pendingCommand?.arguments["comment"], "bridge")
|
||||
}
|
||||
|
||||
/// Regression tests for the Experte-tab port-conflict check (per explicit request: "die
|
||||
/// Abfrage vom Einrichten-Assistenten auf Expert anwenden ... mit allen Warnungen"). No live
|
||||
/// connection is set up here, so `checkInterfacePortConflict()`'s actual network call always
|
||||
/// fails/is skipped — these only cover the surrounding logic that doesn't need one: schemas
|
||||
/// without an `.interfacePick` field are correctly ignored, and the unresolved-conflict gate
|
||||
/// starts clear.
|
||||
private func makeSchemaWithInterfaceField() -> RouterOSMenuSchema {
|
||||
RouterOSMenuSchema(
|
||||
menuPath: "/ip address", restPath: "ip/address", category: .ipAddressing,
|
||||
displayName: "Test", summary: "", explanation: "",
|
||||
fields: [
|
||||
RouterOSFieldSchema(key: "address", label: "Adresse", kind: .text, help: ""),
|
||||
RouterOSFieldSchema(key: "interface", label: "Interface", kind: .interfacePick, help: "")
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
func testCheckInterfacePortConflictNoOpsForSchemaWithoutInterfaceField() {
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = makeSchema() // no .interfacePick field
|
||||
viewModel.startNewItem()
|
||||
|
||||
viewModel.checkInterfacePortConflict()
|
||||
|
||||
XCTAssertFalse(viewModel.isCheckingInterfacePortConflict)
|
||||
XCTAssertNil(viewModel.interfacePortConflict)
|
||||
XCTAssertFalse(viewModel.hasUnresolvedInterfacePortConflict)
|
||||
}
|
||||
|
||||
func testCheckInterfacePortConflictNoOpsWhenInterfaceFieldIsEmpty() {
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = makeSchemaWithInterfaceField()
|
||||
viewModel.startNewItem() // "interface" has no default, starts empty
|
||||
|
||||
viewModel.checkInterfacePortConflict()
|
||||
|
||||
XCTAssertFalse(viewModel.isCheckingInterfacePortConflict)
|
||||
XCTAssertNil(viewModel.interfacePortConflict)
|
||||
}
|
||||
|
||||
func testStartingOrCancelingEditResetsAcknowledgedConflictState() {
|
||||
let viewModel = ExpertViewModel(connectionService: ConnectionService())
|
||||
viewModel.selectedSchema = makeSchemaWithInterfaceField()
|
||||
viewModel.startNewItem()
|
||||
viewModel.acknowledgeInterfacePortConflict()
|
||||
XCTAssertFalse(viewModel.hasUnresolvedInterfacePortConflict, "acknowledging clears the block even with no conflict object set")
|
||||
|
||||
viewModel.cancelEditing()
|
||||
viewModel.startNewItem()
|
||||
|
||||
// A fresh edit must not inherit a stale acknowledgement from a previous one.
|
||||
XCTAssertNil(viewModel.interfacePortConflict)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,4 +29,26 @@ final class PortConflictTests: XCTestCase {
|
||||
XCTAssertEqual(commands[1].menuPath, "/interface pppoe-client")
|
||||
XCTAssertEqual(commands[1].operation, .remove(matchField: "interface", matchValue: "ether1"))
|
||||
}
|
||||
|
||||
/// Regression test for the Experte-tab "Port freimachen?" flow (per explicit request): unlike
|
||||
/// the Setup Wizard, the Experte tab has no separate step that unconditionally detaches a
|
||||
/// bridge port on its own, so its resolution list must include that removal explicitly —
|
||||
/// `resolutionCommands()` alone (the wizard's version) deliberately leaves it out.
|
||||
func testResolutionCommandsIncludingBridgeDetachAddsBridgePortRemoval() {
|
||||
let conflict = PortConflict(interfaceName: "ether4", reasons: [.bridgeMember(bridgeName: "bridge")])
|
||||
let commands = conflict.resolutionCommandsIncludingBridgeDetach()
|
||||
|
||||
XCTAssertEqual(commands.count, 1)
|
||||
XCTAssertEqual(commands[0].menuPath, "/interface bridge port")
|
||||
XCTAssertEqual(commands[0].operation, .remove(matchField: "interface", matchValue: "ether4"))
|
||||
}
|
||||
|
||||
func testResolutionCommandsIncludingBridgeDetachKeepsOtherReasonsToo() {
|
||||
let conflict = PortConflict(interfaceName: "ether4", reasons: [.bridgeMember(bridgeName: "bridge"), .dhcpClient])
|
||||
let commands = conflict.resolutionCommandsIncludingBridgeDetach()
|
||||
|
||||
XCTAssertEqual(commands.count, 2)
|
||||
XCTAssertEqual(commands[0].menuPath, "/interface bridge port")
|
||||
XCTAssertEqual(commands[1].menuPath, "/ip dhcp-client")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user