feat(registration): added email verification, feat(diary): fixed moderation request, feat(oauth): fixed email and phone verification

This commit is contained in:
misha
2025-12-25 16:31:52 +03:00
parent 99659f7ea1
commit 0ead14bca5
18 changed files with 148 additions and 165 deletions

View File

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

View File

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

View File

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

View File

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