v2.55 — Echtzeit-Cloud-Sync (SSE) mit 3 Modi: Echtzeit/Manuell/Zeit
This commit is contained in:
@@ -0,0 +1,283 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user