test(session): cover avatar refresh and session teardown

Add regression tests for avatar reset and token-change refresh, stale 401 handling, idempotent session teardown and account-scoped preference cleanup. Update existing cubit tests for the new session dependency.

Refs: #1113
This commit is contained in:
2026-10-30 15:15:36 +03:00
parent cba2689406
commit 51aff6c175
20 changed files with 684 additions and 67 deletions

View File

@@ -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<dynamic> 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<dynamic>(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>(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,
);
},
);
});
}

View File

@@ -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<WsManager>(mockWsManager);
TokenStorage.clearCache();
await sl.reset();
fakeSession = FakeSessionLifecycleService();
sl.registerSingleton<SessionLifecycleService>(fakeSession);
interceptor = TokenInterceptor();
// Сбрасываем токен перед каждым тестом
try {
await TokenStorage.removeTokenInStorage();
} catch (e) {
// Игнорируем ошибки при очистке
}
await TokenStorage.removeTokenInStorage();
});
group('onRequest', () {
@@ -59,6 +56,7 @@ void main() {
final headers = <String, dynamic>{};
final options = MockRequestOptions();
when(() => options.headers).thenReturn(headers);
when(() => options.extra).thenReturn(<String, dynamic>{});
final handler = MockRequestInterceptorHandler();
when(() => handler.next(any())).thenReturn(null);
@@ -73,6 +71,7 @@ void main() {
final headers = <String, dynamic>{};
final options = MockRequestOptions();
when(() => options.headers).thenReturn(headers);
when(() => options.extra).thenReturn(<String, dynamic>{});
final handler = MockRequestInterceptorHandler();
when(() => handler.next(any())).thenReturn(null);
@@ -89,6 +88,7 @@ void main() {
final headers = <String, dynamic>{};
final options = MockRequestOptions();
when(() => options.headers).thenReturn(headers);
when(() => options.extra).thenReturn(<String, dynamic>{});
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);
});

View File

@@ -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<void> clear() async {
clearCalls++;
}
@override
dynamic noSuchMethod(Invocation invocation) {
throw UnimplementedError('WsManager.${invocation.memberName}');
}
}
class _FakeErrorReporter implements ErrorReporter {
int reportCalls = 0;
@override
Future<void> report(
Object error,
StackTrace? stackTrace, {
String? origin,
Map<String, Object?>? 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<void> 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>(profileAvatarCubit);
sl.registerSingleton<ChatsCubit>(chatsCubit);
sl.registerSingleton<NotificationsCubit>(notificationsCubit);
sl.registerSingleton<NewMessagesCubit>(newMessagesCubit);
sl.registerSingleton<FeedCubit>(feedCubit);
sl.registerSingleton<VideoCallSessionCubit>(videoCallSessionCubit);
sl.registerSingleton<Prefs>(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<Prefs>();
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);
});
});
}

View File

@@ -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<AuthState>());
@@ -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

View File

@@ -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<String, dynamic> 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 => {});

View File

@@ -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<String, dynamic> signInRequestFixture;
late Map<String, dynamic> 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,
);

View File

@@ -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<AuthCubit>());

View File

@@ -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<ProfileAvatarStateLoaded>());
cubit!.reset();
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
await cubit!.ensureFreshForCurrentSession();
verify(() => repository.getMyProfile()).called(2);
expect(cubit!.state, isA<ProfileAvatarStateLoaded>());
});
test('late getMyProfile completion after reset is ignored', () async {
final profileCompleter = Completer<ProfileModel>();
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<ProfileAvatarStateLoading>());
cubit!.reset();
profileCompleter.complete(_profileA);
await pendingRequest;
await pumpEventQueue();
expect(cubit!.state, isA<ProfileAvatarStateLoading>());
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<ProfileAvatarStateLoaded>());
},
);
test(
'ensureFreshForCurrentSession refetches after the token changes',
() async {
final profiles = <ProfileModel>[_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<ProfileAvatarStateError>());
expect(
(cubit!.state as ProfileAvatarStateError).errorMessage,
contains('profile request failed'),
);
});
}

View File

@@ -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();
});

View File

@@ -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<ProfileRepository>()])
@@ -25,12 +27,12 @@ void main() {
});
test('начальное состояние - MenuState', () {
menuCubit = MenuCubit(mockProfileRepository);
menuCubit = MenuCubit(mockProfileRepository, FakeSessionLifecycleService());
expect(menuCubit!.state, isA<MenuState>());
});
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',

View File

@@ -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<ProfileRepository>()])
@@ -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);

View File

@@ -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<RegistrationCubit>());

View File

@@ -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(() {

View File

@@ -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(() {

View File

@@ -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<RegistrationRepository>(), MockSpec<Prefs>()])
@@ -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<RegistrationRepository>(() => repository);
sl.registerLazySingleton<Prefs>(() => 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<RegistrationRepository>(() => repository);
sl.registerLazySingleton<Prefs>(() => prefs);

View File

@@ -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'),

View File

@@ -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())

View File

@@ -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(),
);
});

View File

@@ -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(),
),
);
});

View File

@@ -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<void> onAuthenticated() async {
onAuthenticatedCalls++;
}
@override
Future<void> endSession({required SessionEndReason reason}) async {
endSessionCalls++;
lastEndReason = reason;
}
@override
Future<void> logout() async {
logoutCalls++;
}
}