mobile bluesky app made with flutter lazurite.stormlightlabs.org/
mobile bluesky flutter
at main 7.2 kB view raw
1import 'package:flutter/material.dart'; 2import 'package:flutter_riverpod/flutter_riverpod.dart'; 3import 'package:flutter_test/flutter_test.dart'; 4import 'package:lazurite/src/core/auth/session_model.dart'; 5import 'package:lazurite/src/core/providers/app_lifecycle_provider.dart'; 6import 'package:lazurite/src/features/auth/application/auth_providers.dart'; 7import 'package:lazurite/src/features/auth/domain/auth_state.dart'; 8import 'package:lazurite/src/features/settings/application/preference_sync_controller.dart'; 9import 'package:lazurite/src/features/settings/application/settings_providers.dart'; 10import 'package:mocktail/mocktail.dart'; 11 12import '../../../../helpers/mocks.dart'; 13 14/// Mock notifier for auth state 15class MockAuthNotifier extends Notifier<AuthState> with Mock implements AuthNotifier { 16 MockAuthNotifier(this._state); 17 18 AuthState _state; 19 20 @override 21 AuthState build() => _state; 22 23 void setState(AuthState newState) { 24 _state = newState; 25 state = newState; 26 } 27} 28 29/// Mock notifier for app lifecycle 30class MockAppLifecycle extends Notifier<AppLifecycleState> with Mock implements AppLifecycle { 31 MockAppLifecycle(this._state); 32 33 AppLifecycleState _state; 34 35 @override 36 AppLifecycleState build() => _state; 37 38 void setState(AppLifecycleState newState) { 39 _state = newState; 40 state = newState; 41 } 42} 43 44void main() { 45 late MockBlueskyPreferencesRepository mockRepository; 46 47 setUp(() { 48 mockRepository = MockBlueskyPreferencesRepository(); 49 50 when(() => mockRepository.syncPreferencesFromRemote(any())).thenAnswer((_) async {}); 51 when(() => mockRepository.processSyncQueue(any())).thenAnswer((_) async {}); 52 when(() => mockRepository.clearAll(any())).thenAnswer((_) async {}); 53 }); 54 55 group('PreferenceSyncController', () { 56 group('initial sync on authenticated state', () { 57 test('syncs preferences when user is authenticated', () async { 58 final fakeSession = _createFakeSession(); 59 final authState = AuthStateAuthenticated(fakeSession); 60 61 final container = ProviderContainer( 62 overrides: [ 63 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 64 authProvider.overrideWith(() => MockAuthNotifier(authState)), 65 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.resumed)), 66 ], 67 ); 68 addTearDown(container.dispose); 69 70 container.read(preferenceSyncControllerProvider); 71 await Future<void>.delayed(Duration.zero); 72 73 verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1); 74 verify(() => mockRepository.processSyncQueue(any())).called(1); 75 }); 76 77 test('does not sync when user is not authenticated', () async { 78 const authState = AuthStateUnauthenticated(); 79 80 final container = ProviderContainer( 81 overrides: [ 82 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 83 authProvider.overrideWith(() => MockAuthNotifier(authState)), 84 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.resumed)), 85 ], 86 ); 87 addTearDown(container.dispose); 88 89 container.read(preferenceSyncControllerProvider); 90 await Future<void>.delayed(Duration.zero); 91 92 verifyNever(() => mockRepository.syncPreferencesFromRemote(any())); 93 verifyNever(() => mockRepository.processSyncQueue(any())); 94 }); 95 }); 96 97 group('error handling', () { 98 test('handles sync errors gracefully', () async { 99 final fakeSession = _createFakeSession(); 100 final authState = AuthStateAuthenticated(fakeSession); 101 102 when( 103 () => mockRepository.syncPreferencesFromRemote(any()), 104 ).thenThrow(Exception('Network error')); 105 106 final container = ProviderContainer( 107 overrides: [ 108 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 109 authProvider.overrideWith(() => MockAuthNotifier(authState)), 110 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.resumed)), 111 ], 112 ); 113 addTearDown(container.dispose); 114 115 expect(() => container.read(preferenceSyncControllerProvider), returnsNormally); 116 await Future<void>.delayed(Duration.zero); 117 118 verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1); 119 }); 120 121 test('handles queue processing errors gracefully', () async { 122 final fakeSession = _createFakeSession(); 123 final authState = AuthStateAuthenticated(fakeSession); 124 125 when(() => mockRepository.processSyncQueue(any())).thenThrow(Exception('Queue error')); 126 127 final container = ProviderContainer( 128 overrides: [ 129 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 130 authProvider.overrideWith(() => MockAuthNotifier(authState)), 131 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.resumed)), 132 ], 133 ); 134 addTearDown(container.dispose); 135 136 expect(() => container.read(preferenceSyncControllerProvider), returnsNormally); 137 await Future<void>.delayed(Duration.zero); 138 }); 139 140 test('does not crash when clearAll fails on logout', () async { 141 when(() => mockRepository.clearAll(any())).thenThrow(Exception('Clear failed')); 142 143 const authState = AuthStateUnauthenticated(); 144 145 final container = ProviderContainer( 146 overrides: [ 147 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 148 authProvider.overrideWith(() => MockAuthNotifier(authState)), 149 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.resumed)), 150 ], 151 ); 152 addTearDown(container.dispose); 153 154 expect(() => container.read(preferenceSyncControllerProvider), returnsNormally); 155 await Future<void>.delayed(Duration.zero); 156 }); 157 }); 158 159 group('lifecycle state', () { 160 test('does not sync on initial paused state', () async { 161 final fakeSession = _createFakeSession(); 162 final authState = AuthStateAuthenticated(fakeSession); 163 164 final container = ProviderContainer( 165 overrides: [ 166 blueskyPreferencesRepositoryProvider.overrideWithValue(mockRepository), 167 authProvider.overrideWith(() => MockAuthNotifier(authState)), 168 appLifecycleProvider.overrideWith(() => MockAppLifecycle(AppLifecycleState.paused)), 169 ], 170 ); 171 addTearDown(container.dispose); 172 173 container.read(preferenceSyncControllerProvider); 174 await Future<void>.delayed(Duration.zero); 175 verify(() => mockRepository.syncPreferencesFromRemote(any())).called(1); 176 }); 177 }); 178 }); 179} 180 181Session _createFakeSession() { 182 return Session( 183 did: 'did:plc:testuser123', 184 handle: 'test.bsky.social', 185 accessJwt: 'test-access-jwt', 186 refreshJwt: 'test-refresh-jwt', 187 pdsUrl: 'https://bsky.social', 188 scope: 'atproto transition:generic', 189 expiresAt: DateTime.now().add(const Duration(hours: 1)), 190 dpopKey: const <String, dynamic>{}, 191 ); 192}