260 lines
9.5 KiB
Dart
260 lines
9.5 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:plezy/media/ids.dart';
|
|
import 'package:plezy/providers/multi_server_provider.dart';
|
|
import 'package:plezy/services/data_aggregation_service.dart';
|
|
import 'package:plezy/services/multi_server_manager.dart';
|
|
|
|
void main() {
|
|
TestWidgetsFlutterBinding.ensureInitialized();
|
|
|
|
late MultiServerManager manager;
|
|
late DataAggregationService aggregation;
|
|
|
|
setUp(() {
|
|
manager = MultiServerManager();
|
|
aggregation = DataAggregationService(manager);
|
|
});
|
|
|
|
// The provider's dispose() also disposes the manager — only call manager.dispose
|
|
// here in tests where the provider is *not* constructed.
|
|
|
|
group('MultiServerProvider', () {
|
|
test('starts with empty server lists and no live TV', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
expect(p.serverIds, isEmpty);
|
|
expect(p.onlineServerIds, isEmpty);
|
|
expect(p.onlineServerCount, 0);
|
|
expect(p.totalServerCount, 0);
|
|
expect(p.hasConnectedServers, isFalse);
|
|
expect(p.hasLiveTv, isFalse);
|
|
expect(p.liveTvServers, isEmpty);
|
|
p.dispose();
|
|
});
|
|
|
|
test('exposes the injected manager and aggregation service', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
expect(identical(p.serverManager, manager), isTrue);
|
|
expect(identical(p.aggregationService, aggregation), isTrue);
|
|
p.dispose();
|
|
});
|
|
|
|
test('isServerOnline / getClientForServer return defaults for unknown ids', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
expect(p.isServerOnline(ServerId('nope')), isFalse);
|
|
expect(p.getClientForServer(ServerId('nope')), isNull);
|
|
p.dispose();
|
|
});
|
|
|
|
test('liveTvServers getter returns an unmodifiable view', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
// Empty by default; mutating through the unmodifiable view must throw.
|
|
expect(() => p.liveTvServers.clear(), throwsUnsupportedError);
|
|
p.dispose();
|
|
});
|
|
|
|
test('clearAllConnections notifies listeners', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
// disconnectAll() also pushes a status event onto the broadcast stream,
|
|
// which will eventually fire the manager-status listener and notify
|
|
// again. We only assert that the synchronous notifyListeners path runs.
|
|
p.clearAllConnections();
|
|
expect(notified, greaterThanOrEqualTo(1));
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('listens to manager status stream and notifies on change', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
// Push a status change through the manager's public API.
|
|
manager.updateServerStatus(ServerId('srv-1'), true);
|
|
// Give the broadcast stream microtask time to deliver.
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(notified, greaterThanOrEqualTo(1));
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('invokes onOnlineServersChanged with the visibility-filtered online set', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
final calls = <Set<String>>[];
|
|
p.addOnlineServersListener(calls.add);
|
|
|
|
manager.updateServerStatus(ServerId('srv-1'), true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(calls, isNotEmpty);
|
|
expect(calls.last, {'srv-1'});
|
|
|
|
// A server that is online in the manager but outside the active profile's
|
|
// visibility filter must not appear in the payload.
|
|
p.setVisibleServerIds({'srv-1'});
|
|
manager.updateServerStatus(ServerId('srv-2'), true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(calls.last, {'srv-1'}, reason: 'srv-2 is online but filtered out');
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('checkServerHealth with no clients completes without error', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
// Empty clients map → no work, but the call must complete.
|
|
await p.checkServerHealth();
|
|
p.dispose();
|
|
});
|
|
|
|
group('visibility filter', () {
|
|
test('setVisibleServerIds replaces the filter and notifies', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
expect(p.hasExplicitVisibleServerFilter, isFalse);
|
|
|
|
// Empty set is a real value (different from null) — switching from
|
|
// null → {} should notify so consumers know the active profile has
|
|
// no servers, not "all servers".
|
|
p.setVisibleServerIds(<String>{});
|
|
expect(notified, 1);
|
|
expect(p.hasExplicitVisibleServerFilter, isTrue);
|
|
|
|
p.setVisibleServerIds({'a', 'b'});
|
|
expect(notified, 2);
|
|
expect(p.hasExplicitVisibleServerFilter, isTrue);
|
|
|
|
// Idempotent: same membership is a no-op.
|
|
p.setVisibleServerIds({'b', 'a'});
|
|
expect(notified, 2);
|
|
|
|
// Clearing back to null after a real filter is a state change.
|
|
p.setVisibleServerIds(null);
|
|
expect(notified, 3);
|
|
expect(p.hasExplicitVisibleServerFilter, isFalse);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('addToVisibleServerIds initializes filter when null', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
var notified = 0;
|
|
p.addListener(() => notified++);
|
|
|
|
// No prior filter — first add seeds it as a one-element set.
|
|
p.addToVisibleServerIds(ServerId('srv-1'));
|
|
expect(notified, 1);
|
|
|
|
// Build up incrementally.
|
|
p.addToVisibleServerIds(ServerId('srv-2'));
|
|
expect(notified, 2);
|
|
|
|
// Idempotent on already-present ids.
|
|
p.addToVisibleServerIds(ServerId('srv-1'));
|
|
expect(notified, 2);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('onlineServerIds respect the visibility filter', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
// updateServerStatus only populates _serverStatus, not _plexServers, so
|
|
// we exercise the filter via onlineServerIds (which is keyed off
|
|
// status). The serverIds list requires actual server registration
|
|
// which goes through addPlexAccount/addJellyfinConnection — beyond
|
|
// what this unit test needs to cover.
|
|
manager.updateServerStatus(ServerId('srv-1'), true);
|
|
manager.updateServerStatus(ServerId('srv-2'), true);
|
|
manager.updateServerStatus(ServerId('srv-3'), false);
|
|
|
|
// No filter — every online id passes through.
|
|
expect(p.onlineServerIds, containsAll({'srv-1', 'srv-2'}));
|
|
|
|
p.setVisibleServerIds({'srv-1'});
|
|
expect(p.onlineServerIds, ['srv-1']);
|
|
expect(p.isServerOnline(ServerId('srv-2')), isFalse, reason: 'filtered out even when manager reports online');
|
|
|
|
// Empty filter blocks everything — covers the "no connections" path
|
|
// for a freshly-created profile that hasn't borrowed anything yet.
|
|
p.setVisibleServerIds(<String>{});
|
|
expect(p.onlineServerIds, isEmpty);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('setVisibleServerIds immediately hides Live TV servers outside the filter', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
p.debugSetLiveTvServersForTesting([
|
|
LiveTvServerInfo(serverId: 'srv-1', dvrKey: 'dvr-1'),
|
|
LiveTvServerInfo(serverId: 'srv-2', dvrKey: 'dvr-2'),
|
|
]);
|
|
|
|
p.setVisibleServerIds({'srv-1'});
|
|
|
|
expect(p.hasLiveTv, isTrue);
|
|
expect(p.liveTvServers.map((s) => s.serverId), ['srv-1']);
|
|
expect(p.liveTvServers.single.dvrKey, 'dvr-1');
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('setVisibleServerIds empty immediately clears stale Live TV state', () {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
p.debugSetLiveTvServersForTesting([LiveTvServerInfo(serverId: 'srv-1', dvrKey: 'dvr-1')]);
|
|
|
|
p.setVisibleServerIds(<String>{});
|
|
|
|
expect(p.hasLiveTv, isFalse);
|
|
expect(p.liveTvServers, isEmpty);
|
|
|
|
p.dispose();
|
|
});
|
|
|
|
test('expected servers become visible when they reconnect', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
final onlineCalls = <Set<String>>[];
|
|
p.addOnlineServersListener(onlineCalls.add);
|
|
|
|
p.setVisibleServerIds({'srv-1'});
|
|
p.setExpectedVisibleServerIds({'srv-1', 'srv-2'});
|
|
manager.updateServerStatus(ServerId('srv-1'), true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(p.onlineServerIds, ['srv-1']);
|
|
|
|
manager.updateServerStatus(ServerId('srv-2'), true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
|
|
expect(p.onlineServerIds, containsAllInOrder(['srv-1', 'srv-2']));
|
|
expect(p.isServerOnline(ServerId('srv-2')), isTrue);
|
|
expect(onlineCalls.last, {'srv-1', 'srv-2'});
|
|
|
|
p.dispose();
|
|
});
|
|
});
|
|
|
|
test('dispose runs cleanly and cancels the status subscription', () async {
|
|
final p = MultiServerProvider(manager, aggregation);
|
|
|
|
var notifyCount = 0;
|
|
p.addListener(() => notifyCount++);
|
|
|
|
// Sanity: subscription works pre-dispose.
|
|
manager.updateServerStatus(ServerId('a'), true);
|
|
await Future<void>.delayed(Duration.zero);
|
|
expect(notifyCount, greaterThanOrEqualTo(1));
|
|
|
|
// After dispose, no further notifications can be observed because the
|
|
// provider has been disposed AND its subscription is cancelled. We
|
|
// can't even push to the manager (disposed), so we just verify that
|
|
// disposing once doesn't throw.
|
|
expect(p.dispose, returnsNormally);
|
|
});
|
|
});
|
|
}
|