239 lines
7.9 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|