fix(jellyfin): prevent false offline loops

This commit is contained in:
edde746
2026-07-13 11:30:17 +02:00
parent e4db04fa62
commit f104b5ffae
4 changed files with 173 additions and 17 deletions
@@ -793,6 +793,60 @@ void main() {
captured.where((uri) => uri.path == '/Users/user-1/Items/Latest').map((uri) => uri.queryParameters['ParentId']),
['movies', 'mv', 'home-vids', 'music'],
);
expect(
captured.where((uri) => uri.path == '/Items' && uri.queryParameters['Filters'] == 'IsPlayed'),
isEmpty,
reason: 'the home screen excludes playback-derived music rows',
);
});
test('music library recommendations retain recently and most-played rows', () async {
final captured = <Uri>[];
final client = JellyfinClient.forTesting(
connection: _conn(),
httpClient: MockClient((req) async {
captured.add(req.url);
if (req.url.path == '/Users/user-1/Items/Latest') {
return _json([
{'Id': 'album-1', 'Type': 'MusicAlbum', 'Name': 'Latest Album', 'ParentLibraryId': 'music'},
]);
}
if (req.url.path == '/Items' && req.url.queryParameters['Filters'] == 'IsPlayed') {
final sortBy = req.url.queryParameters['SortBy'];
return _json({
'Items': [
{
'Id': sortBy == 'DatePlayed' ? 'recent-track' : 'most-played-track',
'Type': 'Audio',
'Name': sortBy == 'DatePlayed' ? 'Recent Track' : 'Most Played Track',
'ParentLibraryId': 'music',
},
],
});
}
return http.Response('unexpected request', 500);
}),
);
addTearDown(client.close);
final hubs = await client.fetchLibraryHubs(
'music',
libraryName: 'Music',
libraryKind: MediaKind.artist,
includePlaybackHubs: true,
);
expect(hubs.map((hub) => hub.identifier), [
'library.music.recent',
'library.music.recentlyplayed',
'library.music.mostplayed',
]);
expect(
captured
.where((uri) => uri.path == '/Items' && uri.queryParameters['Filters'] == 'IsPlayed')
.map((uri) => uri.queryParameters['SortBy']),
['DatePlayed', 'PlayCount'],
);
});
test('Plex home layout keeps promoted hubs instead of splitting by preview libraries', () async {
@@ -148,6 +148,53 @@ void main() {
});
});
group('endpoint exhaustion verification', () {
test('content-route exhaustion keeps an authenticated Jellyfin server online', () async {
final manager = MultiServerManager();
addTearDown(manager.dispose);
final client = testJellyfinClient(
connection: _jellyfinConnection('user-a'),
handler: (_) async =>
http.Response('{"Policy":{"IsAdministrator":false}}', 200, headers: {'content-type': 'application/json'}),
);
manager.debugRegisterJellyfinClientForTesting(client);
final emitted = <Map<String, bool>>[];
final sub = manager.statusStream.listen(emitted.add);
addTearDown(sub.cancel);
await manager.debugVerifyServerEndpointsExhaustedForTesting(ServerId('jf-machine'));
await Future<void>.delayed(Duration.zero);
expect(manager.isServerOnline(ServerId('jf-machine')), isTrue);
expect(manager.authErrorServerIds, isEmpty);
expect(emitted, isEmpty, reason: 'a successful health probe must not publish a false offline transition');
});
test('auth rejection is published without attempting generic reconnection', () async {
final manager = MultiServerManager();
addTearDown(manager.dispose);
final client = testJellyfinClient(
connection: _jellyfinConnection('user-a'),
handler: (_) async => http.Response('', 401),
);
manager.debugRegisterJellyfinClientForTesting(client);
final emitted = <Map<String, bool>>[];
final sub = manager.statusStream.listen(emitted.add);
addTearDown(sub.cancel);
await manager.debugVerifyServerEndpointsExhaustedForTesting(ServerId('jf-machine'));
await Future<void>.delayed(Duration.zero);
expect(manager.isServerOnline(ServerId('jf-machine')), isFalse);
expect(manager.authErrorServerIds, {'jf-machine'});
expect(emitted, [
{'jf-machine': false},
]);
});
});
group('refreshTokensForProfile', () {
test('successful in-place Plex token refresh clears auth-error state', () async {
final db = AppDatabase.forTesting(NativeDatabase.memory());