Files
RouterOS/RouterOSAssistantTests/SavedRoutersStoreTests.swift
T
KayandClaude Sonnet 5 9de9311bee M26+M27: Seriennummer bei Bekannte Router, Schlüsselbund-Trennung, terse-Fallback verallgemeinert
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>
2026-09-16 14:02:27 +02:00

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