From 22238138e830e65695eadf2f669af3379fb25bf4 Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Fri, 14 Aug 2026 23:48:26 +0200 Subject: [PATCH] =?UTF-8?q?Fix:=20Cloud-Playlists=20lassen=20sich=20nicht?= =?UTF-8?q?=20l=C3=B6schen=20(DELETE=20statt=20POST)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit deletePlaylist() schickte ein POST auf /api/v1/cloud/playlists. Der Server unterscheidet dort nach Methode (POST = anlegen, DELETE = löschen), also landete das Löschen im Anlegen-Handler, scheiterte an der leeren Namensprüfung und lieferte HTTP 200 mit `status: error` zurück. Zusätzlich wurde der Rückgabewert in cloud_screen.dart ignoriert: die UI meldete "Playlist gelöscht", während die Playlist auf dem Server bestehen blieb und beim nächsten Sync wieder auftauchte. - cloud_service.dart: DELETE mit id im Body, Fehler werden geloggt - cloud_screen.dart: Rückgabewert auswerten statt pauschal Erfolg melden - cloud_service_test.dart: 3 Regressionstests (Methode ist DELETE, id im Body, status != ok → false, Netzwerkfehler → false) Co-Authored-By: Claude Opus 5 --- lib/screens/cloud_screen.dart | 9 ++++++-- lib/services/cloud_service.dart | 23 ++++++++++++++++--- test/cloud_service_test.dart | 39 +++++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/lib/screens/cloud_screen.dart b/lib/screens/cloud_screen.dart index 79092d3..05a69be 100644 --- a/lib/screens/cloud_screen.dart +++ b/lib/screens/cloud_screen.dart @@ -816,9 +816,14 @@ class _CloudScreenState extends State ), ); if (ok == true) { - await widget.cloud.deletePlaylist(id); + // Rückgabewert auswerten — sonst meldet die UI Erfolg, während die + // Playlist auf dem Server bleibt und beim nächsten Sync wieder auftaucht. + final geloescht = await widget.cloud.deletePlaylist(id); await _ladePlaylists(); - _setzeStatus('Playlist gelöscht', ok: true); + _setzeStatus( + geloescht ? 'Playlist gelöscht' : 'Löschen fehlgeschlagen', + ok: geloescht, + ); } } diff --git a/lib/services/cloud_service.dart b/lib/services/cloud_service.dart index fc2606f..9421dd1 100644 --- a/lib/services/cloud_service.dart +++ b/lib/services/cloud_service.dart @@ -199,10 +199,27 @@ class CloudService extends ChangeNotifier { return r?['playlist']; } - /// Playlist löschen + /// Playlist löschen. + /// + /// Muss DELETE sein: der Server unterscheidet die Methode auf demselben Pfad + /// (POST = anlegen, DELETE = löschen). Ein POST landete früher im + /// Anlegen-Handler, scheiterte dort an der leeren Namensprüfung und lieferte + /// HTTP 200 mit `status: error` — die Playlist blieb unbemerkt bestehen. Future deletePlaylist(int id) async { - final r = await _post('/api/v1/cloud/playlists', {'id': id}); - return r?['status'] == 'ok'; + try { + final r = await _client + .delete(Uri.parse('$_base/api/v1/cloud/playlists'), + headers: _jsonHeader, body: jsonEncode({'id': id})) + .timeout(const Duration(seconds: 10)); + if (r.statusCode != 200) { + MeloLogger().fehler('cloud_playlist_delete', '${r.statusCode}'); + return false; + } + return jsonDecode(r.body)['status'] == 'ok'; + } catch (e) { + MeloLogger().fehler('cloud_playlist_delete_error', '$e'); + return false; + } } /// Playlist-Details mit Songs abrufen diff --git a/test/cloud_service_test.dart b/test/cloud_service_test.dart index a4fc77c..27c8320 100644 --- a/test/cloud_service_test.dart +++ b/test/cloud_service_test.dart @@ -355,4 +355,43 @@ void main() { expect(await cloud401.globalList(), isEmpty); }); }); + + // Regression: deletePlaylist schickte ein POST auf /playlists. Der Server + // unterscheidet dort nach Methode (POST = anlegen, DELETE = löschen), also + // landete das Löschen im Anlegen-Handler, scheiterte an der leeren + // Namensprüfung und lieferte HTTP 200 mit `status: error`. Die UI meldete + // trotzdem Erfolg, die Playlist blieb bestehen. + group('deletePlaylist', () { + test('nutzt DELETE (nicht POST) und sendet die id im Body', () async { + String? methode; + String? body; + final cloud = CloudService( + client: MockClient((req) async { + methode = req.method; + body = req.body; + return jsonOk({'status': 'ok'}); + }), + ); + + expect(await cloud.deletePlaylist(42), isTrue); + expect(methode, 'DELETE'); + expect(jsonDecode(body!)['id'], 42); + }); + + test('status != ok → false (meldet keinen falschen Erfolg)', () async { + final cloud = CloudService( + client: MockClient( + (req) async => jsonOk({'status': 'error', 'error': 'Name erforderlich'})), + ); + expect(await cloud.deletePlaylist(42), isFalse); + }); + + test('Netzwerkfehler → false (kein Crash)', () async { + final cloud = CloudService( + client: MockClient( + (req) async => throw http.ClientException('offline')), + ); + expect(await cloud.deletePlaylist(42), isFalse); + }); + }); }