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>
55 lines
2.8 KiB
Swift
55 lines
2.8 KiB
Swift
import XCTest
|
|
@testable import RouterOSAssistant
|
|
|
|
final class PortConflictTests: XCTestCase {
|
|
func testBridgeMembershipProducesNoResolutionCommand() {
|
|
// DhcpServerCommandBuilder already unconditionally detaches any bridge membership —
|
|
// repeating it here would just be a harmless duplicate at best, so it's deliberately
|
|
// excluded from this list.
|
|
let conflict = PortConflict(interfaceName: "ether4", reasons: [.bridgeMember(bridgeName: "bridge")])
|
|
XCTAssertTrue(conflict.resolutionCommands().isEmpty)
|
|
}
|
|
|
|
func testExistingAddressIsRemovedByInterfaceMatch() {
|
|
let conflict = PortConflict(interfaceName: "ether4", reasons: [.hasAddresses(["10.0.0.1/24"])])
|
|
let commands = conflict.resolutionCommands()
|
|
|
|
XCTAssertEqual(commands.count, 1)
|
|
XCTAssertEqual(commands[0].menuPath, "/ip address")
|
|
XCTAssertEqual(commands[0].operation, .remove(matchField: "interface", matchValue: "ether4"))
|
|
}
|
|
|
|
func testDhcpClientAndPppoeClientAreBothRemoved() {
|
|
let conflict = PortConflict(interfaceName: "ether1", reasons: [.dhcpClient, .pppoeClient])
|
|
let commands = conflict.resolutionCommands()
|
|
|
|
XCTAssertEqual(commands.count, 2)
|
|
XCTAssertEqual(commands[0].menuPath, "/ip dhcp-client")
|
|
XCTAssertEqual(commands[0].operation, .remove(matchField: "interface", matchValue: "ether1"))
|
|
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")
|
|
}
|
|
}
|