Files
plezy/test/providers/offline_watch_provider_test.dart
2026-06-27 10:36:21 +02:00

161 lines
6.1 KiB
Dart

import 'package:drift/native.dart';
import 'package:plezy/media/ids.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/providers/download_provider.dart';
import 'package:plezy/providers/offline_watch_provider.dart';
import 'package:plezy/services/download_manager_service.dart';
import 'package:plezy/services/download_storage_service.dart';
import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/offline_watch_sync_service.dart';
import 'package:plezy/services/plex_api_cache.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
late AppDatabase db;
late MultiServerManager serverManager;
late OfflineWatchSyncService syncService;
late DownloadManagerService downloadManager;
late DownloadProvider downloadProvider;
setUp(() async {
db = AppDatabase.forTesting(NativeDatabase.memory());
PlexApiCache.initialize(db);
serverManager = MultiServerManager();
syncService = OfflineWatchSyncService(database: db, serverManager: serverManager);
downloadManager = DownloadManagerService(
database: db,
storageService: DownloadStorageService.instance,
clientResolver: (serverId, {clientScopeId}) => null,
);
downloadManager.recoveryFuture = Future<void>.value();
downloadProvider = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
await downloadProvider.ensureInitialized();
});
tearDown(() async {
downloadProvider.dispose();
downloadManager.dispose();
syncService.dispose();
serverManager.dispose();
await db.close();
});
group('OfflineWatchProvider', () {
test('initial isSyncing reflects sync service state', () {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(p.isSyncing, isFalse);
expect(syncService.isSyncing, isFalse);
p.dispose();
});
test('getPendingSyncCount delegates to sync service (initially 0)', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(await p.getPendingSyncCount(), 0);
p.dispose();
});
test('isWatched returns false when no local action and no metadata', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(await p.isWatched('srv:absent'), isFalse);
p.dispose();
});
test('getViewOffset returns null when no local progress and no metadata', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(await p.getViewOffset('srv:absent'), isNull);
p.dispose();
});
test('getViewOffset returns null for local progress that crossed watched threshold', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
await syncService.queueProgressUpdate(
serverId: ServerId('srv'),
itemId: '42',
viewOffset: 95000,
duration: 100000,
);
expect(await p.isWatched('srv:42'), isTrue);
expect(await p.getViewOffset('srv:42'), isNull);
p.dispose();
});
test('getNextUnwatchedEpisode returns null for show with no downloads', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(await p.getNextUnwatchedEpisode('show-123'), isNull);
p.dispose();
});
test('getEpisodesWithWatchStatus returns empty list for show with no downloads', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
expect(await p.getEpisodesWithWatchStatus('show-123'), isEmpty);
p.dispose();
});
test('forwards listener notifications from sync service', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
var notified = 0;
p.addListener(() => notified++);
// queueMarkWatched on the sync service notifies its listeners; the
// provider's internal listener forwards via safeNotifyListeners.
await syncService.queueMarkWatched(serverId: ServerId('srv'), itemId: '42');
expect(notified, greaterThanOrEqualTo(1));
p.dispose();
});
test('markAsWatched queues an offline action and notifies', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
var notified = 0;
p.addListener(() => notified++);
await p.markAsWatched(serverId: ServerId('srv'), itemId: '50');
// The local watch status now reads as true via the sync service.
expect(await p.isWatched('srv:50'), isTrue);
// At least one notification: from sync service forwarding + provider's
// explicit safeNotifyListeners after queueing.
expect(notified, greaterThanOrEqualTo(1));
p.dispose();
});
test('markAsUnwatched queues an offline action and notifies', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
await p.markAsUnwatched(serverId: ServerId('srv'), itemId: '60');
expect(await p.isWatched('srv:60'), isFalse);
p.dispose();
});
test('dispose removes the sync service listener', () async {
final p = OfflineWatchProvider(syncService: syncService, downloadProvider: downloadProvider);
var notified = 0;
p.addListener(() => notified++);
// Sanity: listener is registered
await syncService.queueMarkWatched(serverId: ServerId('srv'), itemId: '70');
final preDisposeNotifies = notified;
expect(preDisposeNotifies, greaterThanOrEqualTo(1));
p.dispose();
// After dispose, sync service notifications should not call our
// listener (provider unsubscribed). Mutating the sync service post-
// dispose must not throw on the provider side.
await syncService.queueMarkUnwatched(serverId: ServerId('srv'), itemId: '70');
expect(notified, preDisposeNotifies);
});
});
}