import 'dart:convert'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:plezy/connection/connection.dart'; import 'package:plezy/connection/connection_bootstrap.dart'; import 'package:plezy/connection/connection_registry.dart'; import 'package:plezy/database/app_database.dart'; import 'package:plezy/models/plex/plex_home_user.dart'; import 'package:plezy/profiles/profile_registry.dart'; import 'package:plezy/services/server_registry.dart'; import 'package:plezy/services/storage_service.dart'; import '../test_helpers/prefs.dart'; void main() { late AppDatabase db; late ConnectionRegistry registry; late ProfileRegistry profileRegistry; late StorageService storage; late ServerRegistry serverRegistry; late ConnectionBootstrap bootstrap; late List fetchedHomeUsers; late Map? fetchedUserInfo; setUp(() async { resetSharedPreferencesForTest(); db = AppDatabase.forTesting(NativeDatabase.memory()); registry = ConnectionRegistry(db); profileRegistry = ProfileRegistry(db); storage = await StorageService.getInstance(); serverRegistry = ServerRegistry(storage); fetchedHomeUsers = const []; fetchedUserInfo = null; bootstrap = ConnectionBootstrap( storage: storage, connectionRegistry: registry, serverRegistry: serverRegistry, profileRegistry: profileRegistry, plexHomeUserFetcher: (_) async => fetchedHomeUsers, plexUserInfoFetcher: (_) async => fetchedUserInfo ?? (throw StateError('user info unavailable')), ); }); tearDown(() async { await db.close(); }); group('ConnectionBootstrap.migrateLegacyPlexAccount', () { test('run leaves fresh installs without a placeholder local profile', () async { await bootstrap.run(); expect(await profileRegistry.list(), isEmpty); expect(storage.getActiveProfileId(), isNull); expect(storage.prefs.getBool('profile_migration_v1_done'), isTrue); }); test('returns null when no legacy Plex token is stored', () async { final result = await bootstrap.migrateLegacyPlexAccount(); expect(result, isNull); expect(await registry.list(), isEmpty); }); test('migrates a stored Plex token into a PlexAccountConnection', () async { await storage.prefs.setString('plex_token', 'legacy-token-abc'); final result = await bootstrap.migrateLegacyPlexAccount(); expect(result, isA()); expect(result!.accountToken, 'legacy-token-abc'); // The clientIdentifier comes from StorageService — non-empty after // first call to getOrCreateClientIdentifier. expect(result.clientIdentifier, isNotEmpty); // Account label falls back to "Plex" when the user-info call fails // (no network in the test environment). expect(result.accountLabel, isNotEmpty); // The migrated row is now in the registry. final stored = await registry.list(); expect(stored.length, 1); expect(stored.single, isA()); expect((stored.single as PlexAccountConnection).accountToken, 'legacy-token-abc'); }); test('keeps the legacy plex_token until full bootstrap succeeds', () async { await storage.prefs.setString('plex_token', 'legacy-token-xyz'); final first = await bootstrap.migrateLegacyPlexAccount(); final second = await bootstrap.migrateLegacyPlexAccount(); expect(first, isA()); // The token is cleared only after run() has also hydrated/selects a // virtual Plex profile. Until then, keeping it makes failed profile // hydration retryable. expect(second, isA()); expect(storage.prefs.getString('plex_token'), 'legacy-token-xyz'); expect((await registry.list()).length, 1); }); test('preserves stable id derived from the device clientIdentifier', () async { await storage.prefs.setString('plex_token', 'legacy-token-1'); final clientId = await storage.getOrCreateClientIdentifier(); final migrated = await bootstrap.migrateLegacyPlexAccount(); expect(migrated!.id, 'plex.$clientId'); }); test('uses Plex account UUID for migrated connection id when available', () async { await storage.prefs.setString('plex_token', 'legacy-token-uuid'); await storage.prefs.setString('client_identifier', 'device-client'); fetchedUserInfo = {'uuid': 'account-uuid-1', 'username': 'edde'}; final migrated = await bootstrap.migrateLegacyPlexAccount(); expect(migrated!.id, 'plex.account-uuid-1'); expect(migrated.clientIdentifier, 'device-client'); expect(migrated.accountLabel, 'edde'); final stored = await registry.list(); expect(stored.single.id, 'plex.account-uuid-1'); }); test('run promotes legacy Plex Home UUID and copies user cache to connection scope', () async { await storage.prefs.setString('plex_token', 'legacy-token-home'); await storage.prefs.setString('client_identifier', 'client-1'); await storage.prefs.setString('servers_list', json.encode([_legacyPlexServerJson(accessToken: 'server-token')])); await storage.prefs.setString('current_user_uuid', 'home-user-1'); await storage.prefs.setString( 'home_users_cache', json.encode({ 'id': 1, 'name': 'Home', 'guestUserID': null, 'guestUserUUID': '', 'guestEnabled': false, 'subscription': true, 'users': [ { 'id': 10, 'uuid': 'home-user-1', 'title': 'Kid', 'thumb': '', 'hasPassword': false, 'restricted': false, 'updatedAt': null, 'admin': false, 'guest': false, 'protected': false, }, ], }), ); await bootstrap.run(); expect(storage.getActiveProfileId(), 'plex-home-plex.client-1-home-user-1'); expect(storage.prefs.getString('plex_token'), isNull); expect(storage.prefs.getString('servers_list'), isNull); expect(storage.prefs.getString('current_user_uuid'), isNull); expect(storage.prefs.getString('home_users_cache'), isNull); final migrated = storage.getPlexHomeUsersCacheJson('plex.client-1'); expect(migrated, isNotNull); final users = json.decode(migrated!) as List; expect(users.single, containsPair('uuid', 'home-user-1')); }); test('run selects fetched Plex Home admin as virtual active profile when no legacy UUID exists', () async { await storage.prefs.setString('plex_token', 'legacy-owner-token'); await storage.prefs.setString('client_identifier', 'client-owner'); fetchedHomeUsers = [ PlexHomeUser( id: 10, uuid: 'managed-user', title: 'Managed', thumb: '', hasPassword: false, restricted: true, updatedAt: null, admin: false, guest: false, protected: false, ), PlexHomeUser( id: 1, uuid: 'admin-user', title: 'Owner', thumb: '', hasPassword: false, restricted: false, updatedAt: null, admin: true, guest: false, protected: false, ), ]; await bootstrap.run(); expect(await profileRegistry.list(), isEmpty); expect(storage.getActiveProfileId(), 'plex-home-plex.client-owner-admin-user'); final cached = storage.getPlexHomeUsersCacheJson('plex.client-owner'); expect(cached, isNotNull); final users = json.decode(cached!) as List; expect(users.map((u) => (u as Map)['uuid']), containsAll(['managed-user', 'admin-user'])); }); test('run clears leftover legacy servers_list when migration was already marked done', () async { await storage.prefs.setBool('profile_migration_v1_done', true); await storage.prefs.setString( 'servers_list', json.encode([_legacyPlexServerJson(accessToken: 'plain-server-token')]), ); await bootstrap.run(); expect(storage.prefs.getString('servers_list'), isNull); }); test('run retries later when Plex Home profiles cannot be hydrated', () async { await storage.prefs.setString('plex_token', 'legacy-owner-token'); await storage.prefs.setString('client_identifier', 'client-owner'); await bootstrap.run(); expect(storage.prefs.getBool('profile_migration_v1_done'), isNull); expect(storage.prefs.getString('plex_token'), 'legacy-owner-token'); expect(storage.getActiveProfileId(), isNull); expect(await registry.list(), isEmpty); expect(await profileRegistry.list(), isEmpty); }); }); } Map _legacyPlexServerJson({required String accessToken}) { return { 'name': 'Plex', 'clientIdentifier': 'server-1', 'accessToken': accessToken, 'owned': true, 'connections': [ {'protocol': 'http', 'address': '127.0.0.1', 'port': 32400, 'uri': 'http://127.0.0.1:32400'}, ], }; }