diff --git a/lib/core/di/cubit_registrations.dart b/lib/core/di/cubit_registrations.dart index e01596f8..d5b109e4 100644 --- a/lib/core/di/cubit_registrations.dart +++ b/lib/core/di/cubit_registrations.dart @@ -76,7 +76,7 @@ class CubitRegistrations { /// Регистрация Cubit'ов как фабрик static void _registerFactoryCubits() { // Auth related cubits - sl.registerFactory(() => AuthCubit(authRepository: sl())); + sl.registerFactory(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl())); sl.registerFactory(() => RegistrationCubit(registrationRepository: sl(), prefs: sl())); sl.registerFactory(() => ResetPasswordCubit(resetPasswordRepository: sl())); sl.registerFactory(() => VerifyEmailCubit(profileRepository: sl())); diff --git a/lib/core/routing/other_routes.dart b/lib/core/routing/other_routes.dart index 1110f88a..28d85d36 100644 --- a/lib/core/routing/other_routes.dart +++ b/lib/core/routing/other_routes.dart @@ -1,11 +1,9 @@ import 'package:dio/dio.dart'; import 'package:eatvio_app/core/app_config.dart'; -import 'package:eatvio_app/core/firebase_api.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/routing/route_constants.dart'; -import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/features/calendar/presentation/pages/calendar_screen.dart'; import 'package:eatvio_app/features/chat/presentation/pages/chat_screen.dart'; import 'package:eatvio_app/features/diary/presentation/pages/diary_screen.dart'; @@ -59,23 +57,14 @@ class OtherRoutes { redirect: (context, state) async { try { final prefs = sl(); - final firebaseApi = sl(); - final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; - await firebaseApi.registerPushToken(); + final welcomeScreenWasShowed = prefs.getBool(PrefsKeys.welcomeScreenWasShowed) ?? false; if (!welcomeScreenWasShowed) { return null; - } else if (await TokenStorage.haveTokenInStorage()) { - return '/feed-screen'; } else { - final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; - if (!hasAnonymousTokenBeenSent) { - await firebaseApi.registerAnonymousPushToken(); - prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); - } return '/auth-screen'; } } catch (e) { - return '/feed-screen'; + return '/auth-screen'; } }, ), @@ -247,7 +236,7 @@ class OtherRoutes { GoRoute( path: PersonalDietSuggestionScreen.path, name: PersonalDietSuggestionScreen.name, - builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int,), + builder: (context, state) => PersonalDietSuggestionScreen(dietId: state.extra as int), ), ]; } diff --git a/lib/features/auth/models/oauth_verification_request.dart b/lib/features/auth/models/oauth_verification_request.dart deleted file mode 100644 index 041791d6..00000000 --- a/lib/features/auth/models/oauth_verification_request.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'oauth_verification_request.freezed.dart'; -part 'oauth_verification_request.g.dart'; - -@freezed -abstract class OAuthVerificationRequest with _$OAuthVerificationRequest { - const factory OAuthVerificationRequest({String? email, String? phone}) = _OAuthVerificationRequest; - - factory OAuthVerificationRequest.fromJson(Map json) => _$OAuthVerificationRequestFromJson(json); -} - - - - - - - - - - - - diff --git a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart index 6f1a59f5..4e3c7db9 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart @@ -1,15 +1,26 @@ import 'dart:developer'; +import 'package:eatvio_app/core/app_router.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/prefs_keys.dart'; +import 'package:eatvio_app/core/routing/route_constants.dart'; import 'package:eatvio_app/core/token_storage.dart'; import 'package:eatvio_app/core/utils/toasts.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; +import 'package:go_router/go_router.dart'; class AuthCubit extends Cubit { final AuthRepository authRepository; + final ProfileRepository profileRepository; + final FirebaseApi firebaseApi; + final Prefs prefs; - AuthCubit({required this.authRepository}) : super(const AuthState()); + AuthCubit({required this.authRepository, required this.prefs, required this.profileRepository, required this.firebaseApi}) : super(const AuthState()); Future getTokenByCredentials(String email, String password) async { emit(const AuthState.loading()); @@ -25,4 +36,32 @@ class AuthCubit extends Cubit { return false; } } + + Future initializeUser() async { + await registerAnonymousPushToken(); + if (await TokenStorage.haveTokenInStorage()) { + final profile = await profileRepository.getMyProfile(); + if (profile.emailVerifiedAt != null) { + await registerPushToken(); + AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen); + return; + } + AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen); + } else { + await registerAnonymousPushToken(); + FlutterNativeSplash.remove(); + } + } + + Future registerAnonymousPushToken() async { + final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false; + if (!hasAnonymousTokenBeenSent) { + await firebaseApi.registerAnonymousPushToken(); + prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true); + } + } + + Future registerPushToken() async { + await firebaseApi.registerPushToken(); + } } diff --git a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart index 22f65f42..daf156a8 100644 --- a/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart +++ b/lib/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart @@ -126,8 +126,10 @@ class OAuthCubit extends Cubit with RegistrationDateCacheMixin { try { emit(const OAuthState.loading()); log('OAuthCubit: Requesting verification code for email: $email, phone: $phone'); - final response = await authRepository.requestOAuthVerification(email: email, phone: phone); - log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}'); + final response = await authRepository.requestOAuthVerification(email: null, phone: phone); + if (response != null) { + log('OAuthCubit: Verification code requested successfully, masked email: ${response.email}'); + } // Сбрасываем состояние после успешного запроса кода emit(const OAuthState()); return response; diff --git a/lib/features/auth/presentation/pages/auth_screen.dart b/lib/features/auth/presentation/pages/auth_screen.dart index 0220c9fb..bf9074d9 100644 --- a/lib/features/auth/presentation/pages/auth_screen.dart +++ b/lib/features/auth/presentation/pages/auth_screen.dart @@ -28,7 +28,6 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_keyboard_visibility/flutter_keyboard_visibility.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:go_router/go_router.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -232,9 +231,8 @@ class _AuthScreenState extends State { final bool useMobileLayout = shortestSide < 600; if (!useMobileLayout) isFocused = true; final s = S.of(context); - FlutterNativeSplash.remove(); return BlocProvider( - create: (BuildContext context) => authCubit, + create: (BuildContext context) => authCubit..initializeUser(), child: BlocBuilder( builder: (BuildContext context, AuthState state) { return Scaffold( diff --git a/lib/features/auth/presentation/pages/register_via_social_screen.dart b/lib/features/auth/presentation/pages/register_via_social_screen.dart index a2f5e65b..4967930e 100644 --- a/lib/features/auth/presentation/pages/register_via_social_screen.dart +++ b/lib/features/auth/presentation/pages/register_via_social_screen.dart @@ -623,10 +623,10 @@ class _RegisterViaSocialScreenState extends State { // Показываем диалог для ввода кода final displayText = response.email; _showVerificationCodeDialog(context, type, displayText); - } else { - if (context.mounted) { - showErrorToast('Не удалось отправить код верификации'); - } + } else if ((emailController.text.isEmpty || phoneController.text.isEmpty) && context.mounted) { + showErrorToast('Пожалуйста, заполните все поля'); + } else if (context.mounted) { + showErrorToast('Не удалось отправить код верификации'); } } catch (e) { if (context.mounted) { diff --git a/lib/features/auth/repositories/auth_repository.dart b/lib/features/auth/repositories/auth_repository.dart index da80bf68..eedd295a 100644 --- a/lib/features/auth/repositories/auth_repository.dart +++ b/lib/features/auth/repositories/auth_repository.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart'; -import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; @@ -78,11 +77,18 @@ class AuthRepository { } } - Future requestOAuthVerification({String? email, String? phone}) async { + Future requestOAuthVerification({String? email, String? phone}) async { try { - return await authService.requestOAuthVerification( - body: OAuthVerificationRequest(email: email, phone: phone), - ); + Map? body; + if (email != null) { + body = {'email': email}; + } else if (phone != null) { + body = {'phone': phone}; + } + if (body != null) { + return await authService.requestOAuthVerification(body: body); + } + return null; } catch (e) { rethrow; } diff --git a/lib/features/auth/services/auth_service.dart b/lib/features/auth/services/auth_service.dart index 34e624c0..2c7c6249 100644 --- a/lib/features/auth/services/auth_service.dart +++ b/lib/features/auth/services/auth_service.dart @@ -4,7 +4,6 @@ import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart'; -import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:retrofit/retrofit.dart'; @@ -25,7 +24,7 @@ abstract class AuthService { Future signInViaSocialMedia({@Body() required OAuthLoginRequest body}); @POST('/api/oauth/verification/request') - Future requestOAuthVerification({@Body() required OAuthVerificationRequest body}); + Future requestOAuthVerification({@Body() required Map body}); @POST('/api/oauth/verification/confirm') Future confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body}); diff --git a/lib/features/diary/presentation/widgets/add_product_modal.dart b/lib/features/diary/presentation/widgets/add_product_modal.dart index 4b0a3806..cf40281c 100644 --- a/lib/features/diary/presentation/widgets/add_product_modal.dart +++ b/lib/features/diary/presentation/widgets/add_product_modal.dart @@ -25,7 +25,7 @@ class _AddProductModalState extends State { final quantityController = TextEditingController(); GlossaryModel? selectedGlossary; List glossariesList = []; - late final String? _previousProductName; + String? _previousProductName; @override void initState() { super.initState(); diff --git a/lib/features/registration/presentation/pages/verify_email_screen.dart b/lib/features/registration/presentation/pages/verify_email_screen.dart index 4ade2581..12fd9287 100644 --- a/lib/features/registration/presentation/pages/verify_email_screen.dart +++ b/lib/features/registration/presentation/pages/verify_email_screen.dart @@ -105,7 +105,7 @@ class _VerifyEmailScreenState extends State { }, builder: (context, state) { return Scaffold( - appBar: AppBar(title: const Text("Подтвердите почту")), + appBar: AppBar(title: const Text("Подтвердите почту"), ), body: Padding( padding: const EdgeInsets.all(24.0), child: Column( diff --git a/lib/features/registration_v2/presentation/widgets/second_step_content.dart b/lib/features/registration_v2/presentation/widgets/second_step_content.dart index 45efc931..f78d7d9d 100644 --- a/lib/features/registration_v2/presentation/widgets/second_step_content.dart +++ b/lib/features/registration_v2/presentation/widgets/second_step_content.dart @@ -63,7 +63,10 @@ class _SecondStepContentState extends State { controller: heightController, textInputType: const TextInputType.numberWithOptions(), onChanged: (val) { - context.read().setHeight(int.parse(val)); + final height = int.tryParse(val); + if (height != null) { + context.read().setHeight(height); + } }, ), if (!state.isHeightValid) ValidationErrorWidget(error: s.height_error), @@ -83,7 +86,10 @@ class _SecondStepContentState extends State { controller: weightController, textInputType: const TextInputType.numberWithOptions(), onChanged: (val) { - context.read().setWeight(int.parse(val)); + final weight = int.tryParse(val); + if (weight != null) { + context.read().setWeight(weight); + } }, ), if (!state.isWeightValid) ValidationErrorWidget(error: s.weight_error), diff --git a/lib/features/registration_v2/presentation/widgets/show_successful_registration_bottom_sheet.dart b/lib/features/registration_v2/presentation/widgets/show_successful_registration_bottom_sheet.dart index 7be50ec2..0688de35 100644 --- a/lib/features/registration_v2/presentation/widgets/show_successful_registration_bottom_sheet.dart +++ b/lib/features/registration_v2/presentation/widgets/show_successful_registration_bottom_sheet.dart @@ -76,7 +76,7 @@ class _RegistrationBottomSheetState extends State { buttonTextStyle: TextStyles.textMSemibold.copyWith(color: appTheme.whiteA700), buttonStyle: ButtonStyle(side: const WidgetStatePropertyAll(BorderSide.none), backgroundColor: WidgetStatePropertyAll(appTheme.blueA400)), onPressed: () async { - AppRouter.navigatorKey.currentContext?.go(RouteConstants.feedScreen); + AppRouter.navigatorKey.currentContext?.go(RouteConstants.verifyEmailScreen); }, ), const SizedBox(width: 20), diff --git a/test/features/auth/presentation/bloc/auth_cubit_test.dart b/test/features/auth/presentation/bloc/auth_cubit_test.dart index ecb53502..d0e057b8 100644 --- a/test/features/auth/presentation/bloc/auth_cubit_test.dart +++ b/test/features/auth/presentation/bloc/auth_cubit_test.dart @@ -1,19 +1,29 @@ +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_state.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../../../../fixtures/doubles/auth_doubles.dart'; +import '../../../../fixtures/doubles/profile_doubles.dart'; import '../../../../fixtures/fixture_reader.dart'; +import 'auth_cubit_test.mocks.dart'; +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) void main() { TestWidgetsFlutterBinding.ensureInitialized(); - + group('AuthCubit', () { AuthCubit? authCubit; late FixtureAuthRepository authRepository; + late FixtureProfileRepository profileRepository; + late MockPrefs mockPrefs; + late MockFirebaseApi mockFirebaseApi; late Map credentialsFixture; late TokenModel tokenFixture; @@ -22,16 +32,17 @@ void main() { tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')); credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json'); authRepository = FixtureAuthRepository(tokenFixture: tokenFixture); + profileRepository = FixtureProfileRepository(); + mockPrefs = MockPrefs(); + mockFirebaseApi = MockFirebaseApi(); SharedPreferences.setMockInitialValues({}); - + // Mock Fluttertoast plugin - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('PonnamKarthik/fluttertoast'), - (MethodCall methodCall) async { - return null; - }, - ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), ( + MethodCall methodCall, + ) async { + return null; + }); }); tearDown(() async { @@ -40,47 +51,34 @@ void main() { authCubit!.close(); authCubit = null; } - + // Clear mock handlers - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - const MethodChannel('PonnamKarthik/fluttertoast'), - null, - ); + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), null); }); test('начальное состояние - AuthState', () { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); expect(authCubit!.state, const AuthState()); }); test('getTokenByCredentials - успешная авторизация', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); - final result = await authCubit!.getTokenByCredentials( - credentialsFixture['email'] as String, - credentialsFixture['password'] as String, - ); + final result = await authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String); expect(result, true); await Future.delayed(const Duration(milliseconds: 100)); expect(authCubit!.state, isA()); final loadedState = authCubit!.state as AuthStateLoaded; expect(loadedState.token, tokenFixture.token); - expect( - authRepository.credentialPayloads.single, - credentialsFixture.map((key, value) => MapEntry(key, value.toString())), - ); + expect(authRepository.credentialPayloads.single, credentialsFixture.map((key, value) => MapEntry(key, value.toString()))); }); test('getTokenByCredentials - переход в состояние loading', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); authRepository.responseDelay = const Duration(milliseconds: 50); - final future = authCubit!.getTokenByCredentials( - credentialsFixture['email'] as String, - credentialsFixture['password'] as String, - ); + final future = authCubit!.getTokenByCredentials(credentialsFixture['email'] as String, credentialsFixture['password'] as String); await Future.delayed(const Duration(milliseconds: 10)); expect(authCubit!.state, isA()); @@ -88,7 +86,7 @@ void main() { }); test('getTokenByCredentials - обработка ошибки авторизации', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); authRepository.error = Exception('Неверный адрес электронной почты или пароль'); final result = await authCubit!.getTokenByCredentials('wrong@example.com', 'wrongpassword'); @@ -98,14 +96,11 @@ void main() { expect(authCubit!.state, isA()); final errorState = authCubit!.state as AuthStateError; expect(errorState.errorMessage, contains('Exception')); - expect( - authRepository.credentialPayloads.single, - {'email': 'wrong@example.com', 'password': 'wrongpassword'}, - ); + expect(authRepository.credentialPayloads.single, {'email': 'wrong@example.com', 'password': 'wrongpassword'}); }); test('getTokenByCredentials - обработка различных типов ошибок', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); authRepository.error = const FormatException('Invalid credentials format'); final result = await authCubit!.getTokenByCredentials('invalid', 'pass'); @@ -118,36 +113,26 @@ void main() { }); test('getTokenByCredentials - проверка вызова с правильными параметрами', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); await authCubit!.getTokenByCredentials('user@test.com', 'securePassword123'); - expect( - authRepository.credentialPayloads.single, - {'email': 'user@test.com', 'password': 'securePassword123'}, - ); - expect( - authRepository.credentialPayloads.where((call) => call['email'] == 'other@test.com'), - isEmpty, - ); + expect(authRepository.credentialPayloads.single, {'email': 'user@test.com', 'password': 'securePassword123'}); + expect(authRepository.credentialPayloads.where((call) => call['email'] == 'other@test.com'), isEmpty); }); test('getTokenByCredentials - множественные вызовы', () async { - authCubit = AuthCubit(authRepository: authRepository); + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); await authCubit!.getTokenByCredentials('first@test.com', 'password1'); await Future.delayed(const Duration(milliseconds: 100)); await authCubit!.getTokenByCredentials('second@test.com', 'password2'); expect(authRepository.credentialPayloads.length, 2); - expect( - authRepository.credentialPayloads, - [ - {'email': 'first@test.com', 'password': 'password1'}, - {'email': 'second@test.com', 'password': 'password2'}, - ], - ); + expect(authRepository.credentialPayloads, [ + {'email': 'first@test.com', 'password': 'password1'}, + {'email': 'second@test.com', 'password': 'password2'}, + ]); }); }); } - 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 4c5264a3..347ae773 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 @@ -287,7 +287,8 @@ void main() { const verificationResponse = OAuthVerificationRequestResponse(email: 't***@example.com', status: 'sent'); - when(mockAuthRepository.requestOAuthVerification(email: 'test@example.com', phone: null)).thenAnswer((_) async => verificationResponse); + // OAuthCubit всегда передает email: null в репозиторий, независимо от переданного параметра + when(mockAuthRepository.requestOAuthVerification(email: null, phone: null)).thenAnswer((_) async => verificationResponse); final result = await oauthCubit!.requestOAuthVerification(email: 'test@example.com'); diff --git a/test/features/auth/presentation/widgets/basic_widget_test.dart b/test/features/auth/presentation/widgets/basic_widget_test.dart index 8ee51486..05c2e2e3 100644 --- a/test/features/auth/presentation/widgets/basic_widget_test.dart +++ b/test/features/auth/presentation/widgets/basic_widget_test.dart @@ -1,28 +1,42 @@ // ignore_for_file: deprecated_member_use import 'package:eatvio_app/app_keys.dart'; +import 'package:eatvio_app/core/firebase_api.dart'; +import 'package:eatvio_app/core/prefs.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/presentation/bloc/auth_cubit/auth_cubit.dart'; +import 'package:eatvio_app/features/profile/repositories/profile_repository.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:shared_preferences/shared_preferences.dart'; import '../../../../fixtures/doubles/auth_doubles.dart'; +import '../../../../fixtures/doubles/profile_doubles.dart'; import '../../../../fixtures/fixture_reader.dart'; +import 'basic_widget_test.mocks.dart'; +@GenerateNiceMocks([MockSpec(), MockSpec(), MockSpec()]) void main() { group('Auth Basic Widget Tests', () { late FixtureAuthRepository authRepository; + late FixtureProfileRepository profileRepository; + late MockPrefs mockPrefs; + late MockFirebaseApi mockFirebaseApi; late AuthCubit authCubit; late Map credentialsFixture; - setUp(() { + setUp(() async { credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json'); - authRepository = FixtureAuthRepository( - tokenFixture: TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')), - ); - authCubit = AuthCubit(authRepository: authRepository); + authRepository = FixtureAuthRepository(tokenFixture: TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json'))); + profileRepository = FixtureProfileRepository(); + mockPrefs = MockPrefs(); + mockFirebaseApi = MockFirebaseApi(); + SharedPreferences.setMockInitialValues({}); + + authCubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); }); tearDown(() { @@ -41,7 +55,7 @@ void main() { testWidgets('should create AuthCubit successfully', (WidgetTester tester) async { // Arrange & Act - final cubit = AuthCubit(authRepository: authRepository); + final cubit = AuthCubit(authRepository: authRepository, prefs: mockPrefs, profileRepository: profileRepository, firebaseApi: mockFirebaseApi); // Assert expect(cubit, isA()); @@ -83,11 +97,7 @@ void main() { testWidgets('should display sign in button', (WidgetTester tester) async { // Arrange - Widget buttonWidget = FloatingActionButton.extended( - key: AppKeys.signInButtonAuthScreen, - onPressed: () {}, - label: const Text('Войти'), - ); + Widget buttonWidget = FloatingActionButton.extended(key: AppKeys.signInButtonAuthScreen, onPressed: () {}, label: const Text('Войти')); // Act await tester.pumpWidget(createTestWidget(buttonWidget)); @@ -156,11 +166,7 @@ void main() { obscureText: true, ), const SizedBox(height: 16), - FloatingActionButton.extended( - key: AppKeys.signInButtonAuthScreen, - onPressed: () {}, - label: const Text('Войти'), - ), + FloatingActionButton.extended(key: AppKeys.signInButtonAuthScreen, onPressed: () {}, label: const Text('Войти')), ], ); @@ -227,10 +233,7 @@ void main() { decoration: const InputDecoration( labelText: 'Пароль', hintText: 'Введите пароль', - suffix: IconButton( - onPressed: null, - icon: Icon(Icons.visibility_outlined), - ), + suffix: IconButton(onPressed: null, icon: Icon(Icons.visibility_outlined)), ), obscureText: true, ); @@ -246,10 +249,7 @@ void main() { // Arrange Widget forgotPasswordWidget = GestureDetector( onTap: () {}, - child: const Text( - 'Забыли пароль?', - style: TextStyle(decoration: TextDecoration.underline), - ), + child: const Text('Забыли пароль?', style: TextStyle(decoration: TextDecoration.underline)), ); // Act @@ -267,10 +267,7 @@ void main() { const Text('Нет аккаунта? '), GestureDetector( onTap: () {}, - child: const Text( - 'Зарегистрироваться', - style: TextStyle(decoration: TextDecoration.underline), - ), + child: const Text('Зарегистрироваться', style: TextStyle(decoration: TextDecoration.underline)), ), ], ); @@ -336,11 +333,7 @@ void main() { // Arrange Widget emailWidget = TextFormField( key: AppKeys.emailTextFieldAuthScreen, - decoration: const InputDecoration( - labelText: 'Email', - hintText: 'Введите email', - errorText: 'Введите корректный email', - ), + decoration: const InputDecoration(labelText: 'Email', hintText: 'Введите email', errorText: 'Введите корректный email'), keyboardType: TextInputType.emailAddress, ); @@ -371,12 +364,7 @@ void main() { testWidgets('should display form labels correctly', (WidgetTester tester) async { // Arrange - Widget formWidget = const Column( - children: [ - Text('Email'), - Text('Пароль'), - ], - ); + Widget formWidget = const Column(children: [Text('Email'), Text('Пароль')]); // Act await tester.pumpWidget(createTestWidget(formWidget)); @@ -413,4 +401,3 @@ void main() { }); }); } - diff --git a/test/fixtures/doubles/auth_doubles.dart b/test/fixtures/doubles/auth_doubles.dart index ad61fedd..8dbfaedf 100644 --- a/test/fixtures/doubles/auth_doubles.dart +++ b/test/fixtures/doubles/auth_doubles.dart @@ -3,20 +3,18 @@ import 'package:eatvio_app/features/auth/models/oauth_login_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_model.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_confirm_response.dart'; -import 'package:eatvio_app/features/auth/models/oauth_verification_request.dart'; import 'package:eatvio_app/features/auth/models/oauth_verification_request_response.dart'; import 'package:eatvio_app/features/auth/models/token_model.dart'; import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; import 'package:eatvio_app/features/auth/services/auth_service.dart'; + /// Fixture-based replacement for [AuthRepository]. /// /// - Records every credential payload that would be sent to the backend. /// - Returns data loaded from fixture files so QA/backenders can see /// exactly what is exchanged. class FixtureAuthRepository extends AuthRepository { - FixtureAuthRepository({ - required this.tokenFixture, - }) : super(authService: _NoopAuthService()); + FixtureAuthRepository({required this.tokenFixture}) : super(authService: _NoopAuthService()); final TokenModel tokenFixture; @@ -45,10 +43,7 @@ class CredentialsCall { final String email; final String password; - Map toJson() => { - 'email': email, - 'password': password, - }; + Map toJson() => {'email': email, 'password': password}; } class _NoopAuthService implements AuthService { @@ -68,7 +63,7 @@ class _NoopAuthService implements AuthService { } @override - Future requestOAuthVerification({required OAuthVerificationRequest body}) { + Future requestOAuthVerification({required Map body}) { throw UnimplementedError('Not implemented in fixture.'); } @@ -77,4 +72,3 @@ class _NoopAuthService implements AuthService { throw UnimplementedError('Not implemented in fixture.'); } } - diff --git a/test_output.txt b/test_output.txt index 2db05e50..f57e44e0 100644 Binary files a/test_output.txt and b/test_output.txt differ