Files
plezy/test/services/jellyfin_favorites_isolation_test.dart
T

98 lines
3.8 KiB
Dart

import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/testing.dart';
import 'package:plezy/connection/connection.dart';
import 'package:plezy/models/livetv_channel.dart';
import 'package:plezy/services/favorite_channels_repository.dart';
import 'package:plezy/services/jellyfin_client.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../test_helpers/backend_client_fixtures.dart';
import '../test_helpers/prefs.dart';
JellyfinConnection _conn({required String userId}) => testJellyfinConnection(
machineId: 'srv-shared',
userId: userId,
serverName: 'Shared JF',
userName: 'user-$userId',
accessToken: 'tok-$userId',
deviceId: 'dev-$userId',
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
);
JellyfinClient _client(JellyfinConnection conn, {FavoriteChannelsRepository? favoritesRepository}) =>
JellyfinClient.forTesting(
connection: conn,
favoritesRepository: favoritesRepository,
// Favorites read path is local-only; any HTTP call is a test failure.
httpClient: MockClient((_) async => throw StateError('no HTTP expected')),
);
String _favKey(JellyfinConnection conn) => 'jellyfin_fav_channels:${conn.id}';
String _legacyFavKey(JellyfinConnection conn) => 'jellyfin_fav_channels:${conn.serverMachineId}';
String _encodeFavorites(List<FavoriteChannel> favs) => jsonEncode(favs.map((f) => f.toJson()).toList());
FavoriteChannel _ch(String id, {String? title}) =>
FavoriteChannel(id: id, title: title ?? id, source: 'server://srv-shared/jellyfin');
void main() {
setUp(resetSharedPreferencesForTest);
group('Jellyfin favorites isolation across users on the same server', () {
test('two users on the same server have independent favorite lists', () async {
final connA = _conn(userId: 'user-a');
final connB = _conn(userId: 'user-b');
// Pre-seed user A's favorites under the compound key, leave user B empty.
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_favKey(connA), _encodeFavorites([_ch('chan-a1'), _ch('chan-a2')]));
final aFavs = await _client(connA).liveTv.fetchFavoriteChannels();
final bFavs = await _client(connB).liveTv.fetchFavoriteChannels();
expect(aFavs.map((f) => f.id).toList(), ['chan-a1', 'chan-a2']);
expect(bFavs, isEmpty, reason: 'user B must not see user A\'s favorites');
});
test('legacy bare-machineId key migrates into the first user\'s compound slot', () async {
final connA = _conn(userId: 'user-a');
// Pre-seed legacy entry only.
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_legacyFavKey(connA), _encodeFavorites([_ch('legacy-1')]));
final aFavs = await _client(connA).liveTv.fetchFavoriteChannels();
expect(aFavs.map((f) => f.id).toList(), ['legacy-1']);
// Migration: legacy key gone, compound key written.
expect(prefs.getString(_legacyFavKey(connA)), isNull);
expect(prefs.getString(_favKey(connA)), isNotNull);
});
test('repository read failures propagate through the favorite Future', () async {
final failure = StateError('favorite repository unavailable');
final client = _client(
_conn(userId: 'user-a'),
favoritesRepository: _ThrowingFavoriteChannelsRepository(failure),
);
addTearDown(client.close);
await expectLater(client.liveTv.fetchFavoriteChannels(), throwsA(same(failure)));
});
});
}
class _ThrowingFavoriteChannelsRepository implements FavoriteChannelsRepository {
const _ThrowingFavoriteChannelsRepository(this.failure);
final Object failure;
@override
Future<List<FavoriteChannel>> read({required String key, required String legacyKey}) => Future.error(failure);
@override
Future<void> write(String key, List<FavoriteChannel> channels) async {}
}