// The multi-server registry: which server the app reads from, and the session // held for each. // // What is worth guarding is that the servers stay separate — one server's // session is never handed to another, and dropping one never takes the rest // with it — and that an upgrade from the single-server build carries the // session and the address it was pointed at onto the home entry rather than // signing the owner out. import "dart:convert"; import "package:flutter_test/flutter_test.dart"; import "package:shared_preferences/shared_preferences.dart"; import "package:drivervault_phone/config.dart"; import "package:drivervault_phone/i18n.dart"; import "package:drivervault_phone/models.dart"; import "package:drivervault_phone/servers.dart"; AuthUser _user(String email) => AuthUser(id: "u1", email: email, name: "Owner"); Future _loaded(Map stored) async { SharedPreferences.setMockInitialValues(stored); final registry = ServerRegistry(); await registry.load(); return registry; } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() async { await loadTranslations(); }); group("normalizeServerUrl", () { test("a bare origin gets the /api every route lives under", () { expect(normalizeServerUrl("https://garage.example.com"), "https://garage.example.com/api"); expect(normalizeServerUrl("https://garage.example.com/"), "https://garage.example.com/api"); }); test("an address that already names a path is taken as given", () { expect(normalizeServerUrl("https://example.com/api"), "https://example.com/api"); expect(normalizeServerUrl("https://example.com/dv/api/"), "https://example.com/dv/api"); }); test("blank stays blank — that is what means 'the default'", () { expect(normalizeServerUrl(" "), ""); }); }); group("a fresh install", () { test("knows one server, unnamed, answering on the built-in address", () async { final registry = await _loaded({}); expect(registry.list.length, 1); expect(registry.activeId, kHomeServerId); expect(registry.activeBase, kDefaultApiBase); expect(registry.isConnected(kHomeServerId), isFalse); expect(registry.displayName(registry.active), t("servers.home")); }); }); group("upgrading from the single-server build", () { test("the session and the address it was pointed at land on home", () async { final registry = await _loaded({ "cc_token": "old-token", "cc_user": jsonEncode(_user("owner@example.com").toJson()), "cc_server_url": "https://garage.example.com", }); expect(registry.activeId, kHomeServerId); expect(registry.activeToken, "old-token"); expect(registry.activeSession?.user?.email, "owner@example.com"); expect(registry.activeBase, "https://garage.example.com/api"); // The keys they came from are gone, so a later boot cannot resurrect a // session that has since been signed out of. final prefs = await SharedPreferences.getInstance(); expect(prefs.getString("cc_token"), isNull); expect(prefs.getString("cc_server_url"), isNull); }); }); group("several servers", () { test("a session belongs to the one server it was minted by", () async { final registry = await _loaded({}); registry.setSession(kHomeServerId, "home-token", _user("home@example.com")); final other = registry.add(name: "Work", url: "https://work.example.com"); expect(registry.isConnected(other.id), isFalse); // Switching swaps the whole session, not just the address. registry.setActive(other.id); expect(registry.activeBase, "https://work.example.com/api"); expect(registry.activeToken, isNull); registry.setSession(other.id, "work-token", _user("work@example.com")); expect(registry.activeToken, "work-token"); expect(registry.sessionFor(kHomeServerId)?.token, "home-token"); }); test("moving one to a new address drops the token the old one minted", () async { final registry = await _loaded({}); final server = registry.add(url: "https://work.example.com"); registry.setSession(server.id, "work-token", _user("work@example.com")); registry.update(server.id, name: "Work"); expect(registry.isConnected(server.id), isTrue, reason: "a rename is not a move"); registry.update(server.id, url: "https://elsewhere.example.com"); expect(registry.isConnected(server.id), isFalse); }); test("removing the active one falls back to home, and leaves it signed in", () async { final registry = await _loaded({}); registry.setSession(kHomeServerId, "home-token", _user("home@example.com")); final other = registry.add(url: "https://work.example.com"); registry.setSession(other.id, "work-token", _user("work@example.com")); registry.setActive(other.id); registry.remove(other.id); expect(registry.activeId, kHomeServerId); expect(registry.activeToken, "home-token"); expect(registry.list.length, 1); }); test("home cannot be removed — there would be nothing to fall back to", () async { final registry = await _loaded({}); registry.remove(kHomeServerId); expect(registry.list.length, 1); expect(registry.byId(kHomeServerId), isNotNull); }); test("what they are called: the name, else the host, else the default label", () async { final registry = await _loaded({}); expect(registry.displayName(registry.add(name: "Home garage", url: "https://a.example.com")), "Home garage"); expect(registry.displayName(registry.add(url: "https://b.example.com")), "b.example.com"); expect(registry.displayName(registry.byId(kHomeServerId)), t("servers.home")); }); }); group("what a later boot reads back", () { test("the list, the active one and every session survive a restart", () async { final registry = await _loaded({}); registry.setSession(kHomeServerId, "home-token", _user("home@example.com")); final other = registry.add(name: "Work", url: "https://work.example.com"); registry.setSession(other.id, "work-token", _user("work@example.com")); registry.setActive(other.id); final rebooted = ServerRegistry(); await rebooted.load(); expect(rebooted.list.length, 2); expect(rebooted.activeId, other.id); expect(rebooted.activeToken, "work-token"); expect(rebooted.activeSession?.user?.email, "work@example.com"); expect(rebooted.displayName(rebooted.active), "Work"); expect(rebooted.sessionFor(kHomeServerId)?.token, "home-token"); }); }); }