diff --git a/test/core/interceptors/token_interceptor_session_test.dart b/test/core/interceptors/token_interceptor_session_test.dart new file mode 100644 index 00000000..4c82bd40 --- /dev/null +++ b/test/core/interceptors/token_interceptor_session_test.dart @@ -0,0 +1,118 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../fixtures/doubles/session_doubles.dart'; + +class _RecordingErrorInterceptorHandler extends ErrorInterceptorHandler { + int nextCalls = 0; + DioException? nextError; + + @override + void next(DioException error) { + nextCalls++; + nextError = error; + } + + @override + void resolve(Response response) { + throw UnimplementedError('resolve should not be called'); + } + + @override + void reject( + DioException error, [ + bool callFollowingErrorInterceptor = false, + ]) { + throw UnimplementedError('reject should not be called'); + } +} + +DioException _unauthorized(RequestOptions options) { + return DioException( + requestOptions: options, + response: Response(requestOptions: options, statusCode: 401), + type: DioExceptionType.badResponse, + ); +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late FakeSessionLifecycleService sessionLifecycleService; + late TokenInterceptor interceptor; + + setUp(() async { + SharedPreferences.setMockInitialValues({}); + TokenStorage.clearCache(); + await sl.reset(); + + sessionLifecycleService = FakeSessionLifecycleService(); + sl.registerSingleton(sessionLifecycleService); + interceptor = TokenInterceptor(); + }); + + tearDown(() async { + await sl.reset(); + TokenStorage.clearCache(); + }); + + group('TokenInterceptor.onError 401', () { + test('request without Authorization does not end the session', () async { + await TokenStorage.saveTokenInStorage('current-token'); + + final options = RequestOptions(path: '/api/profile/index'); + final error = _unauthorized(options); + final handler = _RecordingErrorInterceptorHandler(); + + await interceptor.onError(error, handler); + + expect(handler.nextCalls, 1); + expect(handler.nextError, same(error)); + expect(sessionLifecycleService.endSessionCalls, 0); + }); + + test('stale request token does not end the new session', () async { + await TokenStorage.saveTokenInStorage('new-token'); + + final options = RequestOptions(path: '/api/profile/index'); + options.headers['Authorization'] = 'Bearer old-token'; + options.extra['authToken'] = 'old-token'; + + final error = _unauthorized(options); + final handler = _RecordingErrorInterceptorHandler(); + + await interceptor.onError(error, handler); + + expect(handler.nextCalls, 1); + expect(sessionLifecycleService.endSessionCalls, 0); + }); + + test( + 'current request token ends the session once as unauthorized', + () async { + await TokenStorage.saveTokenInStorage('current-token'); + + final options = RequestOptions(path: '/api/profile/index'); + options.headers['Authorization'] = 'Bearer current-token'; + options.extra['authToken'] = 'current-token'; + + final error = _unauthorized(options); + final handler = _RecordingErrorInterceptorHandler(); + + await interceptor.onError(error, handler); + + expect(handler.nextCalls, 1); + expect(sessionLifecycleService.endSessionCalls, 1); + expect( + sessionLifecycleService.lastEndReason, + SessionEndReason.unauthorized, + ); + }, + ); + }); +} diff --git a/test/core/interceptors/token_interceptor_test.dart b/test/core/interceptors/token_interceptor_test.dart index 5995d4a5..d997a13a 100644 --- a/test/core/interceptors/token_interceptor_test.dart +++ b/test/core/interceptors/token_interceptor_test.dart @@ -1,22 +1,24 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/interceptors/token_interceptor.dart'; -import 'package:eatvio_app/core/token_storage.dart'; -import 'package:eatvio_app/core/websockets/ws_manager.dart'; import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../fixtures/doubles/session_doubles.dart'; + class MockRequestOptions extends Mock implements RequestOptions {} -class MockRequestInterceptorHandler extends Mock implements RequestInterceptorHandler {} +class MockRequestInterceptorHandler extends Mock + implements RequestInterceptorHandler {} -class MockErrorInterceptorHandler extends Mock implements ErrorInterceptorHandler {} +class MockErrorInterceptorHandler extends Mock + implements ErrorInterceptorHandler {} class MockResponse extends Mock implements Response {} -class MockWsManager extends Mock implements WsManager {} - class MockDioException extends Mock implements DioException {} class FakeRequestOptions extends Fake implements RequestOptions {} @@ -32,23 +34,18 @@ void main() { }); group('TokenInterceptor', () { late TokenInterceptor interceptor; + late FakeSessionLifecycleService fakeSession; setUp(() async { SharedPreferences.setMockInitialValues({}); - - final mockWsManager = MockWsManager(); - when(() => mockWsManager.clear()).thenAnswer((_) async {}); - - await sl.reset(); // Сбрасываем sl перед каждым тестом - sl.registerSingleton(mockWsManager); - + TokenStorage.clearCache(); + + await sl.reset(); + fakeSession = FakeSessionLifecycleService(); + sl.registerSingleton(fakeSession); + interceptor = TokenInterceptor(); - // Сбрасываем токен перед каждым тестом - try { - await TokenStorage.removeTokenInStorage(); - } catch (e) { - // Игнорируем ошибки при очистке - } + await TokenStorage.removeTokenInStorage(); }); group('onRequest', () { @@ -59,6 +56,7 @@ void main() { final headers = {}; final options = MockRequestOptions(); when(() => options.headers).thenReturn(headers); + when(() => options.extra).thenReturn({}); final handler = MockRequestInterceptorHandler(); when(() => handler.next(any())).thenReturn(null); @@ -73,6 +71,7 @@ void main() { final headers = {}; final options = MockRequestOptions(); when(() => options.headers).thenReturn(headers); + when(() => options.extra).thenReturn({}); final handler = MockRequestInterceptorHandler(); when(() => handler.next(any())).thenReturn(null); @@ -89,6 +88,7 @@ void main() { final headers = {}; final options = MockRequestOptions(); when(() => options.headers).thenReturn(headers); + when(() => options.extra).thenReturn({}); final handler = MockRequestInterceptorHandler(); when(() => handler.next(any())).thenReturn(null); @@ -101,11 +101,16 @@ void main() { }); group('onError', () { - test('удаляет токен и перенаправляет на auth при 401', () async { + test('завершает сессию при 401 для текущего токена', () async { const token = 'test-token'; await TokenStorage.saveTokenInStorage(token); final options = MockRequestOptions(); + when( + () => options.headers, + ).thenReturn({'Authorization': 'Bearer $token'}); + when(() => options.extra).thenReturn({'authToken': token}); + final response = MockResponse(); when(() => response.statusCode).thenReturn(401); @@ -116,14 +121,10 @@ void main() { final handler = MockErrorInterceptorHandler(); when(() => handler.next(any())).thenReturn(null); - // Проверяем, что токен был удален - final tokenBefore = await TokenStorage.getTokenFromStorage(); - expect(tokenBefore, token); - await interceptor.onError(error, handler); - final tokenAfter = await TokenStorage.getTokenFromStorage(); - expect(tokenAfter, null); + expect(fakeSession.endSessionCalls, 1); + expect(fakeSession.lastEndReason, SessionEndReason.unauthorized); verify(() => handler.next(error)).called(1); }); diff --git a/test/core/session/session_lifecycle_service_test.dart b/test/core/session/session_lifecycle_service_test.dart new file mode 100644 index 00000000..ce991a17 --- /dev/null +++ b/test/core/session/session_lifecycle_service_test.dart @@ -0,0 +1,223 @@ +import 'package:eatvio_app/core/error_reporting/error_reporter.dart'; +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/core/websockets/ws_manager.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/new_messages_cubit/new_messages_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/chat/presentation/bloc/chats_cubit/chats_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/notifications/presentation/notifications_cubit/notifications_cubit.dart'; +import 'package:eatvio_app/features/video_calls/presentation/bloc/video_call_session/video_call_session_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class _FakeWsManager implements WsManager { + int clearCalls = 0; + + @override + Future clear() async { + clearCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('WsManager.${invocation.memberName}'); + } +} + +class _FakeErrorReporter implements ErrorReporter { + int reportCalls = 0; + + @override + Future report( + Object error, + StackTrace? stackTrace, { + String? origin, + Map? extras, + }) async { + reportCalls++; + } +} + +class _FakeProfileAvatarCubit implements ProfileAvatarCubit { + int resetCalls = 0; + + @override + void reset() { + resetCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('ProfileAvatarCubit.${invocation.memberName}'); + } +} + +class _FakeChatsCubit implements ChatsCubit { + int resetCalls = 0; + + @override + void reset() { + resetCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('ChatsCubit.${invocation.memberName}'); + } +} + +class _FakeNotificationsCubit implements NotificationsCubit { + int resetCalls = 0; + + @override + void reset() { + resetCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('NotificationsCubit.${invocation.memberName}'); + } +} + +class _FakeNewMessagesCubit implements NewMessagesCubit { + int resetCalls = 0; + + @override + void reset() { + resetCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('NewMessagesCubit.${invocation.memberName}'); + } +} + +class _FakeFeedCubit implements FeedCubit { + int resetCalls = 0; + + @override + void reset() { + resetCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('FeedCubit.${invocation.memberName}'); + } +} + +class _FakeVideoCallSessionCubit implements VideoCallSessionCubit { + int endCallCalls = 0; + + @override + Future endCall() async { + endCallCalls++; + } + + @override + dynamic noSuchMethod(Invocation invocation) { + throw UnimplementedError('VideoCallSessionCubit.${invocation.memberName}'); + } +} + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late _FakeWsManager wsManager; + late _FakeProfileAvatarCubit profileAvatarCubit; + late _FakeChatsCubit chatsCubit; + late _FakeNotificationsCubit notificationsCubit; + late _FakeNewMessagesCubit newMessagesCubit; + late _FakeFeedCubit feedCubit; + late _FakeVideoCallSessionCubit videoCallSessionCubit; + late SessionLifecycleService service; + + setUp(() async { + SharedPreferences.setMockInitialValues({ + 'token': 'token-a', + 'socketId': 'socket-a', + PrefsKeys.customBaseUrl.name: 'https://old.example', + PrefsKeys.isEmailVerificationSkipped.name: true, + PrefsKeys.registrationDate.name: '2024-01-01', + PrefsKeys.welcomeScreenWasShowed.name: true, + }); + TokenStorage.clearCache(); + await sl.reset(); + + wsManager = _FakeWsManager(); + profileAvatarCubit = _FakeProfileAvatarCubit(); + chatsCubit = _FakeChatsCubit(); + notificationsCubit = _FakeNotificationsCubit(); + newMessagesCubit = _FakeNewMessagesCubit(); + feedCubit = _FakeFeedCubit(); + videoCallSessionCubit = _FakeVideoCallSessionCubit(); + + sl.registerSingleton(profileAvatarCubit); + sl.registerSingleton(chatsCubit); + sl.registerSingleton(notificationsCubit); + sl.registerSingleton(newMessagesCubit); + sl.registerSingleton(feedCubit); + sl.registerSingleton(videoCallSessionCubit); + sl.registerSingleton(Prefs(await SharedPreferences.getInstance())); + + service = SessionLifecycleService( + wsManager: wsManager, + errorReporter: _FakeErrorReporter(), + ); + }); + + tearDown(() async { + await sl.reset(); + TokenStorage.clearCache(); + }); + + group('SessionLifecycleService.endSession', () { + test( + 'resets user-scoped state and clears only session-scoped keys', + () async { + await service.endSession(reason: SessionEndReason.logout); + + expect(wsManager.clearCalls, 1); + expect(profileAvatarCubit.resetCalls, 1); + expect(chatsCubit.resetCalls, 1); + expect(notificationsCubit.resetCalls, 1); + expect(newMessagesCubit.resetCalls, 1); + expect(feedCubit.resetCalls, 1); + expect(videoCallSessionCubit.endCallCalls, 1); + + expect(await TokenStorage.getTokenFromStorage(), isNull); + expect(await SocketIdStorage.getSocketIdFromStorage(), isNull); + + final prefs = sl(); + expect(prefs.getString(PrefsKeys.customBaseUrl), 'https://old.example'); + expect(prefs.getBool(PrefsKeys.isEmailVerificationSkipped), isNull); + expect(prefs.getString(PrefsKeys.registrationDate), isNull); + expect(prefs.getBool(PrefsKeys.welcomeScreenWasShowed), isTrue); + }, + ); + + test('is idempotent for concurrent calls', () async { + final first = service.endSession(reason: SessionEndReason.logout); + final second = service.endSession(reason: SessionEndReason.unauthorized); + + await Future.wait([first, second]); + + expect(wsManager.clearCalls, 1); + expect(profileAvatarCubit.resetCalls, 1); + expect(chatsCubit.resetCalls, 1); + expect(notificationsCubit.resetCalls, 1); + expect(newMessagesCubit.resetCalls, 1); + expect(feedCubit.resetCalls, 1); + expect(videoCallSessionCubit.endCallCalls, 1); + + expect(await TokenStorage.getTokenFromStorage(), isNull); + expect(await SocketIdStorage.getSocketIdFromStorage(), isNull); + }); + }); +} diff --git a/test/features/auth/presentation/bloc/auth_cubit/auth_cubit_test.dart b/test/features/auth/presentation/bloc/auth_cubit/auth_cubit_test.dart index 65bb710e..27f3c737 100644 --- a/test/features/auth/presentation/bloc/auth_cubit/auth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/auth_cubit/auth_cubit_test.dart @@ -12,6 +12,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../../../fixtures/doubles/session_doubles.dart'; import 'auth_cubit_test.mocks.dart'; @GenerateNiceMocks([ @@ -75,6 +76,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); expect(cubit!.state, isA()); @@ -86,6 +88,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); const email = 'test@example.com'; @@ -116,6 +119,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); const email = 'test@example.com'; @@ -140,6 +144,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); when(mockPrefs.getBool(any)).thenReturn(false); @@ -159,6 +164,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); when(mockPrefs.getBool(any)).thenReturn(true); @@ -174,6 +180,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); when(mockFirebaseApi.registerPushToken()) @@ -190,6 +197,7 @@ void main() { profileRepository: mockProfileRepository, firebaseApi: mockFirebaseApi, prefs: mockPrefs, + sessionLifecycleService: FakeSessionLifecycleService(), ); // TokenStorage.haveTokenInStorage() вернет false diff --git a/test/features/auth/presentation/bloc/auth_cubit_test.dart b/test/features/auth/presentation/bloc/auth_cubit_test.dart index 790775b0..cc57e231 100644 --- a/test/features/auth/presentation/bloc/auth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/auth_cubit_test.dart @@ -13,6 +13,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../../../fixtures/doubles/auth_doubles.dart'; import '../../../../fixtures/doubles/profile_doubles.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import '../../../../fixtures/fixture_reader.dart'; import 'auth_cubit_test.mocks.dart'; @@ -26,6 +27,7 @@ void main() { late FixtureProfileRepository profileRepository; late MockPrefs mockPrefs; late MockFirebaseApi mockFirebaseApi; + late FakeSessionLifecycleService fakeSession; late Map credentialsFixture; late TokenModel tokenFixture; @@ -37,6 +39,7 @@ void main() { profileRepository = FixtureProfileRepository(); mockPrefs = MockPrefs(); mockFirebaseApi = MockFirebaseApi(); + fakeSession = FakeSessionLifecycleService(); SharedPreferences.setMockInitialValues({}); // Mock Fluttertoast plugin @@ -59,12 +62,12 @@ void main() { }); test('начальное состояние - AuthState', () { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); expect(authCubit!.state, const AuthState()); }); test('getTokenByCredentials - успешная авторизация', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String); @@ -77,7 +80,7 @@ void main() { }); test('getTokenByCredentials - переход в состояние loading', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); authRepository.responseDelay = const Duration(milliseconds: 50); final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String); @@ -88,7 +91,7 @@ void main() { }); test('getTokenByCredentials - обработка ошибки авторизации', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); authRepository.error = Exception('Неверный адрес электронной почты или пароль'); final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword'); @@ -102,7 +105,7 @@ void main() { }); test('getTokenByCredentials - обработка различных типов ошибок', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); authRepository.error = const FormatException('Invalid credentials format'); final result = await authCubit!.getTokenByCredentials('invalid', 'pass'); @@ -115,7 +118,7 @@ void main() { }); test('getTokenByCredentials - проверка вызова с правильными параметрами', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123'); @@ -124,7 +127,7 @@ void main() { }); test('getTokenByCredentials - множественные вызовы', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); await authCubit!.getTokenByCredentials('first@test.com', 'password1'); await Future.delayed(const Duration(milliseconds: 100)); @@ -138,7 +141,7 @@ void main() { }); test('registerAnonymousPushToken - регистрирует токен если не был отправлен', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(false); when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {}); when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {}); @@ -150,7 +153,7 @@ void main() { }); test('registerAnonymousPushToken - не регистрирует токен если уже был отправлен', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(true); await authCubit!.registerAnonymousPushToken(); @@ -159,7 +162,7 @@ void main() { }); test('registerPushToken - регистрирует push токен', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); when(mockFirebaseApi.registerPushToken()).thenAnswer((_) async => {}); await authCubit!.registerPushToken(); @@ -168,7 +171,7 @@ void main() { }); test('registerAnonymousPushToken - обрабатывает null значение hasAnonymousTokenBeenSent', () async { - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: fakeSession); when(mockPrefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent)).thenReturn(null); when(mockFirebaseApi.registerAnonymousPushToken()).thenAnswer((_) async => {}); when(mockPrefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true)).thenAnswer((_) async => {}); diff --git a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart index 3f757bb1..443ee7e6 100644 --- a/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart @@ -14,6 +14,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../../../fixtures/doubles/session_doubles.dart'; import '../../../../../fixtures/fixture_reader.dart'; import 'oauth_cubit_test.mocks.dart'; @@ -37,6 +38,7 @@ void main() { late MockAuthRepository mockAuthRepository; late MockPrefs mockPrefs; late _RecordingFirebaseApi firebaseApi; + late FakeSessionLifecycleService fakeSession; late TokenModel tokenFixture; late Map signInRequestFixture; late Map signUpRequestFixture; @@ -55,6 +57,7 @@ void main() { mockAuthRepository = MockAuthRepository(); mockPrefs = MockPrefs(); firebaseApi = _RecordingFirebaseApi(); + fakeSession = FakeSessionLifecycleService(); SharedPreferences.setMockInitialValues({}); @@ -87,7 +90,7 @@ void main() { test('начальное состояние - OAuthState', () { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); expect(oauthCubit!.state, const OAuthState()); @@ -96,7 +99,7 @@ void main() { test('signInViaSocialMedia - успешный вход через соцсеть', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -129,7 +132,7 @@ void main() { test('signInViaSocialMedia - переход в состояние loading', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -160,7 +163,7 @@ void main() { test('signInViaSocialMedia - обработка ошибки', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -189,7 +192,7 @@ void main() { test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -237,8 +240,7 @@ void main() { final loadedState = oauthCubit!.state as OAuthStateLoaded; expect(loadedState.token, tokenFixture.token); expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token); - expect(firebaseApi.registerPushTokenCalls, 1); - expect(firebaseApi.hadAuthToken, isTrue); + expect(fakeSession.onAuthenticatedCalls, 1); }); test( @@ -246,7 +248,7 @@ void main() { () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -289,7 +291,7 @@ void main() { test('signUpViaSocialMedia - обработка ошибки валидации email', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -350,7 +352,7 @@ void main() { () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -379,7 +381,7 @@ void main() { test('requestOAuthVerification - обработка ошибки', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -403,7 +405,7 @@ void main() { () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); @@ -458,15 +460,14 @@ void main() { final loadedState = oauthCubit!.state as OAuthStateLoaded; expect(loadedState.token, tokenFixture.token); expect(await TokenStorage.getTokenFromStorage(), tokenFixture.token); - expect(firebaseApi.registerPushTokenCalls, 1); - expect(firebaseApi.hadAuthToken, isTrue); + expect(fakeSession.onAuthenticatedCalls, 1); }, ); test('confirmOAuthVerification - обработка ошибки', () async { oauthCubit = OAuthCubit( authRepository: mockAuthRepository, - firebaseApi: firebaseApi, + firebaseApi: firebaseApi, sessionLifecycleService: fakeSession, prefs: mockPrefs, ); diff --git a/test/features/auth/presentation/widgets/basic_widget_test.dart b/test/features/auth/presentation/widgets/basic_widget_test.dart index 05c2e2e3..0fac7d35 100644 --- a/test/features/auth/presentation/widgets/basic_widget_test.dart +++ b/test/features/auth/presentation/widgets/basic_widget_test.dart @@ -15,6 +15,7 @@ import 'package:shared_preferences/shared_preferences.dart'; import '../../../../fixtures/doubles/auth_doubles.dart'; import '../../../../fixtures/doubles/profile_doubles.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import '../../../../fixtures/fixture_reader.dart'; import 'basic_widget_test.mocks.dart'; @@ -36,7 +37,7 @@ void main() { mockFirebaseApi = MockFirebaseApi(); SharedPreferences.setMockInitialValues({}); - authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService()); }); tearDown(() { @@ -55,7 +56,7 @@ void main() { testWidgets('should create AuthCubit successfully', (WidgetTester tester) async { // Arrange & Act - final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); + final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi, sessionLifecycleService: FakeSessionLifecycleService()); // Assert expect(cubit, isA()); diff --git a/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_session_test.dart b/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_session_test.dart new file mode 100644 index 00000000..0535e564 --- /dev/null +++ b/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_session_test.dart @@ -0,0 +1,204 @@ +import 'dart:async'; + +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; +import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; +import 'package:eatvio_app/features/profile/models/profile_model.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class MockProfileRepository extends Mock implements ProfileRepository {} + +const _profileA = ProfileModel( + id: 1, + slug: 'user-a', + name: 'User A', + email: 'a@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: 'avatars/a.png', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], +); + +const _profileB = ProfileModel( + id: 2, + slug: 'user-b', + name: 'User B', + email: 'b@example.com', + phoneConfirmationState: 'confirmed', + isPublicProfile: true, + isBlockedByMe: false, + isBlockedByAuthor: false, + isBlockedByUser: false, + isEntitySubscribedByMe: false, + profilePhotoUrl: 'avatars/b.png', + roles: [], + badges: [], + subscriptions: [], + subscribers: [], +); + +const _details = ProfileDetailsModel( + firstName: 'User', + lastName: 'Test', + birthdate: '1990-01-01', + gender: 'male', + country: 'Russia', + city: 'Moscow', + lang: 'ru', + weight: 70.0, + height: 175, + chestCircumference: 100, + waistCircumference: 80, + hipCircumference: 90, + bloodPressureUp: 120, + bloodPressureDown: 80, + hasHypo: false, + hasHyper: false, + aboutAlergy: '-', + aboutTakingPils: '-', + aboutSportHobbies: '-', + favoriteFoods: '-', + unfavoriteFoods: '-', + additionalInfo: '-', + isMoodFillable: true, + isBedtimeFillable: true, + activityCoefficient: 1.5, +); + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + late MockProfileRepository repository; + ProfileAvatarCubit? cubit; + + setUp(() async { + await sl.reset(); + SharedPreferences.setMockInitialValues({'token': 'token-a'}); + TokenStorage.clearCache(); + repository = MockProfileRepository(); + }); + + tearDown(() async { + if (cubit != null && !cubit!.isClosed) { + await cubit!.close(); + } + cubit = null; + await sl.reset(); + TokenStorage.clearCache(); + }); + + void stubSuccessfulProfile(ProfileModel profile) { + when(() => repository.getMyProfile()).thenAnswer((_) async => profile); + when( + () => repository.getMyProfileDetails(), + ).thenAnswer((_) async => _details); + } + + test('reset emits loading and clears the cached profile', () async { + stubSuccessfulProfile(_profileA); + cubit = ProfileAvatarCubit(profileRepository: repository); + + await cubit!.ensureFreshForCurrentSession(); + expect(cubit!.state, isA()); + + cubit!.reset(); + expect(cubit!.state, isA()); + + await cubit!.ensureFreshForCurrentSession(); + + verify(() => repository.getMyProfile()).called(2); + expect(cubit!.state, isA()); + }); + + test('late getMyProfile completion after reset is ignored', () async { + final profileCompleter = Completer(); + when( + () => repository.getMyProfile(), + ).thenAnswer((_) => profileCompleter.future); + when( + () => repository.getMyProfileDetails(), + ).thenAnswer((_) async => _details); + + cubit = ProfileAvatarCubit(profileRepository: repository); + + final pendingRequest = cubit!.getMyProfile(); + await pumpEventQueue(); + expect(cubit!.state, isA()); + + cubit!.reset(); + profileCompleter.complete(_profileA); + await pendingRequest; + await pumpEventQueue(); + + expect(cubit!.state, isA()); + verifyNever(() => repository.getMyProfileDetails()); + }); + + test( + 'ensureFreshForCurrentSession does not refetch for the same token', + () async { + stubSuccessfulProfile(_profileA); + cubit = ProfileAvatarCubit(profileRepository: repository); + + await cubit!.ensureFreshForCurrentSession(); + await cubit!.ensureFreshForCurrentSession(); + + verify(() => repository.getMyProfile()).called(1); + expect(cubit!.state, isA()); + }, + ); + + test( + 'ensureFreshForCurrentSession refetches after the token changes', + () async { + final profiles = [_profileA, _profileB]; + when( + () => repository.getMyProfile(), + ).thenAnswer((_) async => profiles.removeAt(0)); + when( + () => repository.getMyProfileDetails(), + ).thenAnswer((_) async => _details); + + cubit = ProfileAvatarCubit(profileRepository: repository); + + await cubit!.ensureFreshForCurrentSession(); + final firstState = cubit!.state as ProfileAvatarStateLoaded; + expect(firstState.profile.id, _profileA.id); + + await TokenStorage.saveTokenInStorage('token-b'); + await cubit!.ensureFreshForCurrentSession(); + + final secondState = cubit!.state as ProfileAvatarStateLoaded; + expect(secondState.profile.id, _profileB.id); + expect(secondState.profile.profilePhotoUrl, _profileB.profilePhotoUrl); + verify(() => repository.getMyProfile()).called(2); + }, + ); + + test('errors emit ProfileAvatarStateError and do not navigate', () async { + when( + () => repository.getMyProfile(), + ).thenAnswer((_) async => throw Exception('profile request failed')); + + cubit = ProfileAvatarCubit(profileRepository: repository); + + await cubit!.getMyProfile(); + + expect(cubit!.state, isA()); + expect( + (cubit!.state as ProfileAvatarStateError).errorMessage, + contains('profile request failed'), + ); + }); +} diff --git a/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_test.dart b/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_test.dart index 61d515c1..981bfff4 100644 --- a/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_test.dart +++ b/test/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit_test.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_cubit.dart'; import 'package:eatvio_app/features/bottom_navigation_bar/presentation/bloc/profile_avatar_cubit/profile_avatar_state.dart'; import 'package:eatvio_app/features/profile/models/profile_model.dart'; @@ -5,6 +6,7 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import 'profile_avatar_cubit_test.mocks.dart'; @@ -16,6 +18,8 @@ void main() { late MockProfileRepository mockRepository; setUp(() { + SharedPreferences.setMockInitialValues({'token': 'test-token'}); + TokenStorage.clearCache(); mockRepository = MockProfileRepository(); }); diff --git a/test/features/menu/presentation/bloc/menu_cubit_test.dart b/test/features/menu/presentation/bloc/menu_cubit_test.dart index 9cdf18d3..e7dd9a9c 100644 --- a/test/features/menu/presentation/bloc/menu_cubit_test.dart +++ b/test/features/menu/presentation/bloc/menu_cubit_test.dart @@ -4,6 +4,8 @@ import 'package:eatvio_app/features/profile/repositories/profile_repository.dart import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; + +import '../../../../fixtures/doubles/session_doubles.dart'; import 'menu_cubit_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) @@ -25,12 +27,12 @@ void main() { }); test('начальное состояние - MenuState', () { - menuCubit = MenuCubit(mockProfileRepository); + menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService()); expect(menuCubit!.state, isA()); }); test('makePassKey - успешно создает passKey', () async { - menuCubit = MenuCubit(mockProfileRepository); + menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService()); const expectedPassKey = 'test-pass-key'; when(mockProfileRepository.getPassKey( @@ -48,7 +50,7 @@ void main() { }); test('makePassKey - возвращает null при ошибке', () async { - menuCubit = MenuCubit(mockProfileRepository); + menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService()); when(mockProfileRepository.getPassKey( routeName: 'product_nutritional_value.index', diff --git a/test/features/menu/presentation/pages/menu_screen_smoke_test.dart b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart index 1facf0bb..cfc1ddf8 100644 --- a/test/features/menu/presentation/pages/menu_screen_smoke_test.dart +++ b/test/features/menu/presentation/pages/menu_screen_smoke_test.dart @@ -15,6 +15,7 @@ import 'package:go_router/go_router.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import 'menu_screen_smoke_test.mocks.dart'; @GenerateNiceMocks([MockSpec()]) @@ -138,7 +139,7 @@ void main() { testWidgets('renders menu screen for user', (tester) async { final repository = MockProfileRepository(); - final cubit = MenuCubit(repository); + final cubit = MenuCubit(repository, FakeSessionLifecycleService()); when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); when(repository.getMyProfile()).thenAnswer((_) async => mockUserProfile); @@ -155,7 +156,7 @@ void main() { testWidgets('renders menu screen for consultant', (tester) async { final repository = MockProfileRepository(); - final cubit = MenuCubit(repository); + final cubit = MenuCubit(repository, FakeSessionLifecycleService()); when(repository.getPassKey(routeName: anyNamed('routeName'), routeParams: anyNamed('routeParams'))).thenAnswer((_) async => 'test-passkey'); when(repository.getMyProfile()).thenAnswer((_) async => mockConsultantProfile); diff --git a/test/features/registration/basic_widget_test.dart b/test/features/registration/basic_widget_test.dart index 16995a82..2b0a6e29 100644 --- a/test/features/registration/basic_widget_test.dart +++ b/test/features/registration/basic_widget_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import '../../fixtures/doubles/session_doubles.dart'; import 'basic_widget_test.mocks.dart'; @GenerateMocks([RegistrationRepository, Prefs]) @@ -19,7 +20,7 @@ void main() { setUp(() { mockRegistrationRepository = MockRegistrationRepository(); - registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs()); + registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService()); }); tearDown(() { @@ -38,7 +39,7 @@ void main() { testWidgets('should create RegistrationCubit successfully', (WidgetTester tester) async { // Arrange & Act - final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs()); + final cubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService()); // Assert expect(cubit, isA()); diff --git a/test/features/registration/integration_widget_test.dart b/test/features/registration/integration_widget_test.dart index e5559a80..0e35d009 100644 --- a/test/features/registration/integration_widget_test.dart +++ b/test/features/registration/integration_widget_test.dart @@ -7,6 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import '../../fixtures/doubles/session_doubles.dart'; import 'integration_widget_test.mocks.dart'; @GenerateMocks([RegistrationRepository, Prefs]) @@ -17,7 +18,7 @@ void main() { setUp(() { mockRegistrationRepository = MockRegistrationRepository(); - registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs()); + registrationCubit = RegistrationCubit(registrationRepository: mockRegistrationRepository, prefs: MockPrefs(), sessionLifecycleService: FakeSessionLifecycleService()); }); tearDown(() { diff --git a/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart b/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart index 11bfe5d6..3f108a79 100644 --- a/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart +++ b/test/features/registration/presentation/bloc/registration_cubit/registration_cubit_test.dart @@ -7,6 +7,8 @@ import 'package:eatvio_app/features/registration/repositories/registration_repos import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import '../../../../../fixtures/doubles/session_doubles.dart'; + class MockRegistrationRepository extends Mock implements RegistrationRepository {} class MockPrefs extends Mock implements Prefs {} @@ -26,7 +28,7 @@ void main() { setUp(() { mockRepository = MockRegistrationRepository(); mockPrefs = MockPrefs(); - cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs); + cubit = RegistrationCubit(registrationRepository: mockRepository, prefs: mockPrefs, sessionLifecycleService: FakeSessionLifecycleService()); }); tearDown(() { diff --git a/test/features/registration/presentation/pages/registration_screen_smoke_test.dart b/test/features/registration/presentation/pages/registration_screen_smoke_test.dart index d3b60f15..4fc28966 100644 --- a/test/features/registration/presentation/pages/registration_screen_smoke_test.dart +++ b/test/features/registration/presentation/pages/registration_screen_smoke_test.dart @@ -13,6 +13,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import 'registration_screen_smoke_test.mocks.dart'; @GenerateNiceMocks([MockSpec(), MockSpec()]) @@ -44,7 +45,7 @@ void main() { testWidgets('renders registration screen with initial step', (tester) async { final repository = MockRegistrationRepository(); final prefs = MockPrefs(); - final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs); + final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService()); sl.registerLazySingleton(() => repository); sl.registerLazySingleton(() => prefs); @@ -59,7 +60,7 @@ void main() { testWidgets('renders phone screen as first step', (tester) async { final repository = MockRegistrationRepository(); final prefs = MockPrefs(); - final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs); + final cubit = RegistrationCubit(registrationRepository: repository, prefs: prefs, sessionLifecycleService: FakeSessionLifecycleService()); sl.registerLazySingleton(() => repository); sl.registerLazySingleton(() => prefs); diff --git a/test/features/registration_v2/cubit/registration_cubit_v2_test.dart b/test/features/registration_v2/cubit/registration_cubit_v2_test.dart index e2d3d8ab..98f4ac25 100644 --- a/test/features/registration_v2/cubit/registration_cubit_v2_test.dart +++ b/test/features/registration_v2/cubit/registration_cubit_v2_test.dart @@ -9,6 +9,7 @@ import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../../../fixtures/doubles/session_doubles.dart'; import 'registration_cubit_v2_test.mocks.dart'; class _RecordingFirebaseApi extends Fake implements FirebaseApi { @@ -30,6 +31,7 @@ void main() { late RegistrationCubitV2 cubit; late MockRegistrationRepositoryV2 mockRepository; late _RecordingFirebaseApi firebaseApi; + late FakeSessionLifecycleService fakeSession; const MethodChannel passwordGeneratorChannel = MethodChannel( 'flutter_password_generator', ); @@ -37,9 +39,11 @@ void main() { setUp(() { mockRepository = MockRegistrationRepositoryV2(); firebaseApi = _RecordingFirebaseApi(); + fakeSession = FakeSessionLifecycleService(); cubit = RegistrationCubitV2( registrationRepository: mockRepository, firebaseApi: firebaseApi, + sessionLifecycleService: fakeSession, ); // Инициализируем SharedPreferences для тестов @@ -212,8 +216,7 @@ void main() { expect(result.$1, isTrue); expect(result.$2, isFalse); expect(await TokenStorage.getTokenFromStorage(), token); - expect(firebaseApi.registerPushTokenCalls, 1); - expect(firebaseApi.hadAuthToken, isTrue); + expect(fakeSession.onAuthenticatedCalls, 1); verify( mockRepository.register( registrationModel: anyNamed('registrationModel'), diff --git a/test/features/settings/presentation/bloc/settings_cubit_test.dart b/test/features/settings/presentation/bloc/settings_cubit_test.dart index 2733bd07..674492c4 100644 --- a/test/features/settings/presentation/bloc/settings_cubit_test.dart +++ b/test/features/settings/presentation/bloc/settings_cubit_test.dart @@ -1,3 +1,4 @@ +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; import 'package:eatvio_app/features/consultant/become_consultant/repositories/consultant_repository.dart'; import 'package:eatvio_app/features/consultations/repositories/consultations_repository.dart'; import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; @@ -7,6 +8,7 @@ import 'package:eatvio_app/features/settings/repositories/service_repository.dar import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import '../../../../fixtures/settings_fixtures.dart'; import 'settings_cubit_test.mocks.dart'; @@ -23,6 +25,7 @@ void main() { late MockSettingsRepository mockSettingsRepository; late MockConsultantRepository mockConsultantRepository; late MockConsultationsRepository mockConsultationsRepository; + late FakeSessionLifecycleService fakeSession; setUp(() { @@ -30,6 +33,7 @@ void main() { mockSettingsRepository = MockSettingsRepository(); mockConsultantRepository = MockConsultantRepository(); mockConsultationsRepository = MockConsultationsRepository(); + fakeSession = FakeSessionLifecycleService(); }); tearDown(() async { @@ -46,6 +50,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); expect(settingsCubit!.state, const SettingsState.loading()); }); @@ -56,6 +61,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.getMyProfile()) @@ -86,6 +92,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); // Ошибка происходит при загрузке настроек профиля (внутри try-catch блока) @@ -114,6 +121,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.getMyProfile()) @@ -145,6 +153,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.getMyProfile()) @@ -176,6 +185,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.deleteAccount()) @@ -184,6 +194,8 @@ void main() { final result = await settingsCubit!.deleteAccount(); expect(result, true); + expect(fakeSession.endSessionCalls, 1); + expect(fakeSession.lastEndReason, SessionEndReason.accountDeleted); verify(mockProfileRepository.deleteAccount()).called(1); }); @@ -193,6 +205,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.deleteAccount()) @@ -209,6 +222,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.getMyProfile()) @@ -244,6 +258,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: fakeSession, ); when(mockProfileRepository.getMyProfile()) diff --git a/test/features/settings/presentation/pages/change_password_screen_test.dart b/test/features/settings/presentation/pages/change_password_screen_test.dart index fe576e3b..c46d2c56 100644 --- a/test/features/settings/presentation/pages/change_password_screen_test.dart +++ b/test/features/settings/presentation/pages/change_password_screen_test.dart @@ -6,6 +6,7 @@ import 'package:eatvio_app/features/settings/presentation/bloc/settings_cubit/se import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_test/flutter_test.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import '../../../../fixtures/settings_fixtures.dart'; import '../bloc/settings_cubit_test.mocks.dart'; @@ -27,6 +28,7 @@ void main() { settingsRepository: mockSettingsRepository, consultantRepository: mockConsultantRepository, consultationsRepository: mockConsultationsRepository, + sessionLifecycleService: FakeSessionLifecycleService(), ); }); diff --git a/test/features/settings/presentation/pages/settings_screen_smoke_test.dart b/test/features/settings/presentation/pages/settings_screen_smoke_test.dart index 92cb508a..4f39560f 100644 --- a/test/features/settings/presentation/pages/settings_screen_smoke_test.dart +++ b/test/features/settings/presentation/pages/settings_screen_smoke_test.dart @@ -18,6 +18,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; +import '../../../../fixtures/doubles/session_doubles.dart'; import 'settings_screen_smoke_test.mocks.dart'; @GenerateNiceMocks([ @@ -57,6 +58,7 @@ void main() { settingsRepository: sl(), consultantRepository: sl(), consultationsRepository: sl(), + sessionLifecycleService: FakeSessionLifecycleService(), ), ); }); diff --git a/test/fixtures/doubles/session_doubles.dart b/test/fixtures/doubles/session_doubles.dart new file mode 100644 index 00000000..09b07b83 --- /dev/null +++ b/test/fixtures/doubles/session_doubles.dart @@ -0,0 +1,24 @@ +import 'package:eatvio_app/core/session/session_lifecycle_service.dart'; + +class FakeSessionLifecycleService implements SessionLifecycleService { + int onAuthenticatedCalls = 0; + int endSessionCalls = 0; + int logoutCalls = 0; + SessionEndReason? lastEndReason; + + @override + Future onAuthenticated() async { + onAuthenticatedCalls++; + } + + @override + Future endSession({required SessionEndReason reason}) async { + endSessionCalls++; + lastEndReason = reason; + } + + @override + Future logout() async { + logoutCalls++; + } +}