This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/test/services/realtime_sync_service_test.dart

284 lines
9.0 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});
}