Files
KayandClaude Sonnet 5 b503bac82c 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>
2026-09-17 22:21:36 +02:00

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")
}
}