From a67316479d5a5e3a8a0fe47978c0d8f40c9e61a4 Mon Sep 17 00:00:00 2001 From: bponders12-cell Date: Tue, 1 Sep 2026 23:13:26 -0400 Subject: [PATCH] test: add persistence and offline sync validation matrix --- test/sync_persistence_validation_test.dart | 105 +++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 test/sync_persistence_validation_test.dart diff --git a/test/sync_persistence_validation_test.dart b/test/sync_persistence_validation_test.dart new file mode 100644 index 0000000..178f22f --- /dev/null +++ b/test/sync_persistence_validation_test.dart @@ -0,0 +1,105 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:motorcycle_dash/services/sync/sync_models.dart'; +import 'package:motorcycle_dash/services/sync/sync_outbox.dart'; +import 'package:motorcycle_dash/services/sync/sync_manager.dart'; +import 'package:motorcycle_dash/services/sync/sync_provider.dart'; + +class _ValidationProvider implements SyncProvider { + _ValidationProvider({this.available = true, this.authenticated = true}); + + bool available; + bool authenticated; + int uploadCount = 0; + + @override + String get providerId => 'firebase'; + + @override + Future isAvailable() async => available; + + @override + Future requiresAuthentication() async => !authenticated; + + @override + Future uploadRide(SyncItem item) async { + uploadCount++; + return SyncResult.success(remoteId: 'cloud-${item.localRideId}'); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('Persistent offline/cloud synchronization validation', () { + setUp(() => SharedPreferences.setMockInitialValues({})); + + test('outbox survives an app restart with retry metadata intact', () async { + final prefs = await SharedPreferences.getInstance(); + final first = SyncOutbox(prefs); + final provider = _ValidationProvider(available: false); + final manager = SyncManager(first, initialProviders: [provider], defaultProviderId: 'firebase'); + + await manager.enqueueRide(42, autoProcess: false); + await manager.processQueue(); + expect(first.getItem(42, 'firebase')?.status, SyncStatus.waitingForNetwork); + + final restarted = SyncOutbox(prefs); + expect(restarted.getItem(42, 'firebase')?.status, SyncStatus.waitingForNetwork); + manager.dispose(); + }); + + test('local offline outbox migrates into authenticated account namespace', () async { + final prefs = await SharedPreferences.getInstance(); + final local = SyncOutbox(prefs); + await local.enqueue(77, 'firebase', autoProcess: false); + + final account = SyncOutbox(prefs, storageNamespace: 'user_abc'); + expect(account.getItem(77, 'firebase')?.status, SyncStatus.queued); + expect(prefs.getString('motorsignal_sync_outbox_manifest_v2_local'), isNull); + }); + + test('reconnect retry uploads the preserved ride and marks it synced', () async { + final prefs = await SharedPreferences.getInstance(); + final outbox = SyncOutbox(prefs); + final provider = _ValidationProvider(available: false); + final manager = SyncManager(outbox, initialProviders: [provider], defaultProviderId: 'firebase'); + + await manager.enqueueRide(88, autoProcess: false); + await manager.processQueue(); + expect(outbox.getItem(88, 'firebase')?.status, SyncStatus.waitingForNetwork); + + provider.available = true; + await manager.processQueue(); + expect(outbox.getItem(88, 'firebase')?.status, SyncStatus.synced); + expect(provider.uploadCount, 1); + manager.dispose(); + }); + + test('authentication block preserves local work until login retry', () async { + final prefs = await SharedPreferences.getInstance(); + final outbox = SyncOutbox(prefs); + final provider = _ValidationProvider(authenticated: false); + final manager = SyncManager(outbox, initialProviders: [provider], defaultProviderId: 'firebase'); + + await manager.enqueueRide(99, autoProcess: false); + await manager.processQueue(); + expect(outbox.getItem(99, 'firebase')?.status, SyncStatus.authRequired); + + provider.authenticated = true; + await manager.retryAuthRequired(); + expect(outbox.getItem(99, 'firebase')?.status, SyncStatus.synced); + manager.dispose(); + }); + + test('duplicate enqueue remains idempotent after restart', () async { + final prefs = await SharedPreferences.getInstance(); + final first = SyncOutbox(prefs); + await first.enqueue(123, 'firebase', autoProcess: false); + + final restarted = SyncOutbox(prefs); + await restarted.enqueue(123, 'firebase', autoProcess: false); + expect(restarted.count, 1); + }); + }); +}