From f14b658566745df57b06668f86a7f0008dcd77f2 Mon Sep 17 00:00:00 2001 From: Dustin Date: Wed, 5 Aug 2026 11:24:51 +0200 Subject: [PATCH] =?UTF-8?q?v2.53.1=20=E2=80=94=20MED-3:=20Tests=20f=C3=BCr?= =?UTF-8?q?=20Tombstone-Heuristik,=20SyncBericht,=20songAusServerMap=20+?= =?UTF-8?q?=20delete-deleted=5Fat?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## MED-3 (Review-Fix) - test/sync_service_test.dart NEU (14 Tests): tombstoneAnwenden-Matrix (letzterSync null → true, deletedAt null → true, älter → false, neuer → true, Gleichstand → false, ungültiger Timestamp → true), SyncBericht (hatAenderungen, zusammenfassung kombiniert/leer, details), songAusServerMap (cloud_id/Titel/Künstler/Dauer/Größe/istKorrupt-Mapping, Defaults bei fehlenden Feldern, String-Robustheit) — deckt die MED-1-Heuristik + den MED-2-DB-Insert ab (123→140 Tests) - test/cloud_service_test.dart +3: delete mit deletedAt sendet deleted_at im Body, delete ohne deletedAt sendet KEIN deleted_at-Feld, delete Netzwerkfehler → false --- test/cloud_service_test.dart | 38 +++++++++ test/sync_service_test.dart | 151 +++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 test/sync_service_test.dart 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); + }); + }); +}