import 'dart:convert'; 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/melo_cloud_service.dart'; class _MemorySpeicher implements TokenSpeicher { _MemorySpeicher(this.werte); 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( speicher: _MemorySpeicher({'baka_token': 'tok', 'baka_user': 'Baka'}), ); await auth.laden(); return auth; } void main() { group('parseListe', () { test('liest Titel inklusive Grabsteinen', () { final body = jsonEncode({ 'status': 'ok', 'songs': [ { 'id': 'abc', 'title': 'Nachtpuls', 'artist': 'Rotklang', 'duration': 210, 'size': 4200, 'deleted': false, }, {'id': 'def', 'title': 'Weg', 'deleted': true}, ], 'count': 1, }); final songs = MeloCloudService.parseListe(body); expect(songs, hasLength(2)); expect(songs.first.id, 'abc'); expect(songs.first.kuenstler, 'Rotklang'); expect(songs.first.dauerSekunden, 210); expect(songs.first.geloescht, isFalse); expect(songs.last.geloescht, isTrue); }); test('leere Liste ist kein Fehler', () { expect( MeloCloudService.parseListe(jsonEncode({'status': 'ok', 'songs': []})), isEmpty, ); }); test('Server-Fehler wird als CloudException gemeldet', () { expect( () => MeloCloudService.parseListe(jsonEncode({'error': 'Auth required'})), throwsA(isA()), ); }); test('fehlender Titel wird nicht zu einer leeren Zeile', () { final songs = MeloCloudService.parseListe(jsonEncode({ 'songs': [ {'id': 'x', 'title': ''} ] })); expect(songs.single.titel, 'Unbekannt'); }); }); group('parseUpload', () { test('liefert die vergebene Server-ID', () { final body = jsonEncode( {'status': 'ok', 'song_id': 'neu123', 'title': 'A', 'action': 'new'}); expect(MeloCloudService.parseUpload(body), 'neu123'); }); test('erkennt eine Dublette an derselben ID', () { // Der Server verknüpft dieselbe Datei mit dem bestehenden Titel, // statt eine zweite Kopie anzulegen. final body = jsonEncode( {'status': 'ok', 'song_id': 'alt999', 'action': 'linked'}); expect(MeloCloudService.parseUpload(body), 'alt999'); }); test('Fehler wird als CloudException gemeldet', () { expect( () => MeloCloudService.parseUpload( jsonEncode({'error': 'Datei zu groß (max 50 MB)'})), throwsA(isA()), ); }); }); group('parseFavoriten', () { test('liefert nur die IDs', () { final body = jsonEncode({ 'favorites': [ {'id': 'a', 'title': 'A'}, {'id': 'b', 'title': 'B'}, ] }); expect(MeloCloudService.parseFavoriten(body), ['a', 'b']); }); test('eine leere Favoritenliste ist kein Fehler', () { expect( MeloCloudService.parseFavoriten(jsonEncode({'favorites': []})), isEmpty, ); }); test('fehlender Schlüssel ist ein Fehler, keine leere Menge', () { // Der Router verdrahtet für GET /favorites hart HTTP 200; sechs Handler // desselben Servers melden Fehler im 200er-Körper. Ein fälschlich // leeres Ergebnis wäre von einer echten Leerantwort nicht zu // unterscheiden — wie parseListe und parseUpload wird deshalb geworfen. expect( () => MeloCloudService.parseFavoriten(jsonEncode({'status': 'ok'})), throwsA(isA()), ); }); test('Server-Fehler im 200er-Körper wird als CloudException gemeldet', () { expect( () => MeloCloudService.parseFavoriten( jsonEncode({'status': 'error', 'error': 'Auth required'})), throwsA(isA()), ); }); }); group('endungFuer', () { test('erkennt die gängigen Formate der Bibliothek', () { expect(MeloCloudService.endungFuer('audio/mpeg'), '.mp3'); expect(MeloCloudService.endungFuer('audio/mp4'), '.m4a'); expect(MeloCloudService.endungFuer('audio/flac'), '.flac'); expect(MeloCloudService.endungFuer('audio/ogg'), '.ogg'); }); test('ignoriert angehängte Parameter und Groß-/Kleinschreibung', () { expect(MeloCloudService.endungFuer('Audio/MP4; charset=binary'), '.m4a'); }); test('fällt ohne bekannte Zuordnung auf .mp3 zurück', () { expect(MeloCloudService.endungFuer(null), '.mp3'); expect(MeloCloudService.endungFuer('application/octet-stream'), '.mp3'); }); }); group('CloudVerlauf', () { test('meldet die Zeit als ISO-Zeit ohne Bruchteile', () { final eintrag = CloudVerlauf( cloudId: 'abc', gespieltAm: DateTime.utc(2026, 8, 21, 7, 28, 5, 123), positionSekunden: 42, ); expect(eintrag.toJson(), { 'song_id': 'abc', 'played_at': '2026-08-21T07:28:05', 'position': 42, }); }); }); group('setzeFavorit', () { test('meldet den Wunsch deterministisch, nicht als Umschalten', () async { Map? gesendet; String? pfad; final dienst = MeloCloudService( auth: await _angemeldeteAuth(), client: MockClient((anfrage) async { pfad = anfrage.url.path; gesendet = jsonDecode(anfrage.body) as Map; return http.Response(jsonEncode({'status': 'ok'}), 200); }), ); await dienst.setzeFavorit('c5', true); expect(pfad, endsWith('/favorites/toggle')); expect(gesendet, {'song_id': 'c5', 'set': true}); }); test('kann einen Favoriten auch ausdrücklich entfernen', () async { Map? gesendet; final dienst = MeloCloudService( auth: await _angemeldeteAuth(), client: MockClient((anfrage) async { gesendet = jsonDecode(anfrage.body) as Map; return http.Response(jsonEncode({'status': 'ok'}), 200); }), ); await dienst.setzeFavorit('c5', false); expect(gesendet, {'song_id': 'c5', 'set': false}); }); }); }