Files
plezy/test/profiles/plex_home_service_test.dart
T

239 lines
7.9 KiB
Dart

import 'dart:async';
import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/connection/connection.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/plex_home_service.dart';
import 'package:plezy/profiles/profile_connection_registry.dart';
import 'package:plezy/services/storage_service.dart';
import '../test_helpers/prefs.dart';
PlexHomeUser _user(String uuid, {bool admin = false, bool protected = false, String name = 'User'}) {
return PlexHomeUser(
id: 0,
uuid: uuid,
title: name,
username: null,
email: null,
friendlyName: null,
thumb: 'https://plex.tv/users/$uuid/avatar',
hasPassword: false,
restricted: false,
updatedAt: null,
admin: admin,
guest: false,
protected: protected,
);
}
PlexAccountConnection _account(String id) {
return PlexAccountConnection(
id: id,
accountToken: 'tok-$id',
clientIdentifier: 'cid-$id',
accountLabel: 'acct-$id',
createdAt: DateTime(2026, 1, 1),
);
}
void main() {
late AppDatabase db;
late ConnectionRegistry connections;
late ProfileConnectionRegistry profileConnections;
late StorageService storage;
late PlexHomeService service;
setUp(() async {
resetSharedPreferencesForTest();
db = AppDatabase.forTesting(NativeDatabase.memory());
connections = ConnectionRegistry(db);
profileConnections = ProfileConnectionRegistry(db);
storage = await StorageService.getInstance();
});
tearDown(() async {
await service.dispose();
await db.close();
});
group('PlexHomeService', () {
test('refresh fetches and caches users for a connection', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => [_user('admin-uuid', admin: true), _user('kid-uuid', protected: true)],
);
final acct = _account('plex.dev1');
await connections.upsert(acct);
await service.refresh(acct);
expect(service.current[acct.id], hasLength(2));
expect(service.current[acct.id]!.firstWhere((u) => u.admin).uuid, 'admin-uuid');
});
test('refresh persists users to SharedPreferences', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => [_user('uuid-1')],
);
final acct = _account('plex.dev2');
await connections.upsert(acct);
await service.refresh(acct);
expect(storage.getPlexHomeUsersCacheJson(acct.id), isNotNull);
});
test('start hydrates the cache from SharedPreferences', () async {
// Pre-seed the cache.
await storage.savePlexHomeUsersCache('plex.dev3', [_user('seeded-uuid').toJson()]);
final acct = _account('plex.dev3');
await connections.upsert(acct);
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => const [], // background refresh returns empty
);
await service.start();
// Cache hydrated synchronously from SharedPreferences before any
// background fetch resolves.
expect(service.current[acct.id], hasLength(1));
expect(service.current[acct.id]!.first.uuid, 'seeded-uuid');
});
test('reloadFromStorage picks up caches written after startup', () async {
final refreshBlocker = Completer<List<PlexHomeUser>>();
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) => refreshBlocker.future,
);
addTearDown(() {
if (!refreshBlocker.isCompleted) refreshBlocker.complete(const []);
});
await service.start();
expect(service.current, isEmpty);
final acct = _account('plex.migrated');
await connections.upsert(acct);
await storage.savePlexHomeUsersCache(acct.id, [_user('migrated-home-user').toJson()]);
await service.reloadFromStorage();
expect(service.current[acct.id], hasLength(1));
expect(service.current[acct.id]!.single.uuid, 'migrated-home-user');
});
test('concurrent start calls await the same in-flight startup', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => const [],
);
final first = service.start();
final second = service.start();
expect(identical(first, second), isTrue);
await second;
});
test('removing a Plex connection clears its cache slot', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => [_user('uuid-1')],
);
final acct = _account('plex.dev4');
await connections.upsert(acct);
await service.refresh(acct);
expect(service.current[acct.id], isNotNull);
await service.start();
// Wait for the service's own stream to emit a snapshot without
// `acct.id` instead of a fixed-duration sleep — deterministic on slow
// CI runners and matches when the listener actually settles, not just
// 30ms after the remove() future resolves.
final cleared = expectLater(
service.stream,
emitsThrough(predicate<Map<String, List<PlexHomeUser>>>((m) => !m.containsKey(acct.id))),
);
await connections.remove(acct.id);
await cleared;
expect(service.current[acct.id], isNull);
expect(storage.getPlexHomeUsersCacheJson(acct.id), isNull);
});
test('materializeFirstPlexHome wraps the first cached account', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => [
_user('admin-uuid', admin: true, name: 'Admin'),
_user('kid-uuid', name: 'Kid'),
],
);
final acct = _account('plex.dev5');
await connections.upsert(acct);
await service.refresh(acct);
final home = await service.materializeFirstPlexHome();
expect(home, isNotNull);
expect(home!.users, hasLength(2));
expect(home.adminUser?.uuid, 'admin-uuid');
});
test('materializeFirstPlexHome waits for startup cache hydration', () async {
await storage.savePlexHomeUsersCache('plex.dev-cached', [_user('cached-admin', admin: true).toJson()]);
final acct = _account('plex.dev-cached');
await connections.upsert(acct);
final refreshBlocker = Completer<List<PlexHomeUser>>();
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) => refreshBlocker.future,
);
addTearDown(() {
if (!refreshBlocker.isCompleted) refreshBlocker.complete(const []);
});
final home = await service.materializeFirstPlexHome();
expect(home, isNotNull);
expect(home!.adminUser?.uuid, 'cached-admin');
});
test('clearAll wipes both memory and disk caches', () async {
service = PlexHomeService(
connections: connections,
profileConnections: profileConnections,
storage: storage,
plexHomeUserFetcher: (_) async => [_user('uuid-1')],
);
final acct = _account('plex.dev6');
await connections.upsert(acct);
await service.refresh(acct);
await service.clearAll();
expect(service.current, isEmpty);
expect(storage.getPlexHomeUsersCacheJson(acct.id), isNull);
});
});
}