v2.52 — Favoriten-Sync-Fix: bidirektionaler Merge (lokal↔Server), Toggle-Push sofort, cloud_id-Verdrahtung

This commit is contained in:
Dustin
2026-08-05 10:33:23 +02:00
parent 4694955f5f
commit 3b898e238e
7 changed files with 286 additions and 17 deletions
+124
View File
@@ -0,0 +1,124 @@
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<String, dynamic> 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: <String>{},
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<String, dynamic> gesendeterBody;
String? pfad;
final cloud = CloudService(
client: MockClient((req) async {
pfad = req.url.path;
gesendeterBody = jsonDecode(req.body) as Map<String, dynamic>;
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<String, dynamic> gesendeterBody;
final cloud = CloudService(
client: MockClient((req) async {
gesendeterBody = jsonDecode(req.body) as Map<String, dynamic>;
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 = <Map<String, dynamic>>[];
final cloud = CloudService(
client: MockClient((req) async {
bodies.add(jsonDecode(req.body) as Map<String, dynamic>);
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');
});
});
}