/
dbnrbv
/
Habit
Обзор
Документация
Войти
/
dbnrbv
/
Habit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
develop
test/unit/data/sync/sync_manager_test.dart
564 строки
21 KB
Daba
fix dart format
04 июн 2026, 09:23
04 июн 2026, 09:23
08f53b1
Код
Авторство
О чём код?
import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:habit/data/datasources/local/habit_local_datasource.dart'; import 'package:habit/data/datasources/remote/habit_remote_datasource.dart'; import 'package:habit/data/sync/sync_manager.dart'; import 'package:habit/domain/models/habit.dart'; import 'package:habit/domain/models/habit_execution.dart'; import 'package:habit/core/services/connectivity_service.dart'; // ─── Fakes для mocktail ─────────────────────────────────────────────────── class FakeHabit extends Fake implements Habit {} class FakeHabitExecution extends Fake implements HabitExecution {} class FakeDateTime extends Fake implements DateTime {} // ─── Моки ───────────────────────────────────────────────────────────────── class MockHabitLocalDataSource extends Mock implements HabitLocalDataSource {} class MockHabitRemoteDataSource extends Mock implements HabitRemoteDataSource {} class MockConnectivityService extends Mock implements ConnectivityService {} // ─── Вспомогательные функции ────────────────────────────────────────────── /// Создаёт тестовую привычку с указанными параметрами Habit createTestHabit({ String id = 'habit-1', String userId = 'user-123', String title = 'Test Habit', DateTime? createdAt, DateTime? updatedAt, DateTime? deletedAt, }) { final now = DateTime.now(); return Habit( id: id, userId: userId, title: title, periodicity: Periodicity.daily, createdAt: createdAt ?? now, updatedAt: updatedAt ?? now, deletedAt: deletedAt, ); } /// Создаёт тестовое выполнение привычки HabitExecution createTestExecution({ String id = 'execution-1', String habitId = 'habit-1', String userId = 'user-123', DateTime? date, DateTime? createdAt, }) { final now = DateTime.now(); return HabitExecution( id: id, habitId: habitId, userId: userId, date: date ?? now, createdAt: createdAt ?? now, ); } // ─── Тесты ──────────────────────────────────────────────────────────────── void main() { setUpAll(() { // Регистрируем fallback-значения для mocktail registerFallbackValue(FakeHabit()); registerFallbackValue(FakeHabitExecution()); registerFallbackValue(FakeDateTime()); registerFallbackValue(<Habit>[]); registerFallbackValue(<HabitExecution>[]); }); late MockHabitLocalDataSource mockLocalDataSource; late MockHabitRemoteDataSource mockRemoteDataSource; late MockConnectivityService mockConnectivityService; late SyncManager syncManager; setUp(() { mockLocalDataSource = MockHabitLocalDataSource(); mockRemoteDataSource = MockHabitRemoteDataSource(); mockConnectivityService = MockConnectivityService(); syncManager = SyncManager( localDataSource: mockLocalDataSource, remoteDataSource: mockRemoteDataSource, connectivityService: mockConnectivityService, ); }); tearDown(() { syncManager.dispose(); }); group('SyncManager.syncAll', () { test('успешная загрузка: удалённые привычки сохраняются локально', () async { // Arrange const userId = 'user-123'; final remoteHabit = createTestHabit(id: 'habit-1', userId: userId); final remoteExecution = createTestExecution( id: 'exec-1', habitId: 'habit-1', userId: userId, ); when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => [remoteHabit]); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => [remoteExecution]); when(() => mockLocalDataSource.getHabit('habit-1')) .thenReturn(null); // Привычка не была локально when(() => mockLocalDataSource.saveHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncAll(userId); // Assert verify(() => mockLocalDataSource.saveHabit(remoteHabit)).called(1); verify(() => mockLocalDataSource.saveExecutions([remoteExecution])) .called(1); verify(() => mockLocalDataSource.setLastSyncTime(any())).called(1); verify(() => mockLocalDataSource.clearLocallyModifiedHabits()).called(1); }); test( 'конфликт версий: если локальная привычка новее удалённой, ' 'локальная версия НЕ перезаписывается', () async { // Arrange const userId = 'user-123'; final remoteUpdateTime = DateTime.now().subtract(const Duration(days: 1)); final localUpdateTime = DateTime.now(); final remoteHabit = createTestHabit( id: 'habit-1', userId: userId, updatedAt: remoteUpdateTime, title: 'Remote Title', ); final localHabit = createTestHabit( id: 'habit-1', userId: userId, updatedAt: localUpdateTime, title: 'Local Title', ); when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => [remoteHabit]); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockLocalDataSource.getHabit('habit-1')) .thenReturn(localHabit); when(() => mockLocalDataSource.saveHabits(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncAll(userId); // Assert // Привычка не должна быть сохранена (так как локальная новее) verifyNever(() => mockLocalDataSource.saveHabit(remoteHabit)); }); test('локально изменённая привычка пушится на сервер', () async { // Arrange const userId = 'user-123'; final localHabit = createTestHabit( id: 'habit-1', userId: userId, title: 'Modified Habit', ); when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn(['habit-1']); when(() => mockLocalDataSource.getHabit('habit-1')) .thenReturn(localHabit); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => []); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockRemoteDataSource.saveHabitsBatch(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncAll(userId); // Assert verify(() => mockRemoteDataSource.saveHabitsBatch([localHabit])) .called(1); }); test( 'если привычка в списке модифицированных, ' 'она не перезаписывается данными с сервера', () async { // Arrange const userId = 'user-123'; final remoteHabit = createTestHabit( id: 'habit-1', userId: userId, title: 'Remote Title', ); final localHabit = createTestHabit( id: 'habit-1', userId: userId, title: 'Local Title', ); when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn(['habit-1']); // Привычка помечена как модифицированная when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => [remoteHabit]); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockLocalDataSource.getHabit('habit-1')) .thenReturn(localHabit); when(() => mockRemoteDataSource.saveHabitsBatch(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncAll(userId); // Assert // Привычка не должна быть перезаписана (т.к. она локально модифицирована) verifyNever(() => mockLocalDataSource.saveHabit(remoteHabit)); // Но она должна быть отправлена на сервер verify(() => mockRemoteDataSource.saveHabitsBatch([localHabit])) .called(1); }); test('успешно обрабатывает пустой список привычек', () async { // Arrange const userId = 'user-123'; when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => []); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncAll(userId); // Assert verify(() => mockLocalDataSource.setLastSyncTime(any())).called(1); verifyNever(() => mockRemoteDataSource.saveHabitsBatch(any())); }); }); group('SyncManager.syncIncremental', () { test('если lastSyncTime == null, вызывается полная синхронизация', () async { // Arrange const userId = 'user-123'; final remoteHabit = createTestHabit(id: 'habit-1', userId: userId); when(() => mockLocalDataSource.getLastSyncTime()) .thenReturn(null); // Нет времени последней синхронизации when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) async => [remoteHabit]); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockLocalDataSource.getHabit('habit-1')).thenReturn(null); when(() => mockLocalDataSource.saveHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncIncremental(userId); // Assert // Должны быть вызваны методы полной синхронизации verify(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .called(1); verify(() => mockLocalDataSource.setLastSyncTime(any())).called(1); }); test('если lastSyncTime присутствует, загружаются только изменённые данные', () async { // Arrange const userId = 'user-123'; final lastSyncTime = DateTime.now().subtract(const Duration(hours: 1)); final modifiedHabit = createTestHabit( id: 'habit-1', userId: userId, updatedAt: DateTime.now(), ); final modifiedExecution = createTestExecution( id: 'exec-1', habitId: 'habit-1', userId: userId, createdAt: DateTime.now(), ); when(() => mockLocalDataSource.getLastSyncTime()) .thenReturn(lastSyncTime); when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsModifiedAfter(userId, lastSyncTime)) .thenAnswer((_) async => [modifiedHabit]); when(() => mockRemoteDataSource.getExecutionsCreatedAfter( userId, lastSyncTime)).thenAnswer((_) async => [modifiedExecution]); when(() => mockLocalDataSource.getHabit('habit-1')).thenReturn(null); when(() => mockLocalDataSource.saveHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act await syncManager.syncIncremental(userId); // Assert verify(() => mockRemoteDataSource.getHabitsModifiedAfter(userId, lastSyncTime)) .called(1); verify(() => mockRemoteDataSource.getExecutionsCreatedAfter( userId, lastSyncTime)).called(1); verify(() => mockLocalDataSource.saveHabit(modifiedHabit)).called(1); verify(() => mockLocalDataSource.saveExecutions([modifiedExecution])) .called(1); }); }); group('SyncManager.syncExecution', () { test('выполнение сохраняется локально', () async { // Arrange const habitId = 'habit-1'; final execution = createTestExecution(habitId: habitId); when(() => mockLocalDataSource.saveExecution(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.saveExecution(any())) .thenAnswer((_) async => {}); // Act await syncManager.syncExecution(execution, habitId); // Assert verify(() => mockLocalDataSource.saveExecution(execution)).called(1); }); test('привычка помечается как модифицированная после выполнения', () async { // Arrange const habitId = 'habit-1'; final execution = createTestExecution(habitId: habitId); when(() => mockLocalDataSource.saveExecution(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.saveExecution(any())) .thenAnswer((_) async => {}); // Act await syncManager.syncExecution(execution, habitId); // Assert verify(() => mockLocalDataSource.markHabitAsModified(habitId)).called(1); }); test('выполнение отправляется на сервер fire-and-forget', () async { // Arrange const habitId = 'habit-1'; final execution = createTestExecution(habitId: habitId); when(() => mockLocalDataSource.saveExecution(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.saveExecution(any())) .thenAnswer((_) async => {}); // Act await syncManager.syncExecution(execution, habitId); // Assert // Даже при fire-and-forget, вызов должен быть инициирован verify(() => mockRemoteDataSource.saveExecution(execution)).called(1); }); test('ошибка локального сохранения пробрасывается', () async { // Arrange const habitId = 'habit-1'; final execution = createTestExecution(habitId: habitId); final error = Exception('Local save failed'); when(() => mockLocalDataSource.saveExecution(any())).thenThrow(error); // Act & Assert expect( () => syncManager.syncExecution(execution, habitId), throwsA(isA<Exception>()), ); }); }); group('SyncManager.deleteHabit', () { test('привычка удаляется локально', () async { // Arrange const habitId = 'habit-1'; const userId = 'user-123'; when(() => mockLocalDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); // Act await syncManager.deleteHabit(habitId, userId); // Assert verify(() => mockLocalDataSource.deleteHabit(habitId)).called(1); }); test('привычка помечается как модифицированная после удаления', () async { // Arrange const habitId = 'habit-1'; const userId = 'user-123'; when(() => mockLocalDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); // Act await syncManager.deleteHabit(habitId, userId); // Assert verify(() => mockLocalDataSource.markHabitAsModified(habitId)).called(1); }); test('удаление отправляется на сервер fire-and-forget', () async { // Arrange const habitId = 'habit-1'; const userId = 'user-123'; when(() => mockLocalDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.markHabitAsModified(any())) .thenAnswer((_) async => {}); when(() => mockRemoteDataSource.deleteHabit(any())) .thenAnswer((_) async => {}); // Act await syncManager.deleteHabit(habitId, userId); // Assert verify(() => mockRemoteDataSource.deleteHabit(habitId)).called(1); }); test('ошибка локального удаления пробрасывается', () async { // Arrange const habitId = 'habit-1'; const userId = 'user-123'; final error = Exception('Local delete failed'); when(() => mockLocalDataSource.deleteHabit(any())).thenThrow(error); // Act & Assert expect( () => syncManager.deleteHabit(habitId, userId), throwsA(isA<Exception>()), ); }); }); group('SyncManager — предотвращение повторной синхронизации', () { test('syncAll не выполняется, если уже идёт синхронизация', () async { // Arrange const userId = 'user-123'; // Первый вызов — пусть ждёт when(() => mockLocalDataSource.getLocallyModifiedHabitIds()) .thenReturn([]); when(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .thenAnswer((_) => Future.delayed( const Duration(milliseconds: 100), () => [createTestHabit(id: 'habit-1', userId: userId)], )); when(() => mockRemoteDataSource.getExecutionsForUser(userId)) .thenAnswer((_) async => []); when(() => mockLocalDataSource.getHabit(any())).thenReturn(null); when(() => mockLocalDataSource.saveHabit(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.saveExecutions(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.setLastSyncTime(any())) .thenAnswer((_) async => {}); when(() => mockLocalDataSource.clearLocallyModifiedHabits()) .thenAnswer((_) async => {}); // Act final firstSync = syncManager.syncAll(userId); // Сразу же попытаемся ещё раз await syncManager.syncAll(userId); await firstSync; // Assert // getHabitsForUserIncludingDeleted должен быть вызван только один раз verify(() => mockRemoteDataSource.getHabitsForUserIncludingDeleted(userId)) .called(1); }); }); }