diff --git a/test/cloud_service_test.dart b/test/cloud_service_test.dart index 4af0436..a4fc77c 100644 --- a/test/cloud_service_test.dart +++ b/test/cloud_service_test.dart @@ -304,6 +304,44 @@ void main() { expect(await cloud401.delete('s1'), isFalse); }); + test('delete mit deletedAt sendet deleted_at im Body (Tombstone, Sprint E)', + () async { + late Map gesendeterBody; + final cloud = CloudService( + client: MockClient((req) async { + gesendeterBody = jsonDecode(req.body) as Map; + return http.Response('{}', 200); + }), + ); + final ok = await cloud.delete('s1', deletedAt: '2026-08-05T10:00:00'); + expect(ok, isTrue); + expect(gesendeterBody['song_id'], 's1'); + expect(gesendeterBody['deleted_at'], '2026-08-05T10:00:00'); + }); + + test('delete ohne deletedAt sendet KEIN deleted_at-Feld', () async { + late Map gesendeterBody; + final cloud = CloudService( + client: MockClient((req) async { + gesendeterBody = jsonDecode(req.body) as Map; + return http.Response('{}', 200); + }), + ); + final ok = await cloud.delete('s1'); + expect(ok, isTrue); + expect(gesendeterBody['song_id'], 's1'); + expect(gesendeterBody.containsKey('deleted_at'), isFalse); + }); + + test('delete Netzwerkfehler → false (kein Crash)', () async { + final cloud = CloudService( + client: MockClient( + (req) async => throw http.ClientException('offline')), + ); + expect(await cloud.delete('s1', deletedAt: '2026-08-05T10:00:00'), + isFalse); + }); + test('globalList 200 → Songs, 401 → []', () async { final cloudOk = CloudService( client: MockClient((req) async => diff --git a/test/sync_service_test.dart b/test/sync_service_test.dart new file mode 100644 index 0000000..32b8dde --- /dev/null +++ b/test/sync_service_test.dart @@ -0,0 +1,151 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:melo_app/services/sync_service.dart'; + +/// SyncService-Tests (Sprint E, MED-3-Review-Fix). +/// +/// Abgedeckt: +/// - `tombstoneAnwenden`-Matrix (null-letzterSync / null-deletedAt / +/// älter → false / neuer → true / Gleichstand / ungültiger Timestamp) +/// - `SyncBericht` (hatAenderungen, zusammenfassung, details) +/// - `songAusServerMap` (MED-2: Server-Listeneintrag → lokaler Song-DB- +/// Eintrag mit cloud_id + Titeldaten) +/// +/// Reine, statische Funktionen — kein DB-/Plattform-Zugriff nötig. +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + final letzterSync = DateTime(2026, 8, 4, 12, 0, 0); + + group('tombstoneAnwenden (Tombstone-Race-Heuristik, MED-1)', () { + test('letzterSync null (Erst-Sync) → anwenden', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: null, deletedAt: '2026-08-05T10:00:00'), + isTrue, + ); + }); + + test('deletedAt null (Alt-Tombstone ohne Zeitstempel) → anwenden', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: letzterSync, deletedAt: null), + isTrue, + ); + }); + + test('deletedAt älter als letzterSync → NICHT anwenden', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: letzterSync, deletedAt: '2026-08-03T10:00:00'), + isFalse, + ); + }); + + test('deletedAt neuer als letzterSync → anwenden', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: letzterSync, deletedAt: '2026-08-05T10:00:00'), + isTrue, + ); + }); + + test('deletedAt == letzterSync (Gleichstand) → NICHT anwenden', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: letzterSync, + deletedAt: '2026-08-04T12:00:00'), + isFalse, + ); + }); + + test('ungültiger deletedAt-String → anwenden (d == null → sicher)', () { + expect( + SyncService.tombstoneAnwenden( + letzterSync: letzterSync, deletedAt: 'kein-timestamp'), + isTrue, + ); + }); + }); + + group('SyncBericht (Konflikt-Report, Sprint E)', () { + test('hatAenderungen: false bei leerem Bericht', () { + const bericht = SyncBericht( + neueSongs: 0, geloeschteSongs: 0, favoritenGeaendert: 0); + expect(bericht.hatAenderungen, isFalse); + }); + + test('hatAenderungen: true sobald eine Kategorie > 0', () { + const bericht = SyncBericht( + neueSongs: 1, geloeschteSongs: 0, favoritenGeaendert: 0); + expect(bericht.hatAenderungen, isTrue); + }); + + test('zusammenfassung kombiniert alle Kategorien', () { + const bericht = SyncBericht( + neueSongs: 8, geloeschteSongs: 3, favoritenGeaendert: 5); + expect(bericht.zusammenfassung, + '+8 neue Songs · −3 gelöscht · ⭐5 Favoriten geändert'); + }); + + test('zusammenfassung leer bei 0 Änderungen', () { + const bericht = SyncBericht( + neueSongs: 0, geloeschteSongs: 0, favoritenGeaendert: 0); + expect(bericht.zusammenfassung, isEmpty); + }); + + test('details werden durchgereicht', () { + const bericht = SyncBericht( + neueSongs: 1, + geloeschteSongs: 1, + favoritenGeaendert: 0, + details: ['+ Neuer Song: A', '− Gelöscht: B'], + ); + expect(bericht.details, hasLength(2)); + expect(bericht.details.first, '+ Neuer Song: A'); + }); + }); + + group('songAusServerMap (MED-2: Server-Listeneintrag → lokaler Song)', () { + test('mappt cloud_id + Titeldaten + Flags', () { + final song = SyncService.songAusServerMap({ + 'id': 'srv-42', + 'title': 'Testtitel', + 'artist': ' Künstler ', + 'duration': 217, + 'size': 5242880, + 'istKorrupt': true, + }, '/data/music/Testtitel'); + expect(song.cloudId, 'srv-42'); + expect(song.titel, 'Testtitel'); + expect(song.kuenstler, 'Künstler'); // getrimmt + expect(song.dauerSekunden, 217); + expect(song.groesseBytes, 5242880); + expect(song.dateiPfad, '/data/music/Testtitel'); + expect(song.downloadQuelle, 'cloud'); + expect(song.istHeruntergeladen, isTrue); + expect(song.istKorrupt, isTrue); + expect(song.ytUrl, isNull); // v2.52-F2: Server bleibt YT-Link-Quelle + }); + + test('fehlende Felder → Defaults (kein Crash)', () { + final song = SyncService.songAusServerMap({'id': 'srv-1'}, '/x'); + expect(song.titel, 'unknown'); + expect(song.kuenstler, ''); + expect(song.dauerSekunden, 0); + expect(song.groesseBytes, 0); + expect(song.istKorrupt, isFalse); + expect(song.cloudId, 'srv-1'); + }); + + test('duration/size als String → robust geparst', () { + final song = SyncService.songAusServerMap({ + 'id': 'srv-2', + 'title': 'A', + 'duration': '180', + 'size': '1024', + }, '/x'); + expect(song.dauerSekunden, 180); + expect(song.groesseBytes, 1024); + }); + }); +}