import 'dart:convert'; import 'package:drift/drift.dart' show driftRuntimeOptions; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart' as http; import 'package:http/testing.dart'; import 'package:rippr/src/data/database.dart'; import 'package:rippr/src/domain/models.dart'; import 'package:rippr/src/telemetry/telemetry_uploader.dart'; /// Ported from `com.rippr.TelemetryUploaderTest`. /// /// The Kotlin suite used MockWebServer plus an in-memory fake DAO. Here a `MockClient` /// stands in for the network and a real in-memory Drift database stands in for the DAO — /// closer to production, and still no device required. void main() { late AppDatabase db; setUp(() { driftRuntimeOptions.dontWarnAboutMultipleDatabases = true; db = AppDatabase(NativeDatabase.memory()); }); tearDown(() async => db.close()); Future seed(int count) async { final tripId = await db.insertTrip(const Trip(startedAt: 1000, state: TripState.recording)); final segmentId = await db.insertSegment(tripId, 1000); await db.insertPoints([ for (var i = 0; i < count; i++) TrackPoint( tripId: tripId, segmentId: segmentId, timestamp: 1000 + i, latitude: 51.0, longitude: -114.0, speedKmh: 40, altitudeM: 1000, ), ]); } TelemetryUploader uploader({ required http.Client client, String endpoint = 'https://example.test/ingest', }) => TelemetryUploader( db: db, endpoint: endpoint, deviceId: 'device-abc', client: client, ); test('an empty endpoint disables upload entirely', () async { var called = false; final client = MockClient((_) async { called = true; return http.Response('', 200); }); final result = await uploader(client: client, endpoint: '').uploadPending(); expect(result, isA()); expect(called, isFalse, reason: 'recording must work with no server at all'); }); test('a successful run marks points synced', () async { await seed(5); final client = MockClient((_) async => http.Response('{}', 200)); final result = await uploader(client: client).uploadPending(); expect(result, isA()); expect((result as UploadSuccess).uploaded, 5); expect(await db.countUnsynced(), 0); }); test('a rejected batch leaves the backlog intact for retry', () async { await seed(5); final client = MockClient((_) async => http.Response('nope', 500)); final result = await uploader(client: client).uploadPending(); expect(result, isA()); expect(await db.countUnsynced(), 5, reason: 'nothing may be marked synced when the server refused it'); }); test('a network error is swallowed and retried later', () async { await seed(3); final client = MockClient((_) async => throw http.ClientException('offline')); final result = await uploader(client: client).uploadPending(); expect(result, isA()); expect(await db.countUnsynced(), 3); }); test('the payload carries trip and segment identity per point', () async { await seed(2); String? captured; final client = MockClient((req) async { captured = req.body; return http.Response('{}', 200); }); await uploader(client: client).uploadPending(); final json = jsonDecode(captured!) as Map; expect(json['device_id'], 'device-abc'); final first = (json['points'] as List).first as Map; // Batches are drawn by id and can straddle a boundary, so identity travels with the // point rather than the batch. expect(first.containsKey('trip_id'), isTrue); expect(first.containsKey('segment_id'), isTrue); }); test('a backlog larger than one batch is sent across several requests', () async { // Two full batches plus a remainder. await seed(uploadBatchSize * 2 + 7); var requests = 0; final client = MockClient((_) async { requests++; return http.Response('{}', 200); }); final result = await uploader(client: client).uploadPending(); expect(requests, 3); expect((result as UploadSuccess).uploaded, uploadBatchSize * 2 + 7); expect(await db.countUnsynced(), 0); }); test('a failure partway through reports what did land', () async { await seed(uploadBatchSize + 10); var requests = 0; final client = MockClient((_) async { requests++; return http.Response('{}', requests == 1 ? 200 : 503); }); final result = await uploader(client: client).uploadPending(); expect(result, isA()); expect((result as UploadPartial).uploaded, uploadBatchSize); expect(await db.countUnsynced(), 10, reason: 'the first batch landed; the rest must survive for retry'); }); }