forked from eatvio/eatvio_app
feat(registration): added email verification, feat(diary): fixed moderation request, feat(oauth): fixed email and phone verification
This commit is contained in:
@@ -76,7 +76,7 @@ class CubitRegistrations {
|
||||
/// Регистрация Cubit'ов как фабрик
|
||||
static void _registerFactoryCubits() {
|
||||
// Auth related cubits
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl()));
|
||||
sl.registerFactory<AuthCubit>(() => AuthCubit(authRepository: sl(), firebaseApi: sl(), prefs: sl(), profileRepository: sl()));
|
||||
sl.registerFactory<RegistrationCubit>(() => RegistrationCubit(registrationRepository: sl(), prefs: sl()));
|
||||
sl.registerFactory<ResetPasswordCubit>(() => ResetPasswordCubit(resetPasswordRepository: sl()));
|
||||
sl.registerFactory<VerifyEmailCubit>(() => VerifyEmailCubit(profileRepository: sl()));
|
||||
|
||||
@@ -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<Prefs>();
|
||||
final firebaseApi = sl<FirebaseApi>();
|
||||
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),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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<String, dynamic> json) => _$OAuthVerificationRequestFromJson(json);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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<AuthState> {
|
||||
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<bool> getTokenByCredentials(String email, String password) async {
|
||||
emit(const AuthState.loading());
|
||||
@@ -25,4 +36,32 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> 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<void> registerAnonymousPushToken() async {
|
||||
final hasAnonymousTokenBeenSent = prefs.getBool(PrefsKeys.hasAnonymousTokenBeenSent) ?? false;
|
||||
if (!hasAnonymousTokenBeenSent) {
|
||||
await firebaseApi.registerAnonymousPushToken();
|
||||
prefs.setBool(PrefsKeys.hasAnonymousTokenBeenSent, true);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> registerPushToken() async {
|
||||
await firebaseApi.registerPushToken();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,8 +126,10 @@ class OAuthCubit extends Cubit<OAuthState> 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;
|
||||
|
||||
@@ -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<AuthScreen> {
|
||||
final bool useMobileLayout = shortestSide < 600;
|
||||
if (!useMobileLayout) isFocused = true;
|
||||
final s = S.of(context);
|
||||
FlutterNativeSplash.remove();
|
||||
return BlocProvider<AuthCubit>(
|
||||
create: (BuildContext context) => authCubit,
|
||||
create: (BuildContext context) => authCubit..initializeUser(),
|
||||
child: BlocBuilder<AuthCubit, AuthState>(
|
||||
builder: (BuildContext context, AuthState state) {
|
||||
return Scaffold(
|
||||
|
||||
@@ -623,10 +623,10 @@ class _RegisterViaSocialScreenState extends State<RegisterViaSocialScreen> {
|
||||
// Показываем диалог для ввода кода
|
||||
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) {
|
||||
|
||||
@@ -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<OAuthVerificationRequestResponse> requestOAuthVerification({String? email, String? phone}) async {
|
||||
Future<OAuthVerificationRequestResponse?> requestOAuthVerification({String? email, String? phone}) async {
|
||||
try {
|
||||
return await authService.requestOAuthVerification(
|
||||
body: OAuthVerificationRequest(email: email, phone: phone),
|
||||
);
|
||||
Map<String, dynamic>? 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;
|
||||
}
|
||||
|
||||
@@ -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<TokenModel> signInViaSocialMedia({@Body() required OAuthLoginRequest body});
|
||||
|
||||
@POST('/api/oauth/verification/request')
|
||||
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required OAuthVerificationRequest body});
|
||||
Future<OAuthVerificationRequestResponse> requestOAuthVerification({@Body() required Map<String, dynamic> body});
|
||||
|
||||
@POST('/api/oauth/verification/confirm')
|
||||
Future<OAuthVerificationConfirmResponse> confirmOAuthVerification({@Body() required OAuthVerificationConfirmRequest body});
|
||||
|
||||
@@ -25,7 +25,7 @@ class _AddProductModalState extends State<AddProductModal> {
|
||||
final quantityController = TextEditingController();
|
||||
GlossaryModel? selectedGlossary;
|
||||
List<GlossaryModel> glossariesList = [];
|
||||
late final String? _previousProductName;
|
||||
String? _previousProductName;
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
@@ -105,7 +105,7 @@ class _VerifyEmailScreenState extends State<VerifyEmailScreen> {
|
||||
},
|
||||
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(
|
||||
|
||||
@@ -63,7 +63,10 @@ class _SecondStepContentState extends State<SecondStepContent> {
|
||||
controller: heightController,
|
||||
textInputType: const TextInputType.numberWithOptions(),
|
||||
onChanged: (val) {
|
||||
context.read<RegistrationCubitV2>().setHeight(int.parse(val));
|
||||
final height = int.tryParse(val);
|
||||
if (height != null) {
|
||||
context.read<RegistrationCubitV2>().setHeight(height);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!state.isHeightValid) ValidationErrorWidget(error: s.height_error),
|
||||
@@ -83,7 +86,10 @@ class _SecondStepContentState extends State<SecondStepContent> {
|
||||
controller: weightController,
|
||||
textInputType: const TextInputType.numberWithOptions(),
|
||||
onChanged: (val) {
|
||||
context.read<RegistrationCubitV2>().setWeight(int.parse(val));
|
||||
final weight = int.tryParse(val);
|
||||
if (weight != null) {
|
||||
context.read<RegistrationCubitV2>().setWeight(weight);
|
||||
}
|
||||
},
|
||||
),
|
||||
if (!state.isWeightValid) ValidationErrorWidget(error: s.weight_error),
|
||||
|
||||
@@ -76,7 +76,7 @@ class _RegistrationBottomSheetState extends State<RegistrationBottomSheet> {
|
||||
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),
|
||||
|
||||
@@ -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<Prefs>(), MockSpec<FirebaseApi>(), MockSpec<ProfileRepository>()])
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
|
||||
group('AuthCubit', () {
|
||||
AuthCubit? authCubit;
|
||||
late FixtureAuthRepository authRepository;
|
||||
late FixtureProfileRepository profileRepository;
|
||||
late MockPrefs mockPrefs;
|
||||
late MockFirebaseApi mockFirebaseApi;
|
||||
late Map<String, dynamic> 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<AuthStateLoaded>());
|
||||
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<AuthStateLoading>());
|
||||
@@ -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<AuthStateError>());
|
||||
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'},
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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<Prefs>(), MockSpec<FirebaseApi>(), MockSpec<ProfileRepository>()])
|
||||
void main() {
|
||||
group('Auth Basic Widget Tests', () {
|
||||
late FixtureAuthRepository authRepository;
|
||||
late FixtureProfileRepository profileRepository;
|
||||
late MockPrefs mockPrefs;
|
||||
late MockFirebaseApi mockFirebaseApi;
|
||||
late AuthCubit authCubit;
|
||||
late Map<String, dynamic> 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<AuthCubit>());
|
||||
@@ -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() {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
14
test/fixtures/doubles/auth_doubles.dart
vendored
14
test/fixtures/doubles/auth_doubles.dart
vendored
@@ -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<String, String> toJson() => <String, String>{
|
||||
'email': email,
|
||||
'password': password,
|
||||
};
|
||||
Map<String, String> toJson() => <String, String>{'email': email, 'password': password};
|
||||
}
|
||||
|
||||
class _NoopAuthService implements AuthService {
|
||||
@@ -68,7 +63,7 @@ class _NoopAuthService implements AuthService {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<OAuthVerificationRequestResponse> requestOAuthVerification({required OAuthVerificationRequest body}) {
|
||||
Future<OAuthVerificationRequestResponse> requestOAuthVerification({required Map<String, dynamic> body}) {
|
||||
throw UnimplementedError('Not implemented in fixture.');
|
||||
}
|
||||
|
||||
@@ -77,4 +72,3 @@ class _NoopAuthService implements AuthService {
|
||||
throw UnimplementedError('Not implemented in fixture.');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
BIN
test_output.txt
BIN
test_output.txt
Binary file not shown.
Reference in New Issue
Block a user