import 'dart:convert'; import 'dart:io'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:mocktail/mocktail.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:sqflite_common_ffi/sqflite_ffi.dart'; import 'package:mabea_app/api/api_client.dart'; import 'package:mabea_app/offline/queue.dart'; class _MockClient extends Mock implements http.Client {} /// OfflineQueue ist ein Singleton mit eigenem Datenbank-Handle (analog zur /// PWA-Queue, siehe frontend/src/offline/queue.ts) - für isolierte Tests /// hier über Reflection auf das private Feld nicht nötig: jeder Test nutzt /// eigene kontrolleId/materialId-Kombinationen, damit sich Einträge nicht /// überschneiden, plus Aufräumen am Ende. void main() { setUpAll(() { sqfliteFfiInit(); databaseFactory = databaseFactoryFfi; }); late _MockClient httpClient; late ApiClient apiClient; setUp(() { SharedPreferences.setMockInitialValues({}); httpClient = _MockClient(); apiClient = ApiClient(baseUrl: 'http://test.local/api/v1', httpClient: httpClient); }); test('enqueue speichert Eintrag, verschwindet nach erfolgreicher Übertragung', () async { const kontrolleId = 'k-erfolg'; when(() => httpClient.put( Uri.parse('http://test.local/api/v1/kontrollen/$kontrolleId/positionen/10'), headers: any(named: 'headers'), body: any(named: 'body'), )).thenAnswer((_) async => http.Response(jsonEncode({'id': 'kp-1'}), 200)); await OfflineQueue.instance.enqueue(kontrolleId: kontrolleId, materialId: 10, istmenge: '5'); var eintraege = await OfflineQueue.instance.eintraegeFuerKontrolle(kontrolleId); expect(eintraege, hasLength(1)); expect(eintraege.single.status, 'wartet'); await OfflineQueue.instance.synchronisiere(apiClient); eintraege = await OfflineQueue.instance.eintraegeFuerKontrolle(kontrolleId); expect(eintraege, isEmpty); }); test('Netzwerkfehler lässt Eintrag auf "wartet" stehen, kein Datenverlust', () async { const kontrolleId = 'k-netzwerkfehler'; when(() => httpClient.put( Uri.parse('http://test.local/api/v1/kontrollen/$kontrolleId/positionen/20'), headers: any(named: 'headers'), body: any(named: 'body'), )).thenThrow(const SocketException('Keine Verbindung')); await OfflineQueue.instance.enqueue(kontrolleId: kontrolleId, materialId: 20, istmenge: '3'); await OfflineQueue.instance.synchronisiere(apiClient); final eintraege = await OfflineQueue.instance.eintraegeFuerKontrolle(kontrolleId); expect(eintraege, hasLength(1)); expect(eintraege.single.status, 'wartet'); }); test('4xx-Antwort setzt Eintrag auf "fehler", kein Auto-Retry', () async { const kontrolleId = 'k-4xx'; when(() => httpClient.put( Uri.parse('http://test.local/api/v1/kontrollen/$kontrolleId/positionen/30'), headers: any(named: 'headers'), body: any(named: 'body'), )).thenAnswer((_) async => http.Response(jsonEncode({'detail': 'Objekt gesperrt'}), 409)); await OfflineQueue.instance.enqueue(kontrolleId: kontrolleId, materialId: 30, istmenge: '3'); await OfflineQueue.instance.synchronisiere(apiClient); var eintraege = await OfflineQueue.instance.eintraegeFuerKontrolle(kontrolleId); expect(eintraege.single.status, 'fehler'); expect(eintraege.single.fehlerText, contains('409')); // Erneuter Sync-Lauf darf den Fehler-Eintrag NICHT automatisch erneut senden. await OfflineQueue.instance.synchronisiere(apiClient); verify(() => httpClient.put( Uri.parse('http://test.local/api/v1/kontrollen/$kontrolleId/positionen/30'), headers: any(named: 'headers'), body: any(named: 'body'), )).called(1); await OfflineQueue.instance.entferneFehlerEintrag(kontrolleId, 30); eintraege = await OfflineQueue.instance.eintraegeFuerKontrolle(kontrolleId); expect(eintraege, isEmpty); }); }