forked from kay/RouterOS
M5: WLAN-Schritt + echte set-Operation im Befehlsmodell
RouterOSCommand unterstützt jetzt neben .add auch .set (bestehenden Eintrag ändern statt neuen anzulegen) — nötig, weil WLAN-Interfaces schon vor jeder Konfiguration existieren. SSH löst das per CLI-eigenem "set [find field=value] ..." inline auf; REST hat dafür keine Entsprechung und muss den Eintrag erst per GET suchen (matchField/ matchValue), seine .id auslesen, dann PATCH auf restPath/<id> senden (RestTransport.findItemID). Mit dem Nutzer abgestimmte Entscheidung gegen die einfachere "WLAN nur über SSH"-Variante. WifiNetworkConfig: pro erkanntem Legacy-Wireless-Interface (/interface wireless, type=wlan) eine SSID/Passwort-Konfiguration, Sicherheitsprofil (WPA2) wird zuerst angelegt, dann per set mit dem Interface verknüpft. Geräte ohne WLAN zeigen einen Hinweistext statt des Formulars (User-Anforderung: muss berücksichtigt werden). Geräte mit dem neueren "wifi"-Treiber (type=wifi, wifiwave2/802.11ax) werden erkannt, aber bewusst nicht unterstützt -- anderes Menü, eigener Umbau nötig, dazu Hinweistext. cliPath wurde in allen bisherigen Command-Buildern (Wan/Lan/Vlan) zu menuPath + .add migriert, da RouterOSCommand jetzt operation-basiert ist statt den Aktionswort im Pfad-String zu verstecken. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HReLXMbmPvtQ23p1iWiJNW
This commit is contained in:
@@ -18,20 +18,20 @@ enum DhcpServerCommandBuilder {
|
|||||||
let routerIP = routerAddress.components(separatedBy: "/").first ?? routerAddress
|
let routerIP = routerAddress.components(separatedBy: "/").first ?? routerAddress
|
||||||
|
|
||||||
return [
|
return [
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip address add",
|
menuPath: "/ip address",
|
||||||
restPath: "ip/address",
|
restPath: "ip/address",
|
||||||
arguments: ["address": routerAddress, "interface": interfaceName],
|
arguments: ["address": routerAddress, "interface": interfaceName],
|
||||||
summary: "IP-Adresse \(routerAddress) \(context) setzen"
|
summary: "IP-Adresse \(routerAddress) \(context) setzen"
|
||||||
),
|
),
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip pool add",
|
menuPath: "/ip pool",
|
||||||
restPath: "ip/pool",
|
restPath: "ip/pool",
|
||||||
arguments: ["name": poolName, "ranges": "\(poolRangeStart)-\(poolRangeEnd)"],
|
arguments: ["name": poolName, "ranges": "\(poolRangeStart)-\(poolRangeEnd)"],
|
||||||
summary: "Adressbereich \(poolRangeStart)–\(poolRangeEnd) \(context) anlegen"
|
summary: "Adressbereich \(poolRangeStart)–\(poolRangeEnd) \(context) anlegen"
|
||||||
),
|
),
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip dhcp-server add",
|
menuPath: "/ip dhcp-server",
|
||||||
restPath: "ip/dhcp-server",
|
restPath: "ip/dhcp-server",
|
||||||
arguments: [
|
arguments: [
|
||||||
"name": serverName,
|
"name": serverName,
|
||||||
@@ -42,8 +42,8 @@ enum DhcpServerCommandBuilder {
|
|||||||
],
|
],
|
||||||
summary: "DHCP-Server \(context) aktivieren"
|
summary: "DHCP-Server \(context) aktivieren"
|
||||||
),
|
),
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip dhcp-server network add",
|
menuPath: "/ip dhcp-server network",
|
||||||
restPath: "ip/dhcp-server/network",
|
restPath: "ip/dhcp-server/network",
|
||||||
arguments: [
|
arguments: [
|
||||||
"address": networkAddress,
|
"address": networkAddress,
|
||||||
|
|||||||
@@ -1,26 +1,75 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
/// A single RouterOS configuration change, expressed once and executed over either transport
|
/// A single RouterOS configuration change, expressed once and executed over either transport
|
||||||
/// (rendered as a CLI line for SSH, as a JSON body for REST).
|
/// (rendered as a CLI line for SSH, as a JSON body — plus a lookup for `.set` — for REST).
|
||||||
struct RouterOSCommand: Equatable, Identifiable {
|
struct RouterOSCommand: Equatable, Identifiable {
|
||||||
var id: String { cliPath + summary }
|
enum Operation: Equatable {
|
||||||
|
/// Creates a new item under `restPath` (SSH: `<menuPath> add ...`, REST: `POST`).
|
||||||
|
case add
|
||||||
|
/// Modifies an existing item matched by one field's value (SSH: `<menuPath> set [find
|
||||||
|
/// field=value] ...`; REST has no such inline lookup, so it needs a GET first to find
|
||||||
|
/// the item's `.id`, then `PATCH restPath/<id>`).
|
||||||
|
case set(matchField: String, matchValue: String)
|
||||||
|
}
|
||||||
|
|
||||||
/// CLI add-path, e.g. "/ip address add".
|
var id: String {
|
||||||
let cliPath: String
|
switch operation {
|
||||||
/// REST resource path, e.g. "ip/address", posted to create the same item.
|
case .add:
|
||||||
|
return "add:\(menuPath):\(summary)"
|
||||||
|
case .set(let field, let value):
|
||||||
|
return "set:\(menuPath):\(field)=\(value):\(summary)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// RouterOS menu path without the action word, e.g. "/ip address" or "/interface wireless".
|
||||||
|
let menuPath: String
|
||||||
|
/// REST resource path under `/rest/`, e.g. "ip/address" or "interface/wireless".
|
||||||
let restPath: String
|
let restPath: String
|
||||||
/// RouterOS "words" (key=value arguments).
|
let operation: Operation
|
||||||
|
/// RouterOS "words" (key=value fields) to add or change.
|
||||||
let arguments: [String: String]
|
let arguments: [String: String]
|
||||||
/// Human-readable description shown on the review screen before applying.
|
/// Human-readable description shown on the review screen before applying.
|
||||||
let summary: String
|
let summary: String
|
||||||
|
|
||||||
/// Renders as a RouterOS CLI line, e.g. `/ip address add address=192.168.88.1/24 interface=bridge`.
|
static func add(menuPath: String, restPath: String, arguments: [String: String], summary: String) -> RouterOSCommand {
|
||||||
|
RouterOSCommand(menuPath: menuPath, restPath: restPath, operation: .add, arguments: arguments, summary: summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func set(
|
||||||
|
menuPath: String,
|
||||||
|
restPath: String,
|
||||||
|
matchField: String,
|
||||||
|
matchValue: String,
|
||||||
|
arguments: [String: String],
|
||||||
|
summary: String
|
||||||
|
) -> RouterOSCommand {
|
||||||
|
RouterOSCommand(
|
||||||
|
menuPath: menuPath,
|
||||||
|
restPath: restPath,
|
||||||
|
operation: .set(matchField: matchField, matchValue: matchValue),
|
||||||
|
arguments: arguments,
|
||||||
|
summary: summary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Renders as a RouterOS CLI line, e.g. `/ip address add address=192.168.88.1/24 interface=bridge`
|
||||||
|
/// or `/interface wireless set [find name=wlan1] ssid=Home`.
|
||||||
var cliLine: String {
|
var cliLine: String {
|
||||||
let args = arguments
|
let args = renderedArguments
|
||||||
|
switch operation {
|
||||||
|
case .add:
|
||||||
|
return args.isEmpty ? "\(menuPath) add" : "\(menuPath) add \(args)"
|
||||||
|
case .set(let field, let value):
|
||||||
|
let finder = "[find \(field)=\(Self.quoteIfNeeded(value))]"
|
||||||
|
return args.isEmpty ? "\(menuPath) set \(finder)" : "\(menuPath) set \(finder) \(args)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var renderedArguments: String {
|
||||||
|
arguments
|
||||||
.sorted { $0.key < $1.key }
|
.sorted { $0.key < $1.key }
|
||||||
.map { "\($0.key)=\(Self.quoteIfNeeded($0.value))" }
|
.map { "\($0.key)=\(Self.quoteIfNeeded($0.value))" }
|
||||||
.joined(separator: " ")
|
.joined(separator: " ")
|
||||||
return args.isEmpty ? cliPath : "\(cliPath) \(args)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func quoteIfNeeded(_ value: String) -> String {
|
private static func quoteIfNeeded(_ value: String) -> String {
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ struct VlanEntry: Identifiable, Equatable {
|
|||||||
var interfaceName: String { "vlan\(vlanID)" }
|
var interfaceName: String { "vlan\(vlanID)" }
|
||||||
|
|
||||||
func buildCommands() -> [RouterOSCommand] {
|
func buildCommands() -> [RouterOSCommand] {
|
||||||
let createVlan = RouterOSCommand(
|
let createVlan = RouterOSCommand.add(
|
||||||
cliPath: "/interface vlan add",
|
menuPath: "/interface vlan",
|
||||||
restPath: "interface/vlan",
|
restPath: "interface/vlan",
|
||||||
arguments: ["name": interfaceName, "vlan-id": "\(vlanID)", "interface": parentInterface],
|
arguments: ["name": interfaceName, "vlan-id": "\(vlanID)", "interface": parentInterface],
|
||||||
summary: "VLAN \"\(name)\" (ID \(vlanID)) auf \(parentInterface) anlegen"
|
summary: "VLAN \"\(name)\" (ID \(vlanID)) auf \(parentInterface) anlegen"
|
||||||
|
|||||||
@@ -31,8 +31,8 @@ struct WanConfig: Equatable {
|
|||||||
switch mode {
|
switch mode {
|
||||||
case .dhcpClient:
|
case .dhcpClient:
|
||||||
return [
|
return [
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip dhcp-client add",
|
menuPath: "/ip dhcp-client",
|
||||||
restPath: "ip/dhcp-client",
|
restPath: "ip/dhcp-client",
|
||||||
arguments: ["interface": interfaceName, "disabled": "no"],
|
arguments: ["interface": interfaceName, "disabled": "no"],
|
||||||
summary: "Internetadresse auf \(interfaceName) automatisch beziehen (DHCP-Client)"
|
summary: "Internetadresse auf \(interfaceName) automatisch beziehen (DHCP-Client)"
|
||||||
@@ -40,8 +40,8 @@ struct WanConfig: Equatable {
|
|||||||
]
|
]
|
||||||
case .staticIP:
|
case .staticIP:
|
||||||
var commands = [
|
var commands = [
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip address add",
|
menuPath: "/ip address",
|
||||||
restPath: "ip/address",
|
restPath: "ip/address",
|
||||||
arguments: ["address": staticAddress, "interface": interfaceName],
|
arguments: ["address": staticAddress, "interface": interfaceName],
|
||||||
summary: "Statische IP-Adresse \(staticAddress) auf \(interfaceName) setzen"
|
summary: "Statische IP-Adresse \(staticAddress) auf \(interfaceName) setzen"
|
||||||
@@ -49,8 +49,8 @@ struct WanConfig: Equatable {
|
|||||||
]
|
]
|
||||||
if !staticGateway.isEmpty {
|
if !staticGateway.isEmpty {
|
||||||
commands.append(
|
commands.append(
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/ip route add",
|
menuPath: "/ip route",
|
||||||
restPath: "ip/route",
|
restPath: "ip/route",
|
||||||
arguments: ["gateway": staticGateway],
|
arguments: ["gateway": staticGateway],
|
||||||
summary: "Standard-Route über \(staticGateway) einrichten"
|
summary: "Standard-Route über \(staticGateway) einrichten"
|
||||||
@@ -60,8 +60,8 @@ struct WanConfig: Equatable {
|
|||||||
return commands
|
return commands
|
||||||
case .pppoe:
|
case .pppoe:
|
||||||
return [
|
return [
|
||||||
RouterOSCommand(
|
RouterOSCommand.add(
|
||||||
cliPath: "/interface pppoe-client add",
|
menuPath: "/interface pppoe-client",
|
||||||
restPath: "interface/pppoe-client",
|
restPath: "interface/pppoe-client",
|
||||||
arguments: [
|
arguments: [
|
||||||
"interface": interfaceName,
|
"interface": interfaceName,
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/// SSID/password for one existing wireless radio (`/interface wireless`, the legacy RouterOS
|
||||||
|
/// wireless driver). Devices without any WLAN hardware simply have no entries.
|
||||||
|
///
|
||||||
|
/// Not supported: RouterOS' newer "wifi" package (`/interface wifi`, used on newer wifiwave2/
|
||||||
|
/// 802.11ax hardware) — different menu structure entirely. Detecting and handling that is left
|
||||||
|
/// for a later milestone; the Setup step explains this to the user when it sees `wifi`-typed
|
||||||
|
/// interfaces instead of `wlan`-typed ones.
|
||||||
|
struct WifiNetworkConfig: Identifiable, Equatable {
|
||||||
|
var id: String { interfaceName }
|
||||||
|
var interfaceName: String
|
||||||
|
var enabled: Bool = false
|
||||||
|
var ssid: String = ""
|
||||||
|
var password: String = ""
|
||||||
|
|
||||||
|
private var securityProfileName: String { "sec_\(interfaceName)" }
|
||||||
|
|
||||||
|
/// The wireless interface already exists out of the box — this only ever `set`s it plus
|
||||||
|
/// `add`s a security profile, never creates a new wireless interface.
|
||||||
|
func buildCommands() -> [RouterOSCommand] {
|
||||||
|
guard enabled else { return [] }
|
||||||
|
|
||||||
|
return [
|
||||||
|
RouterOSCommand.add(
|
||||||
|
menuPath: "/interface wireless security-profiles",
|
||||||
|
restPath: "interface/wireless/security-profiles",
|
||||||
|
arguments: [
|
||||||
|
"name": securityProfileName,
|
||||||
|
"mode": "dynamic-keys",
|
||||||
|
"authentication-types": "wpa2-psk",
|
||||||
|
"wpa2-pre-shared-key": password
|
||||||
|
],
|
||||||
|
summary: "WLAN-Sicherheitsprofil für \"\(ssid)\" anlegen (WPA2)"
|
||||||
|
),
|
||||||
|
RouterOSCommand.set(
|
||||||
|
menuPath: "/interface wireless",
|
||||||
|
restPath: "interface/wireless",
|
||||||
|
matchField: "name",
|
||||||
|
matchValue: interfaceName,
|
||||||
|
arguments: [
|
||||||
|
"ssid": ssid,
|
||||||
|
"security-profile": securityProfileName,
|
||||||
|
"disabled": "no"
|
||||||
|
],
|
||||||
|
summary: "WLAN \"\(ssid)\" auf \(interfaceName) aktivieren"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,9 +56,29 @@ final class RestTransport: NSObject, RouterOSTransport {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates the item described by `command` via `POST /rest/<restPath>`.
|
/// Creates or modifies the item described by `command`. `.add` is a plain
|
||||||
|
/// `POST /rest/<restPath>`. `.set` has no CLI-style inline lookup on REST, so it first
|
||||||
|
/// `GET`s the collection to find the item whose `matchField` equals `matchValue`, reads
|
||||||
|
/// its RouterOS-internal `.id`, then `PATCH`es `restPath/<id>`.
|
||||||
func apply(_ command: RouterOSCommand) async throws {
|
func apply(_ command: RouterOSCommand) async throws {
|
||||||
_ = try await send(path: command.restPath, method: "POST", jsonBody: command.arguments)
|
switch command.operation {
|
||||||
|
case .add:
|
||||||
|
_ = try await send(path: command.restPath, method: "POST", jsonBody: command.arguments)
|
||||||
|
case .set(let matchField, let matchValue):
|
||||||
|
let itemID = try await findItemID(path: command.restPath, matchField: matchField, matchValue: matchValue)
|
||||||
|
_ = try await send(path: "\(command.restPath)/\(itemID)", method: "PATCH", jsonBody: command.arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func findItemID(path: String, matchField: String, matchValue: String) async throws -> String {
|
||||||
|
let items = try await getJSONArray(path: path)
|
||||||
|
guard let match = items.first(where: { ($0[matchField] as? String) == matchValue }) else {
|
||||||
|
throw RouterOSError.invalidResponse("Kein Eintrag mit \(matchField)=\(matchValue) unter \(path) gefunden")
|
||||||
|
}
|
||||||
|
guard let id = match[".id"] as? String else {
|
||||||
|
throw RouterOSError.invalidResponse("Eintrag unter \(path) hat keine .id")
|
||||||
|
}
|
||||||
|
return id
|
||||||
}
|
}
|
||||||
|
|
||||||
func disconnect() async {
|
func disconnect() async {
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ struct SetupView: View {
|
|||||||
LanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces)
|
LanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces)
|
||||||
case .vlan:
|
case .vlan:
|
||||||
VlanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces)
|
VlanStepView(viewModel: viewModel, availableInterfaces: connectionService.interfaces)
|
||||||
|
case .wifi:
|
||||||
|
WifiStepView(viewModel: viewModel)
|
||||||
case .review:
|
case .review:
|
||||||
ReviewApplyView(viewModel: viewModel, credentials: connectionService.credentials)
|
ReviewApplyView(viewModel: viewModel, credentials: connectionService.credentials)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ enum SetupStep: Int, CaseIterable {
|
|||||||
case wan
|
case wan
|
||||||
case lan
|
case lan
|
||||||
case vlan
|
case vlan
|
||||||
|
case wifi
|
||||||
case review
|
case review
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ final class SetupViewModel: ObservableObject {
|
|||||||
@Published var lanConfig = LanDhcpConfig()
|
@Published var lanConfig = LanDhcpConfig()
|
||||||
@Published private(set) var vlanSectionEnabled = false
|
@Published private(set) var vlanSectionEnabled = false
|
||||||
@Published var vlans: [VlanEntry] = []
|
@Published var vlans: [VlanEntry] = []
|
||||||
|
@Published var wifiNetworks: [WifiNetworkConfig] = []
|
||||||
|
@Published private(set) var unsupportedWifiInterfaces: [NetworkInterface] = []
|
||||||
|
|
||||||
@Published private(set) var isApplying = false
|
@Published private(set) var isApplying = false
|
||||||
@Published private(set) var applyLog: [String] = []
|
@Published private(set) var applyLog: [String] = []
|
||||||
@@ -30,7 +33,10 @@ final class SetupViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var plannedCommands: [RouterOSCommand] {
|
var plannedCommands: [RouterOSCommand] {
|
||||||
wanConfig.buildCommands() + lanConfig.buildCommands() + vlans.flatMap { $0.buildCommands() }
|
wanConfig.buildCommands()
|
||||||
|
+ lanConfig.buildCommands()
|
||||||
|
+ vlans.flatMap { $0.buildCommands() }
|
||||||
|
+ wifiNetworks.flatMap { $0.buildCommands() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func setVlanSectionEnabled(_ enabled: Bool) {
|
func setVlanSectionEnabled(_ enabled: Bool) {
|
||||||
@@ -73,6 +79,12 @@ final class SetupViewModel: ObservableObject {
|
|||||||
lanConfig.interfaceName = interfaces[0].name
|
lanConfig.interfaceName = interfaces[0].name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if wifiNetworks.isEmpty {
|
||||||
|
let legacyWireless = interfaces.filter { $0.type.lowercased() == "wlan" }
|
||||||
|
wifiNetworks = legacyWireless.map { WifiNetworkConfig(interfaceName: $0.name) }
|
||||||
|
}
|
||||||
|
unsupportedWifiInterfaces = interfaces.filter { $0.type.lowercased() == "wifi" }
|
||||||
}
|
}
|
||||||
|
|
||||||
func goNext() {
|
func goNext() {
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct WifiStepView: View {
|
||||||
|
@ObservedObject var viewModel: SetupViewModel
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
Form {
|
||||||
|
if viewModel.wifiNetworks.isEmpty && viewModel.unsupportedWifiInterfaces.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text("An diesem Gerät wurde kein WLAN erkannt. Dieser Schritt wird übersprungen.")
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !viewModel.unsupportedWifiInterfaces.isEmpty {
|
||||||
|
Section {
|
||||||
|
Text(
|
||||||
|
"Dein Gerät nutzt den neueren RouterOS-WiFi-Treiber (\(viewModel.unsupportedWifiInterfaces.map(\.name).joined(separator: ", "))), "
|
||||||
|
+ "den diese App noch nicht unterstützt. WLAN muss dafür vorerst manuell über Winbox eingerichtet werden."
|
||||||
|
)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ForEach($viewModel.wifiNetworks) { $network in
|
||||||
|
Section("WLAN auf \(network.interfaceName)") {
|
||||||
|
Toggle("Aktivieren", isOn: $network.enabled)
|
||||||
|
if network.enabled {
|
||||||
|
TextField("Netzwerkname (SSID)", text: $network.ssid)
|
||||||
|
SecureField("Passwort (mind. 8 Zeichen)", text: $network.password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Button("Zurück") { viewModel.goBack() }
|
||||||
|
Spacer()
|
||||||
|
Button("Weiter") { viewModel.goNext() }
|
||||||
|
.disabled(!isStepValid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.formStyle(.grouped)
|
||||||
|
.navigationTitle("WLAN (falls vorhanden)")
|
||||||
|
}
|
||||||
|
|
||||||
|
private var isStepValid: Bool {
|
||||||
|
viewModel.wifiNetworks.allSatisfy { network in
|
||||||
|
!network.enabled || (!network.ssid.isEmpty && network.password.count >= 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,8 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
|||||||
let commands = config.buildCommands()
|
let commands = config.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 1)
|
XCTAssertEqual(commands.count, 1)
|
||||||
XCTAssertEqual(commands[0].cliPath, "/ip dhcp-client add")
|
XCTAssertEqual(commands[0].menuPath, "/ip dhcp-client")
|
||||||
|
XCTAssertEqual(commands[0].operation, .add)
|
||||||
XCTAssertEqual(commands[0].arguments["interface"], "ether1")
|
XCTAssertEqual(commands[0].arguments["interface"], "ether1")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,7 +34,7 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
|||||||
let commands = config.buildCommands()
|
let commands = config.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 1)
|
XCTAssertEqual(commands.count, 1)
|
||||||
XCTAssertEqual(commands[0].cliPath, "/interface pppoe-client add")
|
XCTAssertEqual(commands[0].menuPath, "/interface pppoe-client")
|
||||||
XCTAssertEqual(commands[0].arguments["user"], "user@isp")
|
XCTAssertEqual(commands[0].arguments["user"], "user@isp")
|
||||||
XCTAssertEqual(commands[0].arguments["password"], "secret")
|
XCTAssertEqual(commands[0].arguments["password"], "secret")
|
||||||
}
|
}
|
||||||
@@ -43,16 +44,16 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
|||||||
let commands = config.buildCommands()
|
let commands = config.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 4)
|
XCTAssertEqual(commands.count, 4)
|
||||||
XCTAssertEqual(commands[0].cliPath, "/ip address add")
|
XCTAssertEqual(commands[0].menuPath, "/ip address")
|
||||||
XCTAssertEqual(commands[1].cliPath, "/ip pool add")
|
XCTAssertEqual(commands[1].menuPath, "/ip pool")
|
||||||
XCTAssertEqual(commands[2].cliPath, "/ip dhcp-server add")
|
XCTAssertEqual(commands[2].menuPath, "/ip dhcp-server")
|
||||||
XCTAssertEqual(commands[3].cliPath, "/ip dhcp-server network add")
|
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server network")
|
||||||
XCTAssertEqual(commands[3].arguments["gateway"], "192.168.88.1")
|
XCTAssertEqual(commands[3].arguments["gateway"], "192.168.88.1")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testCliLineRendersSortedQuotedArguments() {
|
func testCliLineRendersSortedQuotedArgumentsForAdd() {
|
||||||
let command = RouterOSCommand(
|
let command = RouterOSCommand.add(
|
||||||
cliPath: "/interface pppoe-client add",
|
menuPath: "/interface pppoe-client",
|
||||||
restPath: "interface/pppoe-client",
|
restPath: "interface/pppoe-client",
|
||||||
arguments: ["user": "user@isp", "password": "a secret"],
|
arguments: ["user": "user@isp", "password": "a secret"],
|
||||||
summary: "test"
|
summary: "test"
|
||||||
@@ -60,4 +61,17 @@ final class RouterOSCommandBuilderTests: XCTestCase {
|
|||||||
|
|
||||||
XCTAssertEqual(command.cliLine, "/interface pppoe-client add password=\"a secret\" user=user@isp")
|
XCTAssertEqual(command.cliLine, "/interface pppoe-client add password=\"a secret\" user=user@isp")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testCliLineRendersFindLookupForSet() {
|
||||||
|
let command = RouterOSCommand.set(
|
||||||
|
menuPath: "/interface wireless",
|
||||||
|
restPath: "interface/wireless",
|
||||||
|
matchField: "name",
|
||||||
|
matchValue: "wlan1",
|
||||||
|
arguments: ["ssid": "Home"],
|
||||||
|
summary: "test"
|
||||||
|
)
|
||||||
|
|
||||||
|
XCTAssertEqual(command.cliLine, "/interface wireless set [find name=wlan1] ssid=Home")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ final class VlanEntryTests: XCTestCase {
|
|||||||
let commands = vlan.buildCommands()
|
let commands = vlan.buildCommands()
|
||||||
|
|
||||||
XCTAssertEqual(commands.count, 5)
|
XCTAssertEqual(commands.count, 5)
|
||||||
XCTAssertEqual(commands[0].cliPath, "/interface vlan add")
|
XCTAssertEqual(commands[0].menuPath, "/interface vlan")
|
||||||
XCTAssertEqual(commands[0].arguments["vlan-id"], "20")
|
XCTAssertEqual(commands[0].arguments["vlan-id"], "20")
|
||||||
XCTAssertEqual(commands[0].arguments["interface"], "bridge")
|
XCTAssertEqual(commands[0].arguments["interface"], "bridge")
|
||||||
XCTAssertEqual(commands[0].arguments["name"], "vlan20")
|
XCTAssertEqual(commands[0].arguments["name"], "vlan20")
|
||||||
|
|
||||||
XCTAssertEqual(commands[1].cliPath, "/ip address add")
|
XCTAssertEqual(commands[1].menuPath, "/ip address")
|
||||||
XCTAssertEqual(commands[1].arguments["interface"], "vlan20")
|
XCTAssertEqual(commands[1].arguments["interface"], "vlan20")
|
||||||
XCTAssertEqual(commands[2].cliPath, "/ip pool add")
|
XCTAssertEqual(commands[2].menuPath, "/ip pool")
|
||||||
XCTAssertEqual(commands[3].cliPath, "/ip dhcp-server add")
|
XCTAssertEqual(commands[3].menuPath, "/ip dhcp-server")
|
||||||
XCTAssertEqual(commands[4].cliPath, "/ip dhcp-server network add")
|
XCTAssertEqual(commands[4].menuPath, "/ip dhcp-server network")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDefaultAddressesAreDerivedFromVlanID() {
|
func testDefaultAddressesAreDerivedFromVlanID() {
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import XCTest
|
||||||
|
@testable import RouterOSAssistant
|
||||||
|
|
||||||
|
final class WifiNetworkConfigTests: XCTestCase {
|
||||||
|
func testDisabledNetworkProducesNoCommands() {
|
||||||
|
let network = WifiNetworkConfig(interfaceName: "wlan1", enabled: false, ssid: "Home", password: "secret123")
|
||||||
|
XCTAssertEqual(network.buildCommands().count, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testEnabledNetworkAddsSecurityProfileThenSetsInterface() {
|
||||||
|
let network = WifiNetworkConfig(interfaceName: "wlan1", enabled: true, ssid: "Home", password: "secret123")
|
||||||
|
let commands = network.buildCommands()
|
||||||
|
|
||||||
|
XCTAssertEqual(commands.count, 2)
|
||||||
|
|
||||||
|
XCTAssertEqual(commands[0].menuPath, "/interface wireless security-profiles")
|
||||||
|
XCTAssertEqual(commands[0].operation, .add)
|
||||||
|
XCTAssertEqual(commands[0].arguments["wpa2-pre-shared-key"], "secret123")
|
||||||
|
|
||||||
|
XCTAssertEqual(commands[1].menuPath, "/interface wireless")
|
||||||
|
XCTAssertEqual(commands[1].operation, .set(matchField: "name", matchValue: "wlan1"))
|
||||||
|
XCTAssertEqual(commands[1].arguments["ssid"], "Home")
|
||||||
|
XCTAssertEqual(commands[1].arguments["security-profile"], "sec_wlan1")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user