## 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
152 lines
4.9 KiB
Dart
152 lines
4.9 KiB
Dart
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);
|
||
});
|
||
});
|
||
}
|