Files
plezy/test/connection/connection_bootstrap_test.dart
T
2026-08-10 20:28:41 +02:00

244 lines
9.0 KiB
Dart

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<PlexHomeUser> fetchedHomeUsers;
late Map<String, dynamic>? 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<PlexAccountConnection>());
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);
final stored = await registry.list();
expect(stored.length, 1);
expect(stored.single, isA<PlexAccountConnection>());
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<PlexAccountConnection>());
// 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<PlexAccountConnection>());
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<dynamic>;
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<dynamic>;
expect(users.map((u) => (u as Map<String, dynamic>)['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<String, dynamic> _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'},
],
};
}