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