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
This commit is contained in:
@@ -17,4 +17,41 @@ final class BackupServiceTests: XCTestCase {
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user