diff --git a/.gitignore b/.gitignore index afeb23f8..530ea713 100644 --- a/.gitignore +++ b/.gitignore @@ -19,4 +19,5 @@ ios/XCBuildData/ env/.env .env android/key.properties -app/upload-keystore.jks \ No newline at end of file +app/upload-keystore.jks +coverage/ \ No newline at end of file diff --git a/test/core/comments/comments_cubit/comments_cubit_test.dart b/test/core/comments/comments_cubit/comments_cubit_test.dart new file mode 100644 index 00000000..b9e9bf72 --- /dev/null +++ b/test/core/comments/comments_cubit/comments_cubit_test.dart @@ -0,0 +1,419 @@ +import 'package:eatvio_app/core/comments/comments_cubit/comments_cubit.dart'; +import 'package:eatvio_app/core/comments/comments_cubit/comments_state.dart'; +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/comment_model.dart'; +import 'package:eatvio_app/features/feed/presentation/widgets/comments_widget.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'comments_cubit_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + group('CommentsCubit', () { + CommentsCubit? commentsCubit; + late MockFeedRepository mockFeedRepository; + late MockCommentsRepository mockCommentsRepository; + late CommentModel testCommentModel; + + setUp(() { + mockFeedRepository = MockFeedRepository(); + mockCommentsRepository = MockCommentsRepository(); + + testCommentModel = CommentModel( + data: [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + }); + + tearDown(() async { + if (commentsCubit != null && !commentsCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + commentsCubit!.close(); + commentsCubit = null; + } + }); + + test('начальное состояние - CommentsState', () { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + expect(commentsCubit!.state, const CommentsState()); + }); + + test('getComments - успешная загрузка комментариев для blogPost', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const postId = '1'; + + when(mockFeedRepository.getBlogPostComments(id: postId)) + .thenAnswer((_) async => testCommentModel); + + await commentsCubit!.getComments(postId, CommentSourceType.blogPost, true); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.comment, testCommentModel); + verify(mockFeedRepository.getBlogPostComments(id: postId)).called(1); + }); + + test('getComments - успешная загрузка комментариев для diet', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const dietId = '1'; + + when(mockFeedRepository.getDietsComments(id: dietId)) + .thenAnswer((_) async => testCommentModel); + + await commentsCubit!.getComments(dietId, CommentSourceType.diet, true); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + verify(mockFeedRepository.getDietsComments(id: dietId)).called(1); + }); + + test('getComments - успешная загрузка комментариев для recipe', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const recipeId = 'recipe-slug'; + + when(mockFeedRepository.getRecipesComments(id: recipeId)) + .thenAnswer((_) async => testCommentModel); + + await commentsCubit!.getComments(recipeId, CommentSourceType.recipe, true); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + verify(mockFeedRepository.getRecipesComments(id: recipeId)).called(1); + }); + + test('getComments - успешная загрузка комментариев для plan', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const planSlug = 'plan-slug'; + + when(mockFeedRepository.getPlansComments(slug: planSlug)) + .thenAnswer((_) async => testCommentModel); + + await commentsCubit!.getComments(planSlug, CommentSourceType.plan, true); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + verify(mockFeedRepository.getPlansComments(slug: planSlug)).called(1); + }); + + test('getComments - переход в состояние loading при первом запросе', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const postId = '1'; + + when(mockFeedRepository.getBlogPostComments(id: postId)) + .thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return testCommentModel; + }); + + final future = commentsCubit!.getComments(postId, CommentSourceType.blogPost, true); + await Future.delayed(const Duration(milliseconds: 10)); + + expect(commentsCubit!.state, const CommentsState.loading()); + await future; + }); + + test('getComments - обработка ошибки', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const postId = '1'; + + when(mockFeedRepository.getBlogPostComments(id: postId)) + .thenThrow(Exception('Ошибка загрузки')); + + await commentsCubit!.getComments(postId, CommentSourceType.blogPost, true); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + final errorState = commentsCubit!.state as CommentsStateError; + expect(errorState.errorMessage, contains('Exception')); + }); + + test('sendComment - успешная отправка комментария для blogPost', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const postId = '1'; + const commentText = 'Тестовый комментарий'; + + // Устанавливаем начальное состояние + commentsCubit!.emit(CommentsState.loaded(comment: testCommentModel)); + + when(mockFeedRepository.sendBlogPostComment( + id: postId, + commentText: commentText, + parentCommentId: null, + )).thenAnswer((_) async => Future.value()); + + when(mockFeedRepository.getBlogPostComments(id: postId)) + .thenAnswer((_) async => testCommentModel); + + await commentsCubit!.sendComment( + id: postId, + commentText: commentText, + commentSourceType: CommentSourceType.blogPost, + ); + + verify(mockFeedRepository.sendBlogPostComment( + id: postId, + commentText: commentText, + parentCommentId: null, + )).called(1); + verify(mockFeedRepository.getBlogPostComments(id: postId)).called(1); + }); + + test('sendComment - обработка ошибки при отправке', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const postId = '1'; + const commentText = 'Тестовый комментарий'; + + commentsCubit!.emit(CommentsState.loaded(comment: testCommentModel)); + + when(mockFeedRepository.sendBlogPostComment( + id: postId, + commentText: commentText, + parentCommentId: null, + )).thenThrow(Exception('Ошибка отправки')); + + await commentsCubit!.sendComment( + id: postId, + commentText: commentText, + commentSourceType: CommentSourceType.blogPost, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(commentsCubit!.state, isA()); + }); + + test('deleteComment - успешное удаление комментария', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const commentId = 1; + + final commentWithData = testCommentModel.copyWith( + data: [ + Comment( + id: commentId, + body: 'Комментарий для удаления', + authorName: 'Автор', + authorSlug: 'author-slug', + authorProfileIcon: 'https://example.com/photo.jpg', + createdAt: DateTime.now(), + childrenComments: [], + ), + ], + ); + + commentsCubit!.emit(CommentsState.loaded(comment: commentWithData)); + + when(mockCommentsRepository.deleteComment(id: commentId)) + .thenAnswer((_) async => Future.value()); + + final result = await commentsCubit!.deleteComment(id: commentId); + + expect(result, true); + expect(commentsCubit!.state, isA()); + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.comment.data, isEmpty); + verify(mockCommentsRepository.deleteComment(id: commentId)).called(1); + }); + + test('deleteComment - возвращает false если состояние не loaded', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const commentId = 1; + + commentsCubit!.emit(const CommentsState()); + + final result = await commentsCubit!.deleteComment(id: commentId); + + expect(result, false); + verifyNever(mockCommentsRepository.deleteComment(id: commentId)); + }); + + test('deleteComment - обработка ошибки при удалении', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const commentId = 1; + + final commentWithData = testCommentModel.copyWith( + data: [ + Comment( + id: commentId, + body: 'Комментарий', + authorName: 'Автор', + authorSlug: 'author-slug', + authorProfileIcon: 'https://example.com/photo.jpg', + createdAt: DateTime.now(), + childrenComments: [], + ), + ], + ); + + commentsCubit!.emit(CommentsState.loaded(comment: commentWithData)); + + when(mockCommentsRepository.deleteComment(id: commentId)) + .thenThrow(Exception('Ошибка удаления')); + + final result = await commentsCubit!.deleteComment(id: commentId); + + expect(result, false); + // Состояние должно вернуться к исходному + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.comment.data.length, 1); + }); + + test('updateComment - успешное обновление комментария', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const commentId = 1; + const newBody = 'Обновлённый комментарий'; + + final commentWithData = testCommentModel.copyWith( + data: [ + Comment( + id: commentId, + body: 'Старый комментарий', + authorName: 'Автор', + authorSlug: 'author-slug', + authorProfileIcon: 'https://example.com/photo.jpg', + createdAt: DateTime.now(), + childrenComments: [], + ), + ], + ); + + commentsCubit!.emit(CommentsState.loaded(comment: commentWithData)); + + when(mockCommentsRepository.updateComment(id: commentId, body: newBody)) + .thenAnswer((_) async => Future.value()); + + final result = await commentsCubit!.updateComment(id: commentId, body: newBody); + + expect(result, true); + expect(commentsCubit!.state, isA()); + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.comment.data.first.body, newBody); + verify(mockCommentsRepository.updateComment(id: commentId, body: newBody)).called(1); + }); + + test('updateComment - обработка ошибки при обновлении', () async { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const commentId = 1; + const newBody = 'Обновлённый комментарий'; + const oldBody = 'Старый комментарий'; + + final commentWithData = testCommentModel.copyWith( + data: [ + Comment( + id: commentId, + body: oldBody, + authorName: 'Автор', + authorSlug: 'author-slug', + authorProfileIcon: 'https://example.com/photo.jpg', + createdAt: DateTime.now(), + childrenComments: [], + ), + ], + ); + + commentsCubit!.emit(CommentsState.loaded(comment: commentWithData)); + + when(mockCommentsRepository.updateComment(id: commentId, body: newBody)) + .thenThrow(Exception('Ошибка обновления')); + + final result = await commentsCubit!.updateComment(id: commentId, body: newBody); + + expect(result, false); + // Состояние должно вернуться к исходному + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.comment.data.first.body, oldBody); + }); + + test('setReplyId - устанавливает ID ответа', () { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + const replyId = 5; + + commentsCubit!.emit(CommentsState.loaded(comment: testCommentModel)); + + commentsCubit!.setReplyId(replyId); + + expect(commentsCubit!.state, isA()); + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.replyId, replyId); + }); + + test('setReplyId - сбрасывает ID ответа', () { + commentsCubit = CommentsCubit( + feedRepository: mockFeedRepository, + commentsRepository: mockCommentsRepository, + ); + + commentsCubit!.emit(CommentsState.loaded(comment: testCommentModel, replyId: 5)); + + commentsCubit!.setReplyId(null); + + expect(commentsCubit!.state, isA()); + final loadedState = commentsCubit!.state as CommentsStateLoaded; + expect(loadedState.replyId, isNull); + }); + }); +} + diff --git a/test/core/comments/repositories/comments_repository_test.dart b/test/core/comments/repositories/comments_repository_test.dart new file mode 100644 index 00000000..6b984404 --- /dev/null +++ b/test/core/comments/repositories/comments_repository_test.dart @@ -0,0 +1,80 @@ +import 'package:eatvio_app/core/comments/models/comment_update_model.dart'; +import 'package:eatvio_app/core/comments/repositories/comments_repository.dart'; +import 'package:eatvio_app/core/comments/services/comments_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'comments_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('CommentsRepository', () { + late CommentsRepository commentsRepository; + late MockCommentsService mockCommentsService; + + setUp(() { + mockCommentsService = MockCommentsService(); + commentsRepository = CommentsRepository(service: mockCommentsService); + }); + + test('deleteComment - успешное удаление комментария', () async { + const commentId = 1; + + when(mockCommentsService.deleteComment(commentId)) + .thenAnswer((_) async => Future.value()); + + await commentsRepository.deleteComment(id: commentId); + + verify(mockCommentsService.deleteComment(commentId)).called(1); + }); + + test('deleteComment - пробрасывает исключение', () async { + const commentId = 1; + + when(mockCommentsService.deleteComment(commentId)) + .thenThrow(Exception('Ошибка удаления')); + + expect( + () => commentsRepository.deleteComment(id: commentId), + throwsException, + ); + }); + + test('updateComment - успешное обновление комментария', () async { + const commentId = 1; + const newBody = 'Обновлённый текст комментария'; + final updateModel = CommentUpdateModel(body: newBody); + + when(mockCommentsService.updateComment(commentId, updateModel)) + .thenAnswer((_) async => Future.value()); + + await commentsRepository.updateComment(id: commentId, body: newBody); + + verify(mockCommentsService.updateComment( + commentId, + argThat( + predicate( + (m) => m.body == newBody, + ), + ), + )).called(1); + }); + + test('updateComment - пробрасывает исключение', () async { + const commentId = 1; + const newBody = 'Обновлённый текст'; + + when(mockCommentsService.updateComment( + commentId, + any, + )).thenThrow(Exception('Ошибка обновления')); + + expect( + () => commentsRepository.updateComment(id: commentId, body: newBody), + throwsException, + ); + }); + }); +} + 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 new file mode 100644 index 00000000..dd4368e3 --- /dev/null +++ b/test/features/auth/presentation/bloc/oauth_cubit/oauth_cubit_test.dart @@ -0,0 +1,190 @@ +import 'package:eatvio_app/core/prefs.dart'; +import 'package:eatvio_app/core/token_storage.dart'; +import 'package:eatvio_app/features/auth/models/token_model.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/oauth_cubit/oauth_state.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:flutter/services.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 '../../../../../fixtures/doubles/auth_doubles.dart'; +import '../../../../../fixtures/fixture_reader.dart'; +import 'oauth_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec(), MockSpec()]) +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group('OAuthCubit', () { + OAuthCubit? oauthCubit; + late MockAuthRepository mockAuthRepository; + late MockPrefs mockPrefs; + late TokenModel tokenFixture; + late Map signInRequestFixture; + late Map signUpRequestFixture; + + setUp(() async { + tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')); + signInRequestFixture = FixtureReader.readAsMap('auth/oauth_signin_request.json'); + signUpRequestFixture = FixtureReader.readAsMap('auth/oauth_signup_request.json'); + + mockAuthRepository = MockAuthRepository(); + mockPrefs = MockPrefs(); + + SharedPreferences.setMockInitialValues({}); + + // Mock Fluttertoast plugin + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), ( + MethodCall methodCall, + ) async { + return null; + }); + }); + + tearDown(() async { + if (oauthCubit != null && !oauthCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + oauthCubit!.close(); + oauthCubit = null; + } + + // Clear mock handlers + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(const MethodChannel('PonnamKarthik/fluttertoast'), null); + }); + + test('начальное состояние - OAuthState', () { + oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs); + expect(oauthCubit!.state, const OAuthState()); + }); + + test('signInViaSocialMedia - успешный вход через соцсеть', () async { + oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs); + + when( + mockAuthRepository.signInViaSocialMedia( + provider: signInRequestFixture['provider'] as String, + accessToken: signInRequestFixture['accessToken'] as String, + refreshToken: signInRequestFixture['refreshToken'] as String, + clientId: signInRequestFixture['clientId'] as String, + ), + ).thenAnswer((_) async => tokenFixture); + + await oauthCubit!.signInViaSocialMedia( + provider: signInRequestFixture['provider'] as String, + accessToken: signInRequestFixture['accessToken'] as String, + refreshToken: signInRequestFixture['refreshToken'] as String, + clientId: signInRequestFixture['clientId'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(oauthCubit!.state, isA()); + final loadedState = oauthCubit!.state as OAuthStateLoaded; + expect(loadedState.token, tokenFixture.token); + + // Проверяем, что токен сохранён + final savedToken = await TokenStorage.getTokenFromStorage(); + expect(savedToken, tokenFixture.token); + }); + + test('signInViaSocialMedia - переход в состояние loading', () async { + oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs); + + when( + mockAuthRepository.signInViaSocialMedia( + provider: anyNamed('provider'), + accessToken: anyNamed('accessToken'), + refreshToken: anyNamed('refreshToken'), + clientId: anyNamed('clientId'), + ), + ).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return tokenFixture; + }); + + final future = oauthCubit!.signInViaSocialMedia( + provider: signInRequestFixture['provider'] as String, + accessToken: signInRequestFixture['accessToken'] as String, + refreshToken: signInRequestFixture['refreshToken'] as String, + clientId: signInRequestFixture['clientId'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 10)); + expect(oauthCubit!.state, isA()); + await future; + }); + + test('signInViaSocialMedia - обработка ошибки', () async { + oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs); + + when( + mockAuthRepository.signInViaSocialMedia( + provider: anyNamed('provider'), + accessToken: anyNamed('accessToken'), + refreshToken: anyNamed('refreshToken'), + clientId: anyNamed('clientId'), + ), + ).thenThrow(Exception('Ошибка авторизации')); + + await oauthCubit!.signInViaSocialMedia( + provider: signInRequestFixture['provider'] as String, + accessToken: signInRequestFixture['accessToken'] as String, + refreshToken: signInRequestFixture['refreshToken'] as String, + clientId: signInRequestFixture['clientId'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(oauthCubit!.state, isA()); + final errorState = oauthCubit!.state as OAuthStateError; + expect(errorState.errorMessage, contains('Exception')); + }); + + test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async { + oauthCubit = OAuthCubit(authRepository: mockAuthRepository, prefs: mockPrefs); + + when( + mockAuthRepository.signUpViaSocialMedia( + provider: anyNamed('provider'), + accessToken: anyNamed('accessToken'), + id: anyNamed('id'), + refreshToken: anyNamed('refreshToken'), + firstName: anyNamed('firstName'), + lastName: anyNamed('lastName'), + email: anyNamed('email'), + phone: anyNamed('phone'), + dateOfBirth: anyNamed('dateOfBirth'), + gender: anyNamed('gender'), + weight: anyNamed('weight'), + height: anyNamed('height'), + clientId: anyNamed('clientId'), + ), + ).thenAnswer((_) async => tokenFixture.token); + + when(mockPrefs.setString(any, any)).thenAnswer((_) async => Future.value()); + + final result = await oauthCubit!.signUpViaSocialMedia( + provider: signUpRequestFixture['provider'] as String, + accessToken: signUpRequestFixture['accessToken'] as String, + id: signUpRequestFixture['id'] as String, + refreshToken: signUpRequestFixture['refreshToken'] as String, + firstName: signUpRequestFixture['firstName'] as String, + lastName: signUpRequestFixture['lastName'] as String, + email: signUpRequestFixture['email'] as String, + phone: signUpRequestFixture['phone'] as String, + dateOfBirth: signUpRequestFixture['dateOfBirth'] as String, + gender: signUpRequestFixture['gender'] as String, + weight: signUpRequestFixture['weight'] as int, + height: signUpRequestFixture['height'] as int, + clientId: signUpRequestFixture['clientId'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(result, isNull); // Нет ошибок валидации + expect(oauthCubit!.state, isA()); + final loadedState = oauthCubit!.state as OAuthStateLoaded; + expect(loadedState.token, tokenFixture.token); + }); + }); +} diff --git a/test/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit_test.dart b/test/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit_test.dart new file mode 100644 index 00000000..6359d579 --- /dev/null +++ b/test/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit_test.dart @@ -0,0 +1,216 @@ +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_cubit.dart'; +import 'package:eatvio_app/features/auth/presentation/bloc/reset_password_cubit/reset_password_state.dart'; +import 'package:eatvio_app/features/auth/repositories/reset_password_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../../fixtures/fixture_reader.dart'; +import 'reset_password_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('ResetPasswordCubit', () { + ResetPasswordCubit? resetPasswordCubit; + late MockResetPasswordRepository mockResetPasswordRepository; + late Map resetPasswordFixture; + + setUp(() { + resetPasswordFixture = FixtureReader.readAsMap('auth/reset_password_request.json'); + mockResetPasswordRepository = MockResetPasswordRepository(); + }); + + tearDown(() async { + if (resetPasswordCubit != null && !resetPasswordCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + resetPasswordCubit!.close(); + resetPasswordCubit = null; + } + }); + + test('начальное состояние - ResetPasswordState.empty', () { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + expect(resetPasswordCubit!.state, const ResetPasswordState.empty()); + }); + + test('trySendEmailCode - успешная отправка кода', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.sendEmailCodeForPasswordChange( + resetPasswordFixture['email'] as String, + )).thenAnswer((_) async => true); + + await resetPasswordCubit!.trySendEmailCode( + resetPasswordFixture['email'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.codeSendSuccess()); + verify(mockResetPasswordRepository.sendEmailCodeForPasswordChange( + resetPasswordFixture['email'] as String, + )).called(1); + }); + + test('trySendEmailCode - переход в состояние loading', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.sendEmailCodeForPasswordChange( + any, + )).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return true; + }); + + final future = resetPasswordCubit!.trySendEmailCode( + resetPasswordFixture['email'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 10)); + expect(resetPasswordCubit!.state, const ResetPasswordState.loadingSendEmailCode()); + await future; + }); + + test('trySendEmailCode - ошибка отправки кода', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.sendEmailCodeForPasswordChange( + resetPasswordFixture['email'] as String, + )).thenAnswer((_) async => false); + + await resetPasswordCubit!.trySendEmailCode( + resetPasswordFixture['email'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.codeSendError()); + }); + + test('trySendEmailCode - обработка исключения', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.sendEmailCodeForPasswordChange( + resetPasswordFixture['email'] as String, + )).thenThrow(Exception('Ошибка сети')); + + await resetPasswordCubit!.trySendEmailCode( + resetPasswordFixture['email'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.codeSendError()); + }); + + test('tryResetPassword - успешный сброс пароля', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.confirmPasswordChange( + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + resetPasswordFixture['email'] as String, + )).thenAnswer((_) async => true); + + await resetPasswordCubit!.tryResetPassword( + resetPasswordFixture['email'] as String, + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.resetPasswordSuccess()); + verify(mockResetPasswordRepository.confirmPasswordChange( + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + resetPasswordFixture['email'] as String, + )).called(1); + }); + + test('tryResetPassword - переход в состояние loading', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.confirmPasswordChange( + any, + any, + any, + any, + )).thenAnswer((_) async { + await Future.delayed(const Duration(milliseconds: 50)); + return true; + }); + + final future = resetPasswordCubit!.tryResetPassword( + resetPasswordFixture['email'] as String, + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 10)); + expect(resetPasswordCubit!.state, const ResetPasswordState.loadingResetPassword()); + await future; + }); + + test('tryResetPassword - ошибка сброса пароля', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.confirmPasswordChange( + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + resetPasswordFixture['email'] as String, + )).thenAnswer((_) async => false); + + await resetPasswordCubit!.tryResetPassword( + resetPasswordFixture['email'] as String, + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.resetPasswordError()); + }); + + test('tryResetPassword - обработка исключения', () async { + resetPasswordCubit = ResetPasswordCubit( + resetPasswordRepository: mockResetPasswordRepository, + ); + + when(mockResetPasswordRepository.confirmPasswordChange( + any, + any, + any, + any, + )).thenThrow(Exception('Неверный код')); + + await resetPasswordCubit!.tryResetPassword( + resetPasswordFixture['email'] as String, + resetPasswordFixture['code'] as String, + resetPasswordFixture['password'] as String, + resetPasswordFixture['passwordConfirmation'] as String, + ); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(resetPasswordCubit!.state, const ResetPasswordState.resetPasswordError()); + }); + }); +} + diff --git a/test/features/auth/repositories/auth_repository_test.dart b/test/features/auth/repositories/auth_repository_test.dart new file mode 100644 index 00000000..509e44a7 --- /dev/null +++ b/test/features/auth/repositories/auth_repository_test.dart @@ -0,0 +1,237 @@ +import 'package:eatvio_app/features/auth/models/credentials_model.dart'; +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/token_model.dart'; +import 'package:eatvio_app/features/auth/repositories/auth_repository.dart'; +import 'package:eatvio_app/features/auth/services/auth_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../fixtures/fixture_reader.dart'; +import 'auth_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('AuthRepository', () { + late AuthRepository authRepository; + late MockAuthService mockAuthService; + late TokenModel tokenFixture; + late Map credentialsFixture; + late Map oauthSignInFixture; + late Map oauthSignUpFixture; + + setUp(() { + tokenFixture = TokenModel.fromJson(FixtureReader.readAsMap('auth/token_response.json')); + credentialsFixture = FixtureReader.readAsMap('auth/login_credentials.json'); + oauthSignInFixture = FixtureReader.readAsMap('auth/oauth_signin_request.json'); + oauthSignUpFixture = FixtureReader.readAsMap('auth/oauth_signup_request.json'); + + mockAuthService = MockAuthService(); + authRepository = AuthRepository(authService: mockAuthService); + }); + + test('getTokenByCredentials - успешное получение токена', () async { + final credentials = CredentialsModel( + email: credentialsFixture['email'] as String, + password: credentialsFixture['password'] as String, + ); + + when(mockAuthService.getTokenByCredentials(credentials: credentials)) + .thenAnswer((_) async => tokenFixture); + + final result = await authRepository.getTokenByCredentials( + credentialsFixture['email'] as String, + credentialsFixture['password'] as String, + ); + + expect(result, isA()); + expect(result.token, tokenFixture.token); + verify(mockAuthService.getTokenByCredentials(credentials: credentials)).called(1); + }); + + test('getTokenByCredentials - пробрасывает исключение', () async { + final credentials = CredentialsModel( + email: credentialsFixture['email'] as String, + password: credentialsFixture['password'] as String, + ); + + when(mockAuthService.getTokenByCredentials(credentials: credentials)) + .thenThrow(Exception('Ошибка авторизации')); + + expect( + () => authRepository.getTokenByCredentials( + credentialsFixture['email'] as String, + credentialsFixture['password'] as String, + ), + throwsException, + ); + }); + + test('signInViaSocialMedia - успешный вход через соцсеть', () async { + final oauthRequest = OAuthLoginRequest( + provider: oauthSignInFixture['provider'] as String, + accessToken: oauthSignInFixture['accessToken'] as String, + refreshToken: oauthSignInFixture['refreshToken'] as String, + clientId: oauthSignInFixture['clientId'] as String, + ); + + when(mockAuthService.signInViaSocialMedia(body: oauthRequest)) + .thenAnswer((_) async => tokenFixture); + + final result = await authRepository.signInViaSocialMedia( + provider: oauthSignInFixture['provider'] as String, + accessToken: oauthSignInFixture['accessToken'] as String, + refreshToken: oauthSignInFixture['refreshToken'] as String, + clientId: oauthSignInFixture['clientId'] as String, + ); + + expect(result, isA()); + expect(result.token, tokenFixture.token); + verify(mockAuthService.signInViaSocialMedia(body: oauthRequest)).called(1); + }); + + test('signInViaSocialMedia - пробрасывает исключение', () async { + final oauthRequest = OAuthLoginRequest( + provider: oauthSignInFixture['provider'] as String, + accessToken: oauthSignInFixture['accessToken'] as String, + refreshToken: oauthSignInFixture['refreshToken'] as String, + clientId: oauthSignInFixture['clientId'] as String, + ); + + when(mockAuthService.signInViaSocialMedia(body: oauthRequest)) + .thenThrow(Exception('Ошибка OAuth')); + + expect( + () => authRepository.signInViaSocialMedia( + provider: oauthSignInFixture['provider'] as String, + accessToken: oauthSignInFixture['accessToken'] as String, + refreshToken: oauthSignInFixture['refreshToken'] as String, + clientId: oauthSignInFixture['clientId'] as String, + ), + throwsException, + ); + }); + + test('signUpViaSocialMedia - успешная регистрация через соцсеть', () async { + final oauthModel = OAuthModel( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: 'male', // Преобразуется из 'М' + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ); + + when(mockAuthService.signUpViaSocialMedia(body: oauthModel)) + .thenAnswer((_) async => {'token': tokenFixture.token}); + + final result = await authRepository.signUpViaSocialMedia( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: oauthSignUpFixture['gender'] as String, + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ); + + expect(result, tokenFixture.token); + verify(mockAuthService.signUpViaSocialMedia(body: oauthModel)).called(1); + }); + + test('signUpViaSocialMedia - преобразует женский пол', () async { + final oauthModel = OAuthModel( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: 'female', // Преобразуется из 'Ж' + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ); + + when(mockAuthService.signUpViaSocialMedia(body: oauthModel)) + .thenAnswer((_) async => {'token': tokenFixture.token}); + + final result = await authRepository.signUpViaSocialMedia( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: 'Ж', + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ); + + expect(result, tokenFixture.token); + verify(mockAuthService.signUpViaSocialMedia(body: oauthModel)).called(1); + }); + + test('signUpViaSocialMedia - пробрасывает исключение', () async { + final oauthModel = OAuthModel( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: 'male', // Преобразуется из 'М' + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ); + + when(mockAuthService.signUpViaSocialMedia(body: oauthModel)) + .thenThrow(Exception('Ошибка регистрации')); + + expect( + () => authRepository.signUpViaSocialMedia( + provider: oauthSignUpFixture['provider'] as String, + accessToken: oauthSignUpFixture['accessToken'] as String, + id: oauthSignUpFixture['id'] as String, + refreshToken: oauthSignUpFixture['refreshToken'] as String, + firstName: oauthSignUpFixture['firstName'] as String, + lastName: oauthSignUpFixture['lastName'] as String, + email: oauthSignUpFixture['email'] as String, + phone: oauthSignUpFixture['phone'] as String, + dateOfBirth: oauthSignUpFixture['dateOfBirth'] as String, + gender: oauthSignUpFixture['gender'] as String, + weight: oauthSignUpFixture['weight'] as int, + height: oauthSignUpFixture['height'] as int, + clientId: oauthSignUpFixture['clientId'] as String, + ), + throwsException, + ); + }); + }); +} + diff --git a/test/features/feed/presentation/bloc/feed_cubit/feed_cubit_test.dart b/test/features/feed/presentation/bloc/feed_cubit/feed_cubit_test.dart new file mode 100644 index 00000000..15cfaa79 --- /dev/null +++ b/test/features/feed/presentation/bloc/feed_cubit/feed_cubit_test.dart @@ -0,0 +1,223 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_cubit.dart'; +import 'package:eatvio_app/features/feed/presentation/bloc/feed_cubit/feed_state.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'feed_cubit_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FeedCubit', () { + FeedCubit? feedCubit; + late MockFeedRepository mockFeedRepository; + late FeedBlogPostSettingsModel feedBlogPostsFixture; + + setUp(() { + mockFeedRepository = MockFeedRepository(); + + // Создаём тестовые данные + final testPosts = [ + FeedBlogPostModel( + id: 1, + state: 'published', + image: 'https://example.com/image1.jpg', + description: 'Описание поста 1', + author: const Author( + authorSlug: 'author-1', + authorName: 'Автор 1', + authorProfileIcon: 'https://example.com/photo1.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + videoUrl: '', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + title: 'Тестовый пост 1', + totalLikes: 10, + totalViews: 100, + totalComments: 5, + wasLikedByMe: false, + ), + FeedBlogPostModel( + id: 2, + state: 'published', + image: 'https://example.com/image2.jpg', + description: 'Описание поста 2', + author: const Author( + authorSlug: 'author-2', + authorName: 'Автор 2', + authorProfileIcon: 'https://example.com/photo2.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + videoUrl: '', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + title: 'Тестовый пост 2', + totalLikes: 20, + totalViews: 200, + totalComments: 10, + wasLikedByMe: true, + ), + ]; + + feedBlogPostsFixture = FeedBlogPostSettingsModel( + feedBlogPostList: testPosts, + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 3, + links: [], + path: null, + perPage: 10, + to: null, + total: 25, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + }); + + tearDown(() async { + if (feedCubit != null && !feedCubit!.isClosed) { + await Future.delayed(const Duration(milliseconds: 100)); + feedCubit!.close(); + feedCubit = null; + } + }); + + test('начальное состояние - FeedStateLoading', () { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + expect(feedCubit!.state, const FeedState.loading()); + }); + + test('loadFeedBlogPostList - успешная загрузка постов', () async { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + when(mockFeedRepository.getFeedBlogPosts(query: null)) + .thenAnswer((_) async => feedBlogPostsFixture); + + await feedCubit!.loadFeedBlogPostList(); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(feedCubit!.state, isA()); + final loadedState = feedCubit!.state as FeedStateLoaded; + expect(loadedState.feedBlogPostList.length, 2); + expect(loadedState.isLoading, false); + expect(feedCubit!.lastPage, 3); + }); + + test('loadFeedBlogPostList - загрузка с запросом', () async { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + const query = 'тест'; + + when(mockFeedRepository.getFeedBlogPosts(query: query)) + .thenAnswer((_) async => feedBlogPostsFixture); + + await feedCubit!.loadFeedBlogPostList(query: query); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(feedCubit!.state, isA()); + verify(mockFeedRepository.getFeedBlogPosts(query: query)).called(1); + }); + + test('loadFeedBlogPostList - обработка ошибки', () async { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + when(mockFeedRepository.getFeedBlogPosts(query: anyNamed('query'))) + .thenThrow(Exception('Ошибка загрузки')); + + await feedCubit!.loadFeedBlogPostList(); + + await Future.delayed(const Duration(milliseconds: 100)); + expect(feedCubit!.state, isA()); + final errorState = feedCubit!.state as FeedStateError; + expect(errorState.errorMessage, contains('Exception')); + }); + + test('updateFeedBlogPostList - обновление лайка поста', () { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + // Сначала загружаем посты + when(mockFeedRepository.getFeedBlogPosts(query: anyNamed('query'))) + .thenAnswer((_) async => feedBlogPostsFixture); + + // Устанавливаем состояние loaded вручную для теста + feedCubit!.emit(FeedState.loaded( + feedBlogPostList: feedBlogPostsFixture.feedBlogPostList, + isLoading: false, + )); + feedCubit!.lastPage = 3; + + final postToUpdate = feedBlogPostsFixture.feedBlogPostList.first; + feedCubit!.updateFeedBlogPostList(postToUpdate, true); + + expect(feedCubit!.state, isA()); + final loadedState = feedCubit!.state as FeedStateLoaded; + final updatedPost = loadedState.feedBlogPostList.firstWhere((p) => p.id == postToUpdate.id); + expect(updatedPost.wasLikedByMe, true); + expect(updatedPost.totalLikes, postToUpdate.totalLikes + 1); + }); + + test('updateFeedBlogPostList - снятие лайка', () { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + feedCubit!.emit(FeedState.loaded( + feedBlogPostList: feedBlogPostsFixture.feedBlogPostList, + isLoading: false, + )); + feedCubit!.lastPage = 3; + + final postToUpdate = feedBlogPostsFixture.feedBlogPostList[1]; // У этого wasLikedByMe = true + final initialLikes = postToUpdate.totalLikes; + + feedCubit!.updateFeedBlogPostList(postToUpdate, false); + + expect(feedCubit!.state, isA()); + final loadedState = feedCubit!.state as FeedStateLoaded; + final updatedPost = loadedState.feedBlogPostList.firstWhere((p) => p.id == postToUpdate.id); + expect(updatedPost.wasLikedByMe, false); + expect(updatedPost.totalLikes, initialLikes - 1); + }); + + test('updateFeedBlogPostList - игнорирует обновление если состояние не loaded', () { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + // Состояние loading + feedCubit!.emit(const FeedState.loading()); + + final postToUpdate = feedBlogPostsFixture.feedBlogPostList.first; + feedCubit!.updateFeedBlogPostList(postToUpdate, true); + + // Состояние не должно измениться + expect(feedCubit!.state, const FeedState.loading()); + }); + + test('updateFeedBlogPostList - игнорирует обновление если пост не найден', () { + feedCubit = FeedCubit(feedRepository: mockFeedRepository); + + feedCubit!.emit(FeedState.loaded( + feedBlogPostList: feedBlogPostsFixture.feedBlogPostList, + isLoading: false, + )); + feedCubit!.lastPage = 3; + + // Создаём пост с несуществующим ID + final nonExistentPost = feedBlogPostsFixture.feedBlogPostList.first.copyWith(id: 999); + feedCubit!.updateFeedBlogPostList(nonExistentPost, true); + + // Список не должен измениться + final loadedState = feedCubit!.state as FeedStateLoaded; + expect(loadedState.feedBlogPostList.length, 2); + }); + }); +} + diff --git a/test/features/feed/repositories/feed_repository_test.dart b/test/features/feed/repositories/feed_repository_test.dart new file mode 100644 index 00000000..51932930 --- /dev/null +++ b/test/features/feed/repositories/feed_repository_test.dart @@ -0,0 +1,344 @@ +import 'package:eatvio_app/core/models/pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/comment_model.dart'; +import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_model.dart'; +import 'package:eatvio_app/features/feed/models/feed_blog_post_settings_model.dart'; +import 'package:eatvio_app/features/feed/models/feed_consultant_model.dart'; +import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_model.dart'; +import 'package:eatvio_app/features/feed/models/recipes_pagination_model.dart'; +import 'package:eatvio_app/features/feed/models/view_model.dart'; +import 'package:eatvio_app/features/feed/repositories/feed_repository.dart'; +import 'package:eatvio_app/features/feed/services/feed_service.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'feed_repository_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + group('FeedRepository', () { + late FeedRepository feedRepository; + late MockFeedService mockFeedService; + + setUp(() { + mockFeedService = MockFeedService(); + feedRepository = FeedRepository(feedService: mockFeedService); + }); + + test('getFeedBlogPosts - успешное получение постов', () async { + final expectedModel = FeedBlogPostSettingsModel( + feedBlogPostList: const [], + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getBlogPostList(page: 1, query: null)) + .thenAnswer((_) async => expectedModel); + + final result = await feedRepository.getFeedBlogPosts(); + + expect(result, isA()); + verify(mockFeedService.getBlogPostList(page: 1, query: null)).called(1); + }); + + test('getFeedBlogPosts - с запросом и страницей', () async { + const query = 'тест'; + const page = 2; + final expectedModel = FeedBlogPostSettingsModel( + feedBlogPostList: const [], + meta: const Meta( + currentPage: page, + from: null, + lastPage: 2, + links: [], + path: null, + perPage: 10, + to: null, + total: 15, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getBlogPostList(page: page, query: query)) + .thenAnswer((_) async => expectedModel); + + final result = await feedRepository.getFeedBlogPosts(query: query, page: page); + + expect(result, isA()); + verify(mockFeedService.getBlogPostList(page: page, query: query)).called(1); + }); + + test('getBlogPost - успешное получение поста', () async { + const postId = 1; + final expectedPost = FeedBlogPostModel( + id: postId, + state: 'published', + image: 'https://example.com/image.jpg', + description: 'Описание', + author: const Author( + authorSlug: 'author-slug', + authorName: 'Автор', + authorProfileIcon: 'https://example.com/photo.jpg', + authorIsMe: false, + isEntitySubscribedByMe: null, + authorPlaceInRating: null, + ), + videoUrl: '', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + title: 'Заголовок', + totalLikes: 10, + totalViews: 100, + totalComments: 5, + wasLikedByMe: false, + ); + + when(mockFeedService.getBlogPost(postId: postId)) + .thenAnswer((_) async => expectedPost); + + final result = await feedRepository.getBlogPost(id: postId); + + expect(result, isA()); + expect(result.id, postId); + verify(mockFeedService.getBlogPost(postId: postId)).called(1); + }); + + test('getRecipes - успешное получение рецептов', () async { + final expectedRecipes = const RecipesPagination( + recipesList: [], + ); + + when(mockFeedService.getRecipes(query: null, tags: null, page: 1)) + .thenAnswer((_) async => expectedRecipes); + + final result = await feedRepository.getRecipes(); + + expect(result, isA>()); + verify(mockFeedService.getRecipes(query: null, tags: null, page: 1)).called(1); + }); + + test('getDiets - успешное получение диет', () async { + final expectedDiets = DietsPagination( + data: const [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getDiets( + page: 1, + query: null, + types: null, + duration: null, + sorting: null, + )).thenAnswer((_) async => expectedDiets); + + final result = await feedRepository.getDiets(); + + expect(result, isA()); + verify(mockFeedService.getDiets( + page: 1, + query: null, + types: null, + duration: null, + sorting: null, + )).called(1); + }); + + test('getPlans - успешное получение планов', () async { + final expectedPlans = PlanPaginationModel( + data: const [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).thenAnswer((_) async => expectedPlans); + + final result = await feedRepository.getPlans(); + + expect(result, isA()); + verify(mockFeedService.getPlans( + page: 1, + query: null, + types: null, + duration: null, + minPrice: null, + maxPrice: null, + )).called(1); + }); + + test('getBlogPostComments - успешное получение комментариев', () async { + const postId = '1'; + final expectedComments = CommentModel( + data: [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getBlogPostComments(id: postId)) + .thenAnswer((_) async => expectedComments); + + final result = await feedRepository.getBlogPostComments(id: postId); + + expect(result, isA()); + verify(mockFeedService.getBlogPostComments(id: postId)).called(1); + }); + + test('sendBlogPostComment - успешная отправка комментария', () async { + const postId = '1'; + const commentText = 'Тестовый комментарий'; + + when(mockFeedService.sendBlogPostComment( + id: postId, + body: commentText, + parentCommentId: null, + )).thenAnswer((_) async => Future.value()); + + await feedRepository.sendBlogPostComment(id: postId, commentText: commentText); + + verify(mockFeedService.sendBlogPostComment( + id: postId, + body: commentText, + parentCommentId: null, + )).called(1); + }); + + test('likeBlogPost - успешный лайк поста', () async { + const postId = 1; + + when(mockFeedService.likeBlogPost(id: postId)) + .thenAnswer((_) async => Future.value()); + + await feedRepository.likeBlogPost(id: postId); + + verify(mockFeedService.likeBlogPost(id: postId)).called(1); + }); + + test('unlikedBlogPost - успешное снятие лайка', () async { + const postId = 1; + + when(mockFeedService.unlikedBlogPost(id: postId)) + .thenAnswer((_) async => Future.value()); + + await feedRepository.unlikedBlogPost(id: postId); + + verify(mockFeedService.unlikedBlogPost(id: postId)).called(1); + }); + + test('getConsultants - успешное получение консультантов', () async { + final expectedConsultants = FeedConsultantPaginationModel( + data: const [], + links: const Links(first: '', last: '', prev: null, next: null), + meta: const Meta( + currentPage: 1, + from: null, + lastPage: 1, + links: [], + path: null, + perPage: 10, + to: null, + total: 0, + totalMinPrice: null, + totalMaxPrice: null, + ), + ); + + when(mockFeedService.getConsultants(name: null)) + .thenAnswer((_) async => expectedConsultants); + + final result = await feedRepository.getConsultants(); + + expect(result, isA>()); + verify(mockFeedService.getConsultants(name: null)).called(1); + }); + + test('viewPost - успешное просмотр поста', () async { + const postId = 1; + + when(mockFeedService.viewPost(id: postId)) + .thenAnswer((_) async => Future.value()); + + await feedRepository.viewPost(postId); + + verify(mockFeedService.viewPost(id: postId)).called(1); + }); + + test('viewContent - успешный просмотр контента', () async { + final viewModel = ViewModel( + type: 'blog_post', + ids: [1], + slugs: null, + ); + + when(mockFeedService.viewContent(viewModel: viewModel)) + .thenAnswer((_) async => Future.value()); + + await feedRepository.viewContent(viewModel: viewModel); + + verify(mockFeedService.viewContent(viewModel: viewModel)).called(1); + }); + + test('getFeedBlogPosts - пробрасывает исключение', () async { + when(mockFeedService.getBlogPostList(page: 1, query: null)) + .thenThrow(Exception('Ошибка сети')); + + expect( + () => feedRepository.getFeedBlogPosts(), + throwsException, + ); + }); + }); +} + diff --git a/test/fixtures/auth/oauth_signin_request.json b/test/fixtures/auth/oauth_signin_request.json new file mode 100644 index 00000000..c79c9979 --- /dev/null +++ b/test/fixtures/auth/oauth_signin_request.json @@ -0,0 +1,7 @@ +{ + "provider": "vk", + "accessToken": "vk_access_token_123", + "refreshToken": "vk_refresh_token_123", + "clientId": "vk_client_id_123" +} + diff --git a/test/fixtures/auth/oauth_signup_request.json b/test/fixtures/auth/oauth_signup_request.json new file mode 100644 index 00000000..457b3c46 --- /dev/null +++ b/test/fixtures/auth/oauth_signup_request.json @@ -0,0 +1,16 @@ +{ + "provider": "vk", + "accessToken": "vk_access_token_123", + "id": "vk_user_id_123", + "refreshToken": "vk_refresh_token_123", + "firstName": "Иван", + "lastName": "Иванов", + "email": "ivan@example.com", + "phone": "+79001234567", + "dateOfBirth": "1990-01-01", + "gender": "М", + "weight": 75, + "height": 180, + "clientId": "vk_client_id_123" +} + diff --git a/test/fixtures/auth/reset_password_request.json b/test/fixtures/auth/reset_password_request.json new file mode 100644 index 00000000..ef60f635 --- /dev/null +++ b/test/fixtures/auth/reset_password_request.json @@ -0,0 +1,7 @@ +{ + "email": "user@example.com", + "code": "123456", + "password": "newPassword123", + "passwordConfirmation": "newPassword123" +} + diff --git a/test/fixtures/feed/feed_blog_posts_response.json b/test/fixtures/feed/feed_blog_posts_response.json new file mode 100644 index 00000000..ee9955c8 --- /dev/null +++ b/test/fixtures/feed/feed_blog_posts_response.json @@ -0,0 +1,33 @@ +{ + "feedBlogPostList": [ + { + "id": 1, + "title": "Тестовый пост", + "content": "Содержимое поста", + "author": "Автор", + "createdAt": "2024-01-01T00:00:00Z", + "totalLikes": 10, + "wasLikedByMe": false, + "totalComments": 5, + "totalViews": 100 + }, + { + "id": 2, + "title": "Второй пост", + "content": "Еще одно содержимое", + "author": "Другой автор", + "createdAt": "2024-01-02T00:00:00Z", + "totalLikes": 20, + "wasLikedByMe": true, + "totalComments": 10, + "totalViews": 200 + } + ], + "meta": { + "currentPage": 1, + "lastPage": 3, + "perPage": 10, + "total": 25 + } +} +