import 'package:flutter_test/flutter_test.dart'; import 'package:melo_app/services/realtime_sync_service.dart'; import 'package:melo_app/services/sync_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; /// RealtimeSyncService-Tests (v2.55 – Echtzeit-Cloud-Sync). /// /// Abgedeckt: /// - Backoff-Berechnung (exponentiell, clamped auf max 8s) /// - SSE-Event-Parsing (heartbeat ignoriert, song_favorite verarbeitet) /// - Tombstone-Respektierung bei Song-Löschung /// - Leere song_id wird ignoriert /// - Sync-Modus-Prüfung (manual/interval/realtime Enum-Werte) /// /// KEINE neuen Packages — reine Unit-Tests mit einfachen Hand-Mocks. /// Verwendet nur flutter_test + bestehende Projekt-Abhängigkeiten. void main() { TestWidgetsFlutterBinding.ensureInitialized(); SharedPreferences.setMockInitialValues({}); // ── Gruppe 1: Backoff-Berechnung ── group('Backoff-Berechnung (exponentiell)', () { test('Versuch 1 → 2 Sekunden', () { final service = RealtimeSyncService(); final backoff = service.backoffBerechnen(1); expect(backoff.inSeconds, 2); }); test('Versuch 2 → 4 Sekunden', () { final service = RealtimeSyncService(); final backoff = service.backoffBerechnen(2); expect(backoff.inSeconds, 4); }); test('Versuch 3 → 8 Sekunden', () { final service = RealtimeSyncService(); final backoff = service.backoffBerechnen(3); expect(backoff.inSeconds, 8); }); test('Versuch 4+ → clamped auf max 8 Sekunden', () { final service = RealtimeSyncService(); expect(service.backoffBerechnen(4).inSeconds, 8); expect(service.backoffBerechnen(5).inSeconds, 8); expect(service.backoffBerechnen(10).inSeconds, 8); }); test('Versuch 0 → 0 Sekunden (Clamp)', () { final service = RealtimeSyncService(); final backoff = service.backoffBerechnen(0); expect(backoff.inSeconds, 0); }); }); // ── Gruppe 2: SSE-Event-Parsing ── group('SSE-Event-Parsing', () { test('Heartbeat wird ignoriert (event: heartbeat)', () { final service = RealtimeSyncService(); // Heartbeat-Event → sollte keinen Fehler werfen service.verarbeiteSseZeile('event: heartbeat'); service.verarbeiteSseZeile('data: {}'); // Kein Fehler → Test bestanden expect(service.fehlerNotifier.value, isNull); }); test('Heartbeat data:-Zeile wird still verarbeitet', () { final service = RealtimeSyncService(); // Nach einem Heartbeat-Event darf keine Exception fliegen service.verarbeiteSseZeile('event: heartbeat'); expect(() => service.verarbeiteSseZeile('data: {}'), returnsNormally); }); test('song_favorite Event wird erkannt (kein Parse-Fehler)', () { final service = RealtimeSyncService(); // Gültiges song_favorite-Event — darf keinen Parse-Fehler werfen service.verarbeiteSseZeile('event: song_favorite'); expect( () => service.verarbeiteSseZeile( 'data: {"song_id":"abc123","is_favorite":true}'), returnsNormally, ); }); test('song_delete mit leerer song_id verarbeitet still', () { final service = RealtimeSyncService(); // Leere song_id → Event wird ignoriert (kein Crash) service.verarbeiteSseZeile('event: song_delete'); expect( () => service.verarbeiteSseZeile( 'data: {"song_id":"","deleted_at":"2026-08-01T12:00:00"}'), returnsNormally, ); }); test('song_update Event wird erkannt (kein Parse-Fehler)', () { final service = RealtimeSyncService(); service.verarbeiteSseZeile('event: song_update'); expect( () => service.verarbeiteSseZeile( 'data: {"song_id":"abc123","title":"Neuer Titel","artist":"Künstler"}'), returnsNormally, ); }); test('playlist_update wird ignoriert (kein Fehler)', () { final service = RealtimeSyncService(); service.verarbeiteSseZeile('event: playlist_update'); expect( () => service.verarbeiteSseZeile( 'data: {"playlist_id":42}'), returnsNormally, ); }); }); // ── Gruppe 3: Tombstone-Respektierung ── group('Tombstone-Respektierung bei Song-Löschung', () { final letzterSync = DateTime(2026, 8, 4, 12, 0, 0); test('deletedAt neuer als letzterSync → anwenden', () { expect( SyncService.tombstoneAnwenden( letzterSync: letzterSync, deletedAt: '2026-08-05T10:00:00'), isTrue, ); }); test('deletedAt älter als letzterSync → NICHT anwenden', () { expect( SyncService.tombstoneAnwenden( letzterSync: letzterSync, deletedAt: '2026-08-03T10:00:00'), isFalse, ); }); test('deletedAt null (Alt-Tombstone ohne Zeitstempel) → anwenden', () { expect( SyncService.tombstoneAnwenden( letzterSync: letzterSync, deletedAt: null), isTrue, ); }); test('letzterSync null (Erst-Sync) → anwenden', () { expect( SyncService.tombstoneAnwenden( letzterSync: null, 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 (sicherer Default)', () { expect( SyncService.tombstoneAnwenden( letzterSync: letzterSync, deletedAt: 'kein-iso-timestamp'), isTrue, ); }); }); // ── Gruppe 4: Leere song_id ── group('Leere song_id wird ignoriert', () { test('song_favorite mit leerer song_id → kein Fehler', () { final service = RealtimeSyncService(); service.verarbeiteSseZeile('event: song_favorite'); expect( () => service.verarbeiteSseZeile( 'data: {"song_id":"","is_favorite":true}'), returnsNormally, ); }); test('song_delete mit null song_id → kein Fehler', () { final service = RealtimeSyncService(); service.verarbeiteSseZeile('event: song_delete'); expect( () => service.verarbeiteSseZeile('data: {"deleted_at":"2026-08-01T12:00:00"}'), returnsNormally, ); }); test('song_update ohne song_id → kein Fehler', () { final service = RealtimeSyncService(); service.verarbeiteSseZeile('event: song_update'); expect( () => service.verarbeiteSseZeile( 'data: {"title":"Nur Titel"}'), returnsNormally, ); }); test('Ungültiges JSON in data:-Zeile → kein Crash', () { final service = RealtimeSyncService(); expect( () => service.verarbeiteSseZeile('data: kein-json'), returnsNormally, ); }); test('Leere data:-Zeile → kein Fehler', () { final service = RealtimeSyncService(); expect( () => service.verarbeiteSseZeile('data: '), returnsNormally, ); }); }); // ── Gruppe 5: Sync-Modus-Prüfung ── group('Sync-Modus (Enum + Werte)', () { test('SyncModus hat genau 3 Werte', () { const alle = SyncModus.values; expect(alle.length, 3); }); test('SyncModus-Werte sind eindeutig', () { const alle = SyncModus.values; expect(alle.toSet().length, 3); }); test('aktuellerModus() liefert immer einen gültigen Modus', () async { final modus = await RealtimeSyncService.aktuellerModus(); // Default ist 'realtime' (SharedPreferences ohne Mock = leer) expect(modus, SyncModus.echtzeit); }); test('cloudIntervallStunden() liefert Default 48h', () async { final intervall = await RealtimeSyncService.cloudIntervallStunden(); // Ohne gespeicherte Werte = Default 48 (SharedPreferences leer) expect(intervall, greaterThanOrEqualTo(48)); }); test('verschiedene Modi sind unterscheidbar', () { expect(SyncModus.manuell, isNot(SyncModus.echtzeit)); expect(SyncModus.manuell, isNot(SyncModus.intervall)); expect(SyncModus.echtzeit, isNot(SyncModus.intervall)); }); }); // ── Gruppe 6: Notifier-Verhalten ── group('Notifier-Verhalten', () { test('istVerbundenNotifier startet mit false', () { final service = RealtimeSyncService(); expect(service.istVerbundenNotifier.value, false); }); test('fehlerNotifier startet mit null', () { final service = RealtimeSyncService(); expect(service.fehlerNotifier.value, isNull); }); test('stoppe() setzt istVerbunden auf false', () { final service = RealtimeSyncService(); service.istVerbundenNotifier.value = true; service.stoppe(); expect(service.istVerbundenNotifier.value, false); }); test('pausiere() setzt istVerbunden auf false', () { final service = RealtimeSyncService(); service.istVerbundenNotifier.value = true; service.pausiere(); expect(service.istVerbundenNotifier.value, false); }); }); }