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/gast_zugang.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, GastZugang? gast}) { final gastZugang = gast ?? GastZugang(); 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.value(value: gastZugang), ChangeNotifierProvider( create: (_) => YtDownloadService( auth: auth, gast: gastZugang, 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( 'Angemeldeter Cloud-Account sieht das Kontingent nach einem Download', (tester) async { final auth = await _angemeldet(); await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); await tester.pumpAndSettle(); expect(find.textContaining('von 100 heute'), findsNothing); auth.merkeVerbleibend(63); await tester.pump(); expect(find.text('Noch 63 von 100 heute'), findsOneWidget); }); 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); }); testWidgets('Gast sieht zwei Buttons: Anmelden und Als Gast fortfahren', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); await tester.pumpWidget(_wrap(auth, db: db, categories: categories)); await tester.pumpAndSettle(); expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); expect( find.text('Als Gast fortfahren (5 Downloads/Tag)'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsNothing); }); testWidgets( 'Als Gast fortfahren holt einen Token und schaltet das Adressfeld frei', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); final gast = GastZugang( client: MockClient((_) async => http.Response(jsonEncode({'guest_token': 'g-1'}), 200)), speicher: _MemorySpeicher(), ); await tester.pumpWidget( _wrap(auth, db: db, categories: categories, gast: gast)); await tester.pumpAndSettle(); await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); await tester.pumpAndSettle(); expect(find.text('YouTube-Adresse'), findsOneWidget); expect(find.text('Als Gast unterwegs (5 Downloads/Tag)'), findsOneWidget); expect(find.text('YouTube-Cookies des Servers verwenden'), findsNothing); }); testWidgets('Fehler beim Gast-Token wird angezeigt', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); final gast = GastZugang( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher(), ); await tester.pumpWidget( _wrap(auth, db: db, categories: categories, gast: gast)); await tester.pumpAndSettle(); await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); await tester.pumpAndSettle(); expect(find.text('Gast-Zugang fehlgeschlagen (500)'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsNothing); }); testWidgets( 'Anmelden im Gast-Modus verwirft den Gast-Token und zeigt wieder die ' 'Wahl', (tester) async { final auth = BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()); final gast = GastZugang( client: MockClient((_) async => http.Response(jsonEncode({'guest_token': 'g-1'}), 200)), speicher: _MemorySpeicher(), ); await tester.pumpWidget( _wrap(auth, db: db, categories: categories, gast: gast)); await tester.pumpAndSettle(); await tester.tap(find.text('Als Gast fortfahren (5 Downloads/Tag)')); await tester.pumpAndSettle(); expect(find.text('YouTube-Adresse'), findsOneWidget); await tester.tap(find.text('Anmelden')); await tester.pumpAndSettle(); expect(find.text('Beim Baka-Konto anmelden'), findsOneWidget); expect(find.text('YouTube-Adresse'), findsNothing); expect(gast.hatToken, isFalse); }); }