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_app/services/cloud_service.dart'; import 'package:melo_app/services/favoriten_sync.dart'; /// Tests für den bidirektionalen Favoriten-Sync (Sprint D v2.52). /// /// Root-Cause-Regression: `_syncAlles` spiegelte Server-Favoriten nur zurück /// (Server → Server) — lokale ⭐-Toggles gingen verloren. Der Fix merge-t /// lokal ∪ Server (favoritenMerge) und pusht Toggles deterministisch via /// `set` (kein Doppel-Toggle-Risiko). void main() { TestWidgetsFlutterBinding.ensureInitialized(); http.Response jsonOk(Map body) => http.Response(jsonEncode(body), 200, headers: {'content-type': 'application/json'}); group('favoritenMerge (Vereinigung lokal ∪ Server)', () { test('lokale Favoriten werden dem Server-Push hinzugefügt', () { final merged = favoritenMerge( lokal: {'cloud-1', 'cloud-2'}, server: {'cloud-9'}, ); expect(merged, containsAll(['cloud-1', 'cloud-2', 'cloud-9'])); expect(merged, hasLength(3)); }); test('Server-Favoriten bleiben erhalten (kein Datenverlust)', () { final merged = favoritenMerge( lokal: {}, server: {'cloud-7', 'cloud-8'}, ); expect(merged, containsAll(['cloud-7', 'cloud-8'])); }); test('Doppelte IDs (lokal == server) werden dedupliziert', () { final merged = favoritenMerge( lokal: {'cloud-1', 'cloud-2'}, server: {'cloud-2', 'cloud-3'}, ); expect(merged, hasLength(3)); expect(merged.where((id) => id == 'cloud-2'), hasLength(1)); }); test('leere Mengen → leere Liste', () { expect(favoritenMerge(lokal: {}, server: {}), isEmpty); }); test('deterministisch sortiert (für Tests/Stabilität)', () { final a = favoritenMerge(lokal: {'b', 'a'}, server: {'c'}); final b = favoritenMerge(lokal: {'c'}, server: {'b', 'a'}); expect(a, b); expect(a, ['a', 'b', 'c']); }); }); group('CloudService.setFavorite (deterministischer Push)', () { test('sendet set:true und wertet status ok aus', () async { late Map gesendeterBody; String? pfad; final cloud = CloudService( client: MockClient((req) async { pfad = req.url.path; gesendeterBody = jsonDecode(req.body) as Map; return jsonOk({'status': 'ok', 'favorited': true}); }), ); final ok = await cloud.setFavorite('cloud-1', favorit: true); expect(ok, isTrue); expect(pfad, '/api/v1/cloud/favorites/toggle'); expect(gesendeterBody['song_id'], 'cloud-1'); expect(gesendeterBody['set'], isTrue); }); test('sendet set:false beim Entfernen', () async { late Map gesendeterBody; final cloud = CloudService( client: MockClient((req) async { gesendeterBody = jsonDecode(req.body) as Map; return jsonOk({'status': 'ok', 'favorited': false}); }), ); final ok = await cloud.setFavorite('cloud-2', favorit: false); expect(ok, isTrue); expect(gesendeterBody['set'], isFalse); }); test('Fehlerantwort (kein status ok) → false', () async { final cloud = CloudService( client: MockClient((req) async => jsonOk({'status': 'error'})), ); expect(await cloud.setFavorite('cloud-3', favorit: true), isFalse); }); test('Netzwerkfehler → false (kein Crash)', () async { final cloud = CloudService( client: MockClient( (req) async => throw http.ClientException('offline')), ); expect(await cloud.setFavorite('cloud-4', favorit: true), isFalse); }); }); group('pusheFavoritAnServer (Feuer-und-Vergessen)', () { test('leitet Toggle-Status an setFavorite weiter', () async { final bodies = >[]; final cloud = CloudService( client: MockClient((req) async { bodies.add(jsonDecode(req.body) as Map); return jsonOk({'status': 'ok'}); }), ); await pusheFavoritAnServer(cloud, 'cloud-5', true); await pusheFavoritAnServer(cloud, 'cloud-5', false); expect(bodies, hasLength(2)); expect(bodies[0]['set'], isTrue); expect(bodies[1]['set'], isFalse); expect(bodies[0]['song_id'], 'cloud-5'); }); }); }