v2.53.1 — MED-3: Tests für Tombstone-Heuristik, SyncBericht, songAusServerMap + delete-deleted_at
## 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
This commit is contained in:
@@ -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<String, dynamic> gesendeterBody;
|
||||
final cloud = CloudService(
|
||||
client: MockClient((req) async {
|
||||
gesendeterBody = jsonDecode(req.body) as Map<String, dynamic>;
|
||||
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<String, dynamic> gesendeterBody;
|
||||
final cloud = CloudService(
|
||||
client: MockClient((req) async {
|
||||
gesendeterBody = jsonDecode(req.body) as Map<String, dynamic>;
|
||||
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 =>
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user