import 'dart:convert'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:drift/native.dart'; import 'package:melo/downloads/downloads_screen.dart'; import 'package:melo/library/category_service.dart'; import 'package:melo/library/database.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/download_service.dart'; import 'package:melo/services/navidrome_service.dart'; import 'package:melo/services/yt_download_service.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; class _MemorySpeicher implements TokenSpeicher { final Map werte = {}; @override Future lesen(String key) async => werte[key]; @override Future schreiben(String key, String wert) async => werte[key] = wert; @override Future loeschen(String key) async => werte.remove(key); } Widget _wrap(BakaAuth auth, {required MeloDb db, required CategoryService categories, NavidromeService? navidrome}) { return MultiProvider( providers: [ Provider.value(value: db), // Bewusst .value: der Test besitzt den Dienst und schließt ihn im // tearDown. Würde ihn der Baum beim Abbauen schließen, bliebe ein // Aufräum-Timer von drift im Test-Rahmen hängen. ChangeNotifierProvider.value(value: categories), ChangeNotifierProvider.value(value: auth), ChangeNotifierProvider( create: (_) => YtDownloadService( auth: auth, client: MockClient((_) async => http.Response('', 500)), ), ), // Nur fürs Badge im Unterreiter-Kopf gebraucht (dienst.laeuft) — der // Download-Tab beobachtet ihn app-weit, unabhängig vom Unterreiter. ChangeNotifierProvider( create: (_) => DownloadService( db: db, navidrome: navidrome ?? NavidromeService()), ), ], child: const MaterialApp(home: Scaffold(body: DownloadsScreen())), ); } Future _angemeldet() async { final auth = BakaAuth( client: MockClient((_) async => http.Response(jsonEncode({'token': 'jwt'}), 200)), speicher: _MemorySpeicher(), ); await auth.anmelden('Baka', 'geheim'); return auth; } void main() { TestWidgetsFlutterBinding.ensureInitialized(); const medienKanal = MethodChannel('de.baka.melo/media_store'); final messenger = TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; /// Ein Handy ohne SD-Karte — mehr Speicherorte setzt der jeweilige Test. void nurInternerSpeicher() { messenger.setMockMethodCallHandler(medienKanal, (call) async { if (call.method != 'listVolumes') return null; return [ {'name': 'external_primary', 'beschreibung': 'Interner Speicher'}, ]; }); } // _YouTubeBereich prüft beim Öffnen per NavidromeService. // ladeGespeicherteZugangsdaten(), ob ein Server-User verbunden ist — ohne // diese Antwort bleibt der Aufruf im Test unbeantwortet hängen. messenger.setMockMethodCallHandler( const MethodChannel('plugins.it_nomads.com/flutter_secure_storage'), (call) async => null, ); late MeloDb db; late CategoryService categories; setUp(() { SharedPreferences.setMockInitialValues({}); nurInternerSpeicher(); db = MeloDb(NativeDatabase.memory()); categories = CategoryService(db); }); tearDown(() async { messenger.setMockMethodCallHandler(medienKanal, null); categories.dispose(); await db.close(); }); testWidgets('Download-Tab hat die Unterreiter YouTube und Server', (tester) async { final auth = BakaAuth(client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); await tester.pump(); expect(find.text('YouTube'), findsOneWidget); expect(find.text('Server'), findsOneWidget); }); testWidgets('Ohne Anmeldung fordert der YouTube-Bereich zum Anmelden auf', (tester) async { final auth = BakaAuth(client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); await tester.pump(); expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); expect(find.byType(TextField), findsNothing); }); testWidgets('Angemeldet zeigt der YouTube-Bereich das Adressfeld', (tester) async { final auth = BakaAuth( client: MockClient( (_) async => http.Response(jsonEncode({'token': 'jwt'}), 200)), speicher: _MemorySpeicher(), ); await auth.anmelden('Baka', 'geheim'); await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); await tester.pump(); expect(find.text('Angemeldet als Baka'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsOneWidget); // Zweites Feld: die Kategorie, in die der Download wandert. expect(find.text('Kategorie (optional)'), findsOneWidget); expect(find.text('Herunterladen'), findsOneWidget); }); testWidgets('Der Cookie-Schalter steht anfangs an', (tester) async { await tester.pumpWidget(_wrap(await _angemeldet(), db: db, categories: categories)); await tester.pumpAndSettle(); expect(find.text('YouTube-Cookies des Servers verwenden'), findsOneWidget); expect(tester.widget(find.byType(SwitchListTile)).value, isTrue); }); testWidgets('Abschalten des Cookie-Schalters wird gemerkt', (tester) async { await tester.pumpWidget(_wrap(await _angemeldet(), db: db, categories: categories)); await tester.pumpAndSettle(); await tester.tap(find.byType(SwitchListTile)); await tester.pumpAndSettle(); expect(tester.widget(find.byType(SwitchListTile)).value, isFalse); final prefs = await SharedPreferences.getInstance(); expect(prefs.getBool('yt_cookies'), isFalse); }); testWidgets('Gemerktes Abschalten gilt auch nach dem Neustart', (tester) async { SharedPreferences.setMockInitialValues({'yt_cookies': false}); await tester.pumpWidget(_wrap(await _angemeldet(), db: db, categories: categories)); await tester.pumpAndSettle(); expect(tester.widget(find.byType(SwitchListTile)).value, isFalse); }); testWidgets('Mehrere Speicherorte bringen die Auswahl zum Vorschein', (tester) async { messenger.setMockMethodCallHandler(medienKanal, (call) async { if (call.method != 'listVolumes') return null; return [ {'name': 'external_primary', 'beschreibung': 'Interner Speicher'}, {'name': '1234-abcd', 'beschreibung': 'SD-Karte (1234-abcd)'}, ]; }); await tester.pumpWidget(_wrap(await _angemeldet(), db: db, categories: categories)); await tester.pumpAndSettle(); expect(find.byType(DropdownButtonFormField), findsOneWidget); expect(find.text('Speicherort'), findsOneWidget); }); testWidgets('Bei nur einem Speicherort gibt es keine Auswahl', (tester) async { await tester.pumpWidget(_wrap(await _angemeldet(), db: db, categories: categories)); await tester.pumpAndSettle(); expect(find.byType(DropdownButtonFormField), findsNothing); }); testWidgets( 'Server-User sieht das Adressfeld sofort, ohne Baka-Anmeldung', (tester) async { final auth = BakaAuth( client: MockClient( (_) async => http.Response(jsonEncode({'token': 'jwt'}), 200)), speicher: _MemorySpeicher(), ); final nav = NavidromeService() ..setCredentials('https://musik.baka-net.de', 'Baka', 'geheim'); await tester.pumpWidget(_wrap(auth, db: db, categories: categories, navidrome: nav)); await tester.pumpAndSettle(); expect(find.text('Beim Baka-Konto anmelden'), findsNothing); expect(find.text('Angemeldet als Baka'), findsNothing); expect(find.text('YouTube-Adresse'), findsOneWidget); }); testWidgets( 'Server-User bekommt bei fehlgeschlagener Auto-Anmeldung einen ' 'Rückfallweg zur manuellen Anmeldung', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); final nav = NavidromeService() ..setCredentials('https://musik.baka-net.de', 'Baka', 'falsch'); await tester.pumpWidget(_wrap(auth, db: db, categories: categories, navidrome: nav)); await tester.pumpAndSettle(); expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsNothing); expect( find.textContaining('automatische Anmeldung hat nicht geklappt'), findsOneWidget); }); testWidgets( 'Der Anmelde-Dialog schlägt den bekannten Navidrome-Benutzer vor, ' 'nicht immer "Baka"', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); final nav = NavidromeService() ..setCredentials('https://musik.baka-net.de', 'Tinker', 'falsch'); await tester.pumpWidget(_wrap(auth, db: db, categories: categories, navidrome: nav)); await tester.pumpAndSettle(); await tester.tap(find.text('Beim Baka-Konto anmelden')); await tester.pumpAndSettle(); final feld = tester.widget(find.byType(TextField).first); expect(feld.controller!.text, 'Tinker'); }); testWidgets( 'Server-User meldet sich im Hintergrund mit denselben Zugangsdaten ' 'beim Baka-Konto an', (tester) async { String? gesendeterBenutzer; final auth = BakaAuth( client: MockClient((request) async { final body = jsonDecode(request.body) as Map; gesendeterBenutzer = body['username'] as String?; return http.Response(jsonEncode({'token': 'jwt'}), 200); }), speicher: _MemorySpeicher(), ); final nav = NavidromeService() ..setCredentials('https://musik.baka-net.de', 'Tinker', 'geheim2'); await tester.pumpWidget(_wrap(auth, db: db, categories: categories, navidrome: nav)); await tester.pumpAndSettle(); expect(gesendeterBenutzer, 'Tinker'); expect(auth.istAngemeldet, isTrue); }); }