forked from kay/RouterOS
M26: SavedRouter bekommt ein optionales serialNumber-Feld. Zwei physisch verschiedene Router mit identischem Host+Benutzername (Werks-Adresse) blieben bisher ein gemeinsamer "Bekannte Router"- Eintrag; recordSuccessfulConnection matcht jetzt zusätzlich nach Seriennummer, mit sauberer Migration bestehender Einträge ohne Seriennummer. Drei neue Tests. M27, beim Live-Test von M26 gefunden: - Passwort-Anzeige-Button (Augen-Symbol) im Verbinden-Tab - Bug 35: KeychainService speicherte Passwörter nur nach "username@host" — beide Router teilten sich denselben Schlüsselbund-Eintrag trotz getrennter SavedRouter-Einträge. Fix: optionaler serialNumber-Parameter qualifiziert den Account-Key, mit Fallback auf den alten Key für bereits gespeicherte Passwörter. - Bug 36: neuer Router zeigte keine Routerboard-Infos/Seriennummer — derselbe Root Cause wie der zuvor gemeldete "Jetzt prüfen"-Fehler: /system routerboard und /system package update scheitern auf diesem Router mit "expected end of command" statt dem bisher einzig abgefangenen "bad parameter terse". SSHTransport.fetchMenuItems erkennt jetzt beide Formulierungen. Alles live bestätigt. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
129 lines
6.1 KiB
Swift
129 lines
6.1 KiB
Swift
import XCTest
|
|
@testable import RouterOSAssistant
|
|
|
|
final class SavedRoutersStoreTests: XCTestCase {
|
|
/// A dedicated suite name per test avoids bleeding state between tests / real app defaults.
|
|
private func makeStore() -> SavedRoutersStore {
|
|
let suiteName = "SavedRoutersStoreTests.\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
return SavedRoutersStore(defaults: defaults)
|
|
}
|
|
|
|
func testFirstSuccessfulConnectionAddsEntryWithDefaultName() {
|
|
let store = makeStore()
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX")
|
|
|
|
XCTAssertEqual(routers.count, 1)
|
|
XCTAssertEqual(routers[0].host, "192.168.88.1")
|
|
XCTAssertEqual(routers[0].username, "admin")
|
|
XCTAssertEqual(routers[0].name, "hEX")
|
|
XCTAssertEqual(routers[0].location, "")
|
|
}
|
|
|
|
func testUpdateLocationRoundTrip() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX")
|
|
let id = store.load()[0].id
|
|
|
|
let updated = store.updateLocation(id, to: "Keller, Serverschrank")
|
|
XCTAssertEqual(updated.first?.location, "Keller, Serverschrank")
|
|
// Renaming afterwards must not clobber the location, and vice versa.
|
|
let renamed = store.rename(id, to: "Hauptrouter")
|
|
XCTAssertEqual(renamed.first?.name, "Hauptrouter")
|
|
XCTAssertEqual(renamed.first?.location, "Keller, Serverschrank")
|
|
}
|
|
|
|
/// A list saved by an earlier app version (before `location` existed) must keep loading
|
|
/// instead of the whole list silently vanishing — see `SavedRouter.init(from:)`.
|
|
func testDecodingAnEntryWithoutALocationFieldDefaultsToEmpty() {
|
|
let suiteName = "SavedRoutersStoreTests.\(UUID().uuidString)"
|
|
let defaults = UserDefaults(suiteName: suiteName)!
|
|
let legacyJSON = """
|
|
[{"id":"\(UUID().uuidString)","host":"192.168.88.1","username":"admin","name":"hEX","lastConnectedAt":0}]
|
|
"""
|
|
defaults.set(Data(legacyJSON.utf8), forKey: "RouterOSAssistant.SavedRouters")
|
|
|
|
let store = SavedRoutersStore(defaults: defaults)
|
|
let routers = store.load()
|
|
|
|
XCTAssertEqual(routers.count, 1)
|
|
XCTAssertEqual(routers[0].name, "hEX")
|
|
XCTAssertEqual(routers[0].location, "")
|
|
}
|
|
|
|
func testReconnectingToKnownRouterUpdatesRecencyNotName() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX")
|
|
store.rename(store.load()[0].id, to: "Büro-Router")
|
|
|
|
// A later connection reports a possibly-different board name (e.g. after a hardware
|
|
// swap behind the same IP) — the user's own rename must survive regardless.
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "RB750Gr3")
|
|
|
|
XCTAssertEqual(routers.count, 1)
|
|
XCTAssertEqual(routers[0].name, "Büro-Router")
|
|
}
|
|
|
|
func testDifferentUsernameOnSameHostIsATwoEntries() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX")
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "gast", defaultName: "hEX")
|
|
|
|
XCTAssertEqual(routers.count, 2)
|
|
}
|
|
|
|
func testRenameAndRemoveRoundTrip() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX")
|
|
let id = store.load()[0].id
|
|
|
|
let renamed = store.rename(id, to: "Wohnzimmer")
|
|
XCTAssertEqual(renamed.first?.name, "Wohnzimmer")
|
|
|
|
let removed = store.remove(id)
|
|
XCTAssertTrue(removed.isEmpty)
|
|
XCTAssertTrue(store.load().isEmpty)
|
|
}
|
|
|
|
/// Live-confirmed (2026-09-16): a second, same-model router left at MikroTik's factory
|
|
/// default (192.168.88.1/admin) previously just renamed the *existing* entry from the first
|
|
/// router in place, with no way to tell the two physical devices apart afterwards.
|
|
func testDifferentSerialAtSameHostUsernameCreatesSeparateEntry() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: "AAA111")
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: "BBB222")
|
|
|
|
XCTAssertEqual(routers.count, 2)
|
|
}
|
|
|
|
/// A router saved before `serialNumber` existed (or from a device with no routerboard) has
|
|
/// no recorded serial yet — the next connection that DOES report one should fill it in on
|
|
/// the same entry rather than creating a duplicate.
|
|
func testLegacyEntryWithoutSerialGetsUpgradedInPlaceNotDuplicated() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: nil)
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: "AAA111")
|
|
|
|
XCTAssertEqual(routers.count, 1)
|
|
XCTAssertEqual(routers[0].serialNumber, "AAA111")
|
|
}
|
|
|
|
func testSameSerialReconnectUpdatesRecencyNotDuplicated() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: "AAA111")
|
|
let routers = store.recordSuccessfulConnection(host: "192.168.88.1", username: "admin", defaultName: "hEX", serialNumber: "AAA111")
|
|
|
|
XCTAssertEqual(routers.count, 1)
|
|
}
|
|
|
|
func testLoadOrdersMostRecentlyConnectedFirst() {
|
|
let store = makeStore()
|
|
store.recordSuccessfulConnection(host: "10.0.0.1", username: "admin", defaultName: "Router A")
|
|
store.recordSuccessfulConnection(host: "10.0.0.2", username: "admin", defaultName: "Router B")
|
|
// Reconnecting to the first one should move it back to the front.
|
|
let routers = store.recordSuccessfulConnection(host: "10.0.0.1", username: "admin", defaultName: "Router A")
|
|
|
|
XCTAssertEqual(routers.first?.host, "10.0.0.1")
|
|
}
|
|
}
|