import 'dart:convert'; 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/library/playlist_service.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 cloudMit( Future Function(http.Request) antwort) async { final auth = BakaAuth( speicher: _MemorySpeicher({'baka_token': 'tok', 'baka_user': 'Baka'}), ); await auth.laden(); return MeloCloudService(auth: auth, client: MockClient(antwort)); } void main() { test('eine neue Playlist wird gemeldet und ihre Server-ID gemerkt', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); final dienst = PlaylistService( db, cloud: await cloudMit((_) async => http.Response( // Form von handle_playlist_create: die ID liegt unter „playlist". jsonEncode({ 'status': 'ok', 'playlist': {'id': 7, 'name': 'Road Trip'} }), 200, )), ); final id = await dienst.createPlaylist('Road Trip'); expect((await db.playlistById(id))!.cloudId, '7'); }); test('ein Song ohne cloudId wird nicht mitgemeldet', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); var songMeldungen = 0; final dienst = PlaylistService( db, cloud: await cloudMit((anfrage) async { if (anfrage.url.path.endsWith('/songs')) songMeldungen++; return http.Response( jsonEncode({ 'status': 'ok', 'playlist': {'id': 7, 'name': 'Mix'} }), 200, ); }), ); await db.into(db.songs).insert(SongsCompanion.insert( id: 'song-1', path: '/a.mp3', title: 'A', dateAddedMs: 0, updatedAtMs: 0, )); final id = await dienst.createPlaylist('Mix'); await dienst.addSongToPlaylist(id, 'song-1', 0); expect(songMeldungen, 0); // Lokal ist er trotzdem drin — kein Fehler, nur nichts zu melden. expect(await db.watchPlaylistSongs(id).first, hasLength(1)); }); /// Legt einen Titel mit Server-ID an. Ohne den fällt jeder Push aus, und /// die Negativtests allein hätten den ganzen Vertrag nie berührt. Future legeSongAn(MeloDb db, String id, String cloudId) async { await db.into(db.songs).insert(SongsCompanion.insert( id: id, path: '/$id.mp3', title: id, dateAddedMs: 0, updatedAtMs: 0, )); await db.setCloudId(id, cloudId); } test('ein Song mit cloudId wird an die Server-Playlist gemeldet', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); Object? koerper; String? pfad; final dienst = PlaylistService( db, cloud: await cloudMit((anfrage) async { if (anfrage.method == 'POST' && anfrage.url.path.endsWith('/songs')) { pfad = anfrage.url.path; koerper = jsonDecode(anfrage.body); } return http.Response( jsonEncode({ 'status': 'ok', 'playlist': {'id': 7, 'name': 'Mix'} }), 200, ); }), ); await legeSongAn(db, 'song-1', 'c1'); final id = await dienst.createPlaylist('Mix'); await dienst.addSongToPlaylist(id, 'song-1', 0); expect(pfad, endsWith('/playlists/7/songs')); expect(koerper, { 'song_ids': ['c1'] }); }); test('das Entfernen geht als DELETE auf den Song-Pfad', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); String? geloeschterPfad; final dienst = PlaylistService( db, cloud: await cloudMit((anfrage) async { if (anfrage.method == 'DELETE') geloeschterPfad = anfrage.url.path; return http.Response( jsonEncode({ 'status': 'ok', 'playlist': {'id': 7, 'name': 'Mix'} }), 200, ); }), ); await legeSongAn(db, 'song-1', 'c1'); final id = await dienst.createPlaylist('Mix'); await dienst.addSongToPlaylist(id, 'song-1', 0); await dienst.removeSongFromPlaylist(id, 'song-1'); expect(geloeschterPfad, endsWith('/playlists/7/songs/c1')); expect(await db.watchPlaylistSongs(id).first, isEmpty); }); test('eine neue Reihenfolge geht als positions-Paare raus', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); Object? koerper; String? methode; final dienst = PlaylistService( db, cloud: await cloudMit((anfrage) async { if (anfrage.url.path.endsWith('/positions')) { methode = anfrage.method; koerper = jsonDecode(anfrage.body); } return http.Response( jsonEncode({ 'status': 'ok', 'playlist': {'id': 7, 'name': 'Mix'} }), 200, ); }), ); await legeSongAn(db, 'song-1', 'c1'); await legeSongAn(db, 'song-2', 'c2'); final id = await dienst.createPlaylist('Mix'); await dienst.addSongToPlaylist(id, 'song-1', 0); await dienst.addSongToPlaylist(id, 'song-2', 1); await dienst.reorderAll(id, ['song-2', 'song-1']); // Unter „song_ids" hätte der Server eine leere Liste gelesen und stumm // „ok" geantwortet — dieser Test ist der einzige Ort, an dem das auffällt. expect(methode, 'PUT'); expect(koerper, { 'positions': [ {'id': 'c2', 'position': 0}, {'id': 'c1', 'position': 1}, ] }); }); test('ein Endpunkt-Fehler ändert lokal nichts', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); final dienst = PlaylistService( db, cloud: await cloudMit( (_) async => http.Response(jsonEncode({'error': 'weg'}), 500)), ); final id = await dienst.createPlaylist('Mix'); expect((await db.playlistById(id))!.cloudId, isNull); expect(await db.watchPlaylists().first, hasLength(1)); }); test('Wiederherstellung greift nur bei leerer Playlisten-Tabelle', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); final dienst = PlaylistService( db, cloud: await cloudMit((anfrage) async { if (anfrage.url.path.endsWith('/playlists')) { return http.Response( jsonEncode({ 'playlists': [ {'id': 7, 'name': 'Vom Server'} ] }), 200, ); } return http.Response(jsonEncode({'songs': []}), 200); }), ); expect(await dienst.stelleWiederHer(), 1); final angelegt = await db.watchPlaylists().first; expect(angelegt.single.name, 'Vom Server'); expect(angelegt.single.cloudId, '7'); }); test('mit einer lokalen Playlist wird nichts angelegt (kein Rück-Merge)', () async { final db = MeloDb(NativeDatabase.memory()); addTearDown(db.close); final dienst = PlaylistService( db, cloud: await cloudMit((_) async => http.Response( jsonEncode({ 'playlists': [ {'id': 7, 'name': 'Vom Server'} ] }), 200, )), ); await db.createPlaylist('Meine eigene'); expect(await dienst.stelleWiederHer(), 0); expect(await db.watchPlaylists().first, hasLength(1)); }); }