| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- import 'package:cardash_launcher/data/models/jellyfin_models.dart';
- import 'package:cardash_launcher/data/repositories/settings_repository.dart';
- import 'package:cardash_launcher/data/services/jellyfin_service.dart';
- import 'package:cardash_launcher/data/services/secure_store.dart';
- import 'package:cardash_launcher/features/settings/jellyfin_controller.dart';
- import 'package:flutter_test/flutter_test.dart';
- import 'package:shared_preferences/shared_preferences.dart';
- class _MemoryStore implements SecureStore {
- final values = <String, String>{};
- @override
- Future<String?> read(String key) async => values[key];
- @override
- Future<void> write(String key, String value) async {
- values[key] = value;
- }
- @override
- Future<void> delete(String key) async {
- values.remove(key);
- }
- }
- class _FakeGateway implements JellyfinGateway {
- bool unreachable = false;
- bool approved = false;
- bool rejectToken = false;
- int initiateCalls = 0;
- int userCalls = 0;
- String? lastDeviceId;
- @override
- Future<JellyfinServerInfo> serverInfo(Uri baseUrl) async {
- if (unreachable) {
- throw JellyfinUnreachable('Could not reach the server.');
- }
- return const JellyfinServerInfo(name: 'DarkTower', version: '12.2.0');
- }
- @override
- Future<JellyfinQuickConnect> startQuickConnect(
- Uri baseUrl, {
- required String deviceId,
- }) async {
- if (unreachable) {
- throw JellyfinUnreachable('Could not reach the server.');
- }
- initiateCalls++;
- lastDeviceId = deviceId;
- return const JellyfinQuickConnect(code: '491006', secret: 'secret');
- }
- @override
- Future<bool> isQuickConnectApproved(Uri baseUrl, String secret) async {
- if (unreachable) {
- throw JellyfinUnreachable('Could not reach the server.');
- }
- return approved;
- }
- @override
- Future<JellyfinSession> authenticateQuickConnect(
- Uri baseUrl,
- String secret, {
- required String deviceId,
- }) async {
- return const JellyfinSession(
- accessToken: 'token-1',
- userId: 'user-1',
- userName: 'wasp',
- );
- }
- @override
- Future<JellyfinUser> currentUser(Uri baseUrl, String token) async {
- userCalls++;
- if (unreachable) {
- throw JellyfinUnreachable('Could not reach the server.');
- }
- if (rejectToken) {
- throw JellyfinAuthException('The saved Jellyfin pairing is not valid.');
- }
- return const JellyfinUser(id: 'user-1', name: 'wasp');
- }
- @override
- void close() {}
- }
- Future<SettingsRepository> _settings([
- Map<String, Object> values = const {},
- ]) async {
- SharedPreferences.setMockInitialValues(values);
- final prefs = await SharedPreferences.getInstance();
- return SettingsRepository(prefs);
- }
- void main() {
- TestWidgetsFlutterBinding.ensureInitialized();
- test('enabling without a URL waits for setup', () async {
- final settings = await _settings();
- final gateway = _FakeGateway();
- final controller = JellyfinController(
- settings,
- gateway,
- _MemoryStore(),
- pollInterval: const Duration(milliseconds: 10),
- retryDelays: const [Duration(milliseconds: 10)],
- );
- addTearDown(controller.dispose);
- await controller.initialize();
- expect(controller.status, JellyfinStatus.disabled);
- await controller.setEnabled(true);
- expect(controller.status, JellyfinStatus.needsSetup);
- expect(controller.enabled, isTrue);
- expect(settings.jellyfinEnabled, isTrue);
- });
- test(
- 'Quick Connect code is shown and the approved token is stored',
- () async {
- final settings = await _settings();
- final gateway = _FakeGateway();
- final store = _MemoryStore();
- final controller = JellyfinController(
- settings,
- gateway,
- store,
- pollInterval: const Duration(milliseconds: 10),
- pairingLifetime: const Duration(seconds: 2),
- retryDelays: const [Duration(milliseconds: 10)],
- );
- addTearDown(controller.dispose);
- await controller.initialize();
- await controller.setEnabled(true);
- await controller.connect('jellyfin.example.com');
- expect(controller.status, JellyfinStatus.pairing);
- expect(controller.quickConnectCode, '491006');
- expect(settings.jellyfinServerUrl, 'https://jellyfin.example.com');
- expect(gateway.lastDeviceId, isNotEmpty);
- gateway.approved = true;
- await Future<void>.delayed(const Duration(milliseconds: 80));
- expect(controller.status, JellyfinStatus.connected);
- expect(controller.quickConnectCode, isNull);
- expect(store.values[JellyfinController.tokenKey], 'token-1');
- expect(settings.jellyfinUserName, 'wasp');
- },
- );
- test('a saved token is reused on startup without Quick Connect', () async {
- final settings = await _settings({
- 'jellyfin.enabled': true,
- 'jellyfin.serverUrl': 'https://jellyfin.example.com',
- });
- final store = _MemoryStore()
- ..values[JellyfinController.tokenKey] = 'token-1';
- final gateway = _FakeGateway();
- final controller = JellyfinController(
- settings,
- gateway,
- store,
- pollInterval: const Duration(milliseconds: 10),
- retryDelays: const [Duration(milliseconds: 10)],
- );
- addTearDown(controller.dispose);
- await controller.initialize();
- expect(controller.status, JellyfinStatus.connecting);
- controller.start();
- await Future<void>.delayed(const Duration(milliseconds: 40));
- expect(controller.status, JellyfinStatus.connected);
- expect(controller.serverName, 'DarkTower');
- expect(controller.userName, 'wasp');
- expect(gateway.initiateCalls, 0);
- expect(gateway.userCalls, 1);
- });
- test(
- 'a rejected token disables Jellyfin, clears the token, and asks to pair',
- () async {
- final settings = await _settings({
- 'jellyfin.enabled': true,
- 'jellyfin.serverUrl': 'https://jellyfin.example.com',
- });
- final store = _MemoryStore()
- ..values[JellyfinController.tokenKey] = 'stale-token';
- final gateway = _FakeGateway()..rejectToken = true;
- final controller = JellyfinController(
- settings,
- gateway,
- store,
- retryDelays: const [Duration(milliseconds: 10)],
- );
- addTearDown(controller.dispose);
- await controller.initialize();
- controller.start();
- await Future<void>.delayed(const Duration(milliseconds: 40));
- expect(controller.enabled, isFalse);
- expect(controller.needsRepair, isTrue);
- expect(controller.status, JellyfinStatus.error);
- expect(controller.error, isNotNull);
- expect(store.values.containsKey(JellyfinController.tokenKey), isFalse);
- expect(settings.jellyfinEnabled, isFalse);
- expect(settings.jellyfinServerUrl, 'https://jellyfin.example.com');
- },
- );
- test('an unreachable server is retried and reconnects', () async {
- final settings = await _settings({
- 'jellyfin.enabled': true,
- 'jellyfin.serverUrl': 'https://jellyfin.example.com',
- });
- final store = _MemoryStore()
- ..values[JellyfinController.tokenKey] = 'token-1';
- final gateway = _FakeGateway()..unreachable = true;
- final controller = JellyfinController(
- settings,
- gateway,
- store,
- retryDelays: const [Duration(milliseconds: 20)],
- );
- addTearDown(controller.dispose);
- await controller.initialize();
- controller.start();
- await Future<void>.delayed(const Duration(milliseconds: 30));
- expect(controller.status, JellyfinStatus.offline);
- expect(controller.enabled, isTrue);
- expect(store.values[JellyfinController.tokenKey], 'token-1');
- gateway.unreachable = false;
- await Future<void>.delayed(const Duration(milliseconds: 80));
- expect(controller.status, JellyfinStatus.connected);
- expect(controller.userName, 'wasp');
- });
- test('urls are normalized with a scheme and without a trailing slash', () {
- expect(
- normalizeJellyfinUrl('jellyfin.example.com')?.toString(),
- 'https://jellyfin.example.com/',
- );
- expect(
- normalizeJellyfinUrl('http://host:8096/library')?.toString(),
- 'http://host:8096/library/',
- );
- expect(normalizeJellyfinUrl(''), isNull);
- expect(normalizeJellyfinUrl('ftp://host'), isNull);
- expect(displayJellyfinUrl('https://host/'), 'https://host');
- });
- }
|