import 'dart:convert'; import 'dart:io'; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:melo/library/database.dart'; import 'package:melo/services/baka_auth.dart'; import 'package:melo/services/melo_cloud_service.dart'; import 'package:melo/services/sync_service.dart'; import 'package:shared_preferences/shared_preferences.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() { TestWidgetsFlutterBinding.ensureInitialized(); late Directory tempDir; late MeloDb db; setUp(() async { SharedPreferences.setMockInitialValues({}); tempDir = await Directory.systemTemp.createTemp('melo_sync'); db = MeloDb(NativeDatabase.memory()); }); tearDown(() async { await db.close(); await tempDir.delete(recursive: true); }); Future baue( Future Function(http.Request) antwort, ) async { return SyncService( db: db, cloud: MeloCloudService( auth: await _angemeldeteAuth(), client: MockClient(antwort), ), musikOrdner: () async => tempDir, ); } test('ein Titel vom Server landet als Datei und in der Bibliothek', () async { final sync = await baue((anfrage) async { final pfad = anfrage.url.path; if (pfad.endsWith('/list')) { return http.Response( jsonEncode({ 'songs': [ {'id': 'c1', 'title': 'Nachtpuls', 'artist': 'Rotklang', 'duration': 200} ] }), 200, ); } if (pfad.contains('/download/')) { return http.Response.bytes([1, 2, 3, 4], 200); } return http.Response(jsonEncode({'status': 'ok'}), 200); }); await sync.synchronisiere(); expect(sync.fehler, isNull); final songs = await db.allSongs(); expect(songs, hasLength(1)); expect(songs.single.title, 'Nachtpuls'); expect(songs.single.artist, 'Rotklang'); expect(songs.single.cloudId, 'c1'); expect(songs.single.durationMs, 200000); expect(await File(songs.single.path).readAsBytes(), [1, 2, 3, 4]); }); test('eine eigene Datei geht zum Server und bekommt die Server-ID', () async { final datei = File('${tempDir.path}/eigen.mp3'); await datei.writeAsBytes([9, 9, 9]); await db.upsertSongs([ SongsCompanion.insert( id: 'lokal-1', path: datei.path, title: 'Eigenes Lied', dateAddedMs: 0, updatedAtMs: 0, ), ]); var hochgeladen = 0; final sync = await baue((anfrage) async { final pfad = anfrage.url.path; if (pfad.endsWith('/list')) { return http.Response(jsonEncode({'songs': []}), 200); } if (pfad.endsWith('/upload')) { hochgeladen++; return http.Response( jsonEncode({'status': 'ok', 'song_id': 'neu-42'}), 200); } return http.Response(jsonEncode({'status': 'ok'}), 200); }); await sync.synchronisiere(); expect(sync.fehler, isNull); expect(hochgeladen, 1); expect((await db.allSongs()).single.cloudId, 'neu-42'); }); test('ein am Server gelöschter Titel verschwindet auch auf dem Gerät', () async { await db.upsertSongs([ SongsCompanion.insert( id: 'lokal-1', path: '${tempDir.path}/weg.mp3', title: 'Weg', dateAddedMs: 0, updatedAtMs: 0, ), ]); await db.setCloudId('lokal-1', 'c9'); final sync = await baue((anfrage) async { if (anfrage.url.path.endsWith('/list')) { return http.Response( jsonEncode({ 'songs': [ {'id': 'c9', 'title': 'Weg', 'deleted': true} ] }), 200, ); } return http.Response(jsonEncode({'status': 'ok'}), 200); }); await sync.synchronisiere(); expect((await db.allSongs()).single.deleted, isTrue); expect(await db.watchSongs().first, isEmpty); }); test('Favoriten werden mit ihren Server-IDs gemeldet', () async { await db.upsertSongs([ SongsCompanion.insert( id: 'lokal-1', path: '${tempDir.path}/fav.mp3', title: 'Lieblingslied', dateAddedMs: 0, updatedAtMs: 0, ), ]); await db.setCloudId('lokal-1', 'c5'); await db.setFavorite('lokal-1', true); List? gemeldet; final sync = await baue((anfrage) async { final pfad = anfrage.url.path; if (pfad.endsWith('/list')) { return http.Response( jsonEncode({ 'songs': [ {'id': 'c5', 'title': 'Lieblingslied'} ] }), 200, ); } if (pfad.endsWith('/favorites')) { final d = jsonDecode(anfrage.body) as Map; gemeldet = (d['song_ids'] as List).cast(); } return http.Response(jsonEncode({'status': 'ok'}), 200); }); await sync.synchronisiere(); expect(gemeldet, ['c5']); }); test('ohne Anmeldung passiert nichts und der Grund steht da', () async { final sync = SyncService( db: db, cloud: MeloCloudService( auth: BakaAuth(speicher: _MemorySpeicher({})), client: MockClient((_) async => http.Response('{}', 200)), ), musikOrdner: () async => tempDir, ); await sync.synchronisiere(); expect(sync.fehler, contains('anmelden')); expect(sync.laeuft, isFalse); }); test('ein Server-Fehler beendet den Lauf mit einer lesbaren Meldung', () async { final sync = await baue( (_) async => http.Response(jsonEncode({'error': 'Auth required'}), 401)); await sync.synchronisiere(); expect(sync.fehler, contains('Anmeldung abgelaufen')); expect(sync.laeuft, isFalse); }); }