Files
KayandClaude Sonnet 5 20f8d5e0bf M13: Backup-Wiederherstellung, mit Modell-Schutz und Login-Erhalt
Letzter offener Punkt aus HANDOFF.md: gespeicherte .rsc-Backups lassen
sich jetzt wieder einspielen. Weg: Backup-Datei per SFTP auf den Router
hochladen (Citadel, die bereits eingebundene SSH-Bibliothek, hat einen
SFTP-Client), dann RouterOS' offiziell dokumentierter Restore-Weg in
einem Rutsch: /system reset-configuration no-defaults=yes
run-after-reset=<datei> (kompletter Wipe + sofortiges Wiederanwenden,
sicherer als ein Re-Import auf eine bestehende, andere Config).

Vor dem Bestätigungsdialog wird das Routermodell abgeglichen
(/system routerboard prints "model"-Feld gegen die "# model = ..."-
Kopfzeile der Sicherung) und bei Mismatch komplett blockiert, um ein
Brick-Risiko durch falsches Modell zu vermeiden - der Dialog selbst warnt
zusaetzlich prominent davor.

Ein ernster Bug live gefunden und gefixt: der erste echte Restore-Test
sperrte den Router komplett aus (RouterOS exportiert nie Passwoerter,
no-defaults=yes loescht zusaetzlich den Werks-Admin-Account), nur per
Hardware-Reset behebbar. Fix: das aktuell verwendete App-Login wird jetzt
vorne ins Restore-Skript eingefuegt, noch vor dem eigentlichen
Sicherungsinhalt, da RouterOS den Import beim ersten Fehler irgendwo im
Skript komplett abbricht. Zwei Tests fuer die Escaping-Logik ergaenzt.

Nebenbei: BackupListView mit den neuen Restore-Dialogen liess sich nicht
mehr kompilieren (SwiftUI-Typpruefung timeoutete bei der langen
Modifier-Kette) - Restore-Dialoge in eine eigene @ViewBuilder-Property
ausgelagert.

HANDOFF.md/CHATLOG.md aktualisiert: M13, Bug 18+19, "Backup-
Wiederherstellung fehlt" aus den offenen Punkten entfernt, Hinweis auf
den zweiten (ungewollten) Werksreset waehrend der Session.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CTgRxJTzaQwaRkngbaE1GJ
2026-09-14 15:21:05 +02:00

58 lines
2.4 KiB
Swift

import XCTest
@testable import RouterOSAssistant
final class BackupServiceTests: XCTestCase {
override func tearDown() {
BackupService.customDirectoryURL = nil
super.tearDown()
}
func testCustomDirectoryDefaultsToNil() {
BackupService.customDirectoryURL = nil
XCTAssertNil(BackupService.customDirectoryURL)
}
func testCustomDirectoryRoundTrips() {
let url = URL(fileURLWithPath: "/tmp/RouterOSAssistantTestBackups", isDirectory: true)
BackupService.customDirectoryURL = url
XCTAssertEqual(BackupService.customDirectoryURL?.path, url.path)
}
func testLoginPreservationScriptEscapesQuotesAndBackslashes() {
let script = BackupService.loginPreservationScript(username: "ad\"min", password: "pa\\ss\"word")
XCTAssertTrue(script.contains("name=\"ad\\\"min\""))
XCTAssertTrue(script.contains("password=\"pa\\\\ss\\\"word\""))
}
func testLoginPreservationScriptRecreatesOrRepasswordsCurrentLogin() {
let script = BackupService.loginPreservationScript(username: "admin", password: "secret")
XCTAssertTrue(script.contains("[/user find name=\"admin\"]"))
XCTAssertTrue(script.contains("/user add name=\"admin\" password=\"secret\" group=full"))
XCTAssertTrue(script.contains("/user set [find name=\"admin\"] password=\"secret\""))
}
func testBackupModelParsesHeaderLine() {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("test-backup-\(UUID().uuidString).rsc")
let contents = """
# 2026-09-14 12:00:00 by RouterOS 7.24.2
# software id = TEST-1234
#
# model = RB750Gr3
# serial number = ABC123
/interface bridge add name=bridge
"""
try? contents.write(to: tempURL, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: tempURL) }
XCTAssertEqual(BackupService.backupModel(from: tempURL), "RB750Gr3")
}
func testBackupModelReturnsNilWithoutHeader() {
let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent("test-backup-\(UUID().uuidString).rsc")
try? "/interface bridge add name=bridge".write(to: tempURL, atomically: true, encoding: .utf8)
defer { try? FileManager.default.removeItem(at: tempURL) }
XCTAssertNil(BackupService.backupModel(from: tempURL))
}
}