import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/yt_download_service.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); } Future _angemeldeteAuth() async { final auth = BakaAuth( client: MockClient( (_) async => http.Response(jsonEncode({'token': 'jwt-abc'}), 200)), speicher: _MemorySpeicher(), ); await auth.anmelden('Baka', 'geheim'); return auth; } void main() { late Directory ziel; setUp(() => ziel = Directory.systemTemp.createTempSync('melo_yt_test')); tearDown(() => ziel.deleteSync(recursive: true)); test('sichererDateiname entfernt Zeichen, die kein Dateisystem mag', () { expect(sichererDateiname('AC/DC: Thunder*struck?'), 'AC_DC_ Thunder_struck_'); }); test('Nicht-YouTube-URL wird abgelehnt, ohne den Proxy zu fragen', () async { var aufrufe = 0; final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((_) async { aufrufe++; return http.Response('', 200); }), ); final ergebnis = await dienst.herunterladen('https://example.com/lied', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Das ist keine YouTube-Adresse'); expect(aufrufe, 0); }); test( 'Zeitüberschreitung beim Proxy meldet sich anders als eine tote ' 'Verbindung', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((_) async => throw TimeoutException('zu lange')), ); final ergebnis = await dienst.herunterladen( 'https://youtu.be/dQw4w9WgXcQ', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Der Server braucht zu lange — bitte später erneut versuchen'); }); test('eine tote Verbindung zum Proxy meldet sich anders als ein Timeout', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((_) async => throw const SocketException('down')), ); final ergebnis = await dienst.herunterladen( 'https://youtu.be/dQw4w9WgXcQ', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Proxy nicht erreichbar'); }); test('Erfolgreicher Download legt die MP3 im Zielordner ab', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((req) async { if (req.url.path == '/api/yt-dl') { expect(req.headers['Authorization'], 'Bearer jwt-abc'); return http.Response( jsonEncode({ 'titel': 'Neoni - Roses', 'dauer': 221, 'dateiname': 'roses.mp3', 'mp3_url': '/api/dl/roses.mp3', 'filesize': 4, }), 200); } return http.Response.bytes([1, 2, 3, 4], 200); }), ); final ergebnis = await dienst.herunterladen( 'https://youtu.be/abc123', zielOrdner: ziel.path); expect(ergebnis, isNotNull); expect(ergebnis!.titel, 'Neoni - Roses'); expect(ergebnis.kuenstler, 'Neoni'); expect(ergebnis.dauerSekunden, 221); expect(File(ergebnis.dateiPfad).readAsBytesSync(), [1, 2, 3, 4]); expect(ergebnis.dateiPfad, '${ziel.path}/Neoni - Roses.mp3'); expect(dienst.fehler, isNull); expect(dienst.laeuft, isFalse); }); test('Ohne " - " im Titel bleibt der Künstler leer statt geraten zu werden', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((req) async { if (req.url.path == '/api/yt-dl') { return http.Response( jsonEncode({ 'titel': 'Irgendein Mitschnitt', 'dauer': 10, 'mp3_url': '/api/dl/x.mp3', }), 200); } return http.Response.bytes([9], 200); }), ); final ergebnis = await dienst.herunterladen('https://youtu.be/x', zielOrdner: ziel.path); expect(ergebnis!.kuenstler, ''); }); test('Abgelaufener Token (401) bittet um erneute Anmeldung', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((_) async => http.Response('Unauthorized', 401)), ); final ergebnis = await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Anmeldung abgelaufen — bitte neu anmelden'); }); test('Ohne Anmeldung wird gar nicht erst gefragt', () async { final dienst = YtDownloadService( auth: BakaAuth( client: MockClient((_) async => http.Response('', 500)), speicher: _MemorySpeicher()), client: MockClient((_) async => http.Response('', 200)), ); final ergebnis = await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Bitte zuerst beim Baka-Konto anmelden'); }); test('Proxy-Fehlermeldung wird im Klartext durchgereicht', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((_) async => http.Response( jsonEncode({'error': 'Video ist gesperrt'}), 500)), ); final ergebnis = await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(dienst.fehler, 'Video ist gesperrt'); }); test('Scheitert der MP3-Abruf, bleibt keine halbe Datei liegen', () async { final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((req) async { if (req.url.path == '/api/yt-dl') { return http.Response( jsonEncode({ 'titel': 'Kaputt', 'dauer': 1, 'mp3_url': '/api/dl/k.mp3', }), 200); } return http.Response('weg', 404); }), ); final ergebnis = await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path); expect(ergebnis, isNull); expect(ziel.listSync(), isEmpty); }); test('Standardmäßig fragt der Auftrag die Cookies des Servers an', () async { String? auftrag; final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((req) async { if (req.url.path == '/api/yt-dl') { auftrag = req.body; return http.Response( jsonEncode({ 'titel': 'Mit Keksen', 'dauer': 5, 'mp3_url': '/api/dl/k.mp3', }), 200); } return http.Response.bytes([7], 200); }), ); await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path); expect(jsonDecode(auftrag!), {'url': 'https://youtu.be/abc', 'cookies': true}); }); test('Abgeschaltete Cookies stehen so im Auftrag', () async { String? auftrag; final dienst = YtDownloadService( auth: await _angemeldeteAuth(), client: MockClient((req) async { if (req.url.path == '/api/yt-dl') { auftrag = req.body; return http.Response( jsonEncode({ 'titel': 'Ohne Kekse', 'dauer': 5, 'mp3_url': '/api/dl/o.mp3', }), 200); } return http.Response.bytes([7], 200); }), ); await dienst.herunterladen('https://youtu.be/abc', zielOrdner: ziel.path, cookies: false); expect((jsonDecode(auftrag!) as Map)['cookies'], isFalse); }); }