Merge remote-tracking branch 'upstream/main' into issue-975

This commit is contained in:
misha
2026-07-15 15:17:44 +03:00
74 changed files with 3880 additions and 251 deletions

View File

@@ -1,4 +1,5 @@
import 'package:eatvio_app/core/app_router.dart';
import 'package:eatvio_app/core/routing/route_constants.dart';
import 'package:eatvio_app/features/auth/entities/auth_result_entity.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -6,6 +7,50 @@ import 'package:go_router/go_router.dart';
void main() {
group('AppRouter', () {
group('resolveDeepLinkTarget - finance/alfa/return', () {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
test('finance/alfa/return/{orderId} → chat-screen?orderId=', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/finance/alfa/return/$orderId'),
);
expect(target, '${RouteConstants.chatScreen}?orderId=$orderId');
});
test('ru/finance/alfa/return/{orderId} → chat-screen?orderId= (metaSegments)', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/ru/finance/alfa/return/$orderId'),
);
expect(target, '${RouteConstants.chatScreen}?orderId=$orderId');
});
test('неполный path /finance/alfa → feed', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/finance/alfa'),
);
expect(target, RouteConstants.feedScreen);
});
test('неполный path /finance → feed', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/finance'),
);
expect(target, RouteConstants.feedScreen);
});
test('регресс: chat/{uuid} → chat-screen?uuid=', () {
final target = AppRouter.resolveDeepLinkTarget(
Uri.parse('https://eatvio.ru/ru/chat/uuid-1'),
);
expect(target, '${RouteConstants.chatScreen}?uuid=uuid-1');
});
});
test('navigatorKey инициализирован', () {
expect(AppRouter.navigatorKey, isNotNull);
expect(AppRouter.navigatorKey, isA<GlobalKey<NavigatorState>>());

View File

@@ -0,0 +1,22 @@
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/fixture_reader.dart';
void main() {
group('OrderIdToConversationModel', () {
test('fromJson создаёт модель из JSON фикстуры', () {
final json = FixtureReader.readAsMap(
'chat/payments/order_id_to_chat_uuid_response.json',
);
final model = OrderIdToConversationModel.fromJson(json);
expect(model.chosenPlanId, 12);
expect(
model.conversationUuid,
'fbd81429-8019-499a-80e8-e3933cc4d872',
);
});
});
}

View File

@@ -1,25 +1,47 @@
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/fixture_reader.dart';
void main() {
group('PaymentModel', () {
test('создает PaymentModel с правильными значениями', () {
final model = PaymentModel(
paymentUrl: 'https://example.com/payment',
);
final model = PaymentModel(paymentUrl: 'https://example.com/payment');
expect(model.paymentUrl, 'https://example.com/payment');
});
test('fromJson создает PaymentModel из JSON', () {
final json = {
'checkout_url': 'https://example.com/payment',
};
final json = {'checkout_url': 'https://example.com/payment'};
final model = PaymentModel.fromJson(json);
expect(model.paymentUrl, 'https://example.com/payment');
});
test(
'fromJson парсит ответ alfa-bank (checkout_url, order_id, payment_provider)',
() {
final json = FixtureReader.readAsMap(
'chat/payments/payment_response_alfa_bank.json',
);
final model = PaymentModel.fromJson(json);
expect(model.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
expect(model.orderId, 'alfa_c29_k15138_t1712345678_r1234');
expect(model.paymentProvider, 'alfa-bank');
},
);
test('fromJson парсит mock-ответ (checkout_url без order_id)', () {
final json = FixtureReader.readAsMap('chat/payments/payment_response.json');
final model = PaymentModel.fromJson(json);
expect(model.paymentUrl, 'https://example.com/checkout/payment-123');
expect(model.orderId, isNull);
expect(model.paymentProvider, isNull);
});
});
}

View File

@@ -11,6 +11,7 @@ import 'package:eatvio_app/features/chat/presentation/bloc/chat_room_cubit/chat_
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart';
import 'package:eatvio_app/features/my_finance/models/card_model.dart';
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/feed/models/diets_pagination_model.dart';
import 'package:eatvio_app/features/feed/models/plan_pagination_model.dart';
@@ -102,6 +103,89 @@ void main() {
// Let's check that at least the repository methods were called
verify(mockChatRepository.getChatRoom(uuid)).called(1);
verify(mockChatRepository.getChatMessages(uuid, 1)).called(1);
verifyNever(
mockChatRepository.resolveOrderIdToConversation(orderId: anyNamed('orderId')),
);
});
test('initChat - с orderId резолвит conversation и инициализирует чат', () async {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
const conversationUuid = 'fbd81429-8019-499a-80e8-e3933cc4d872';
const chosenPlanId = 12;
final resolved = OrderIdToConversationModel(
chosenPlanId: chosenPlanId,
conversationUuid: conversationUuid,
);
final chatRoomWithPlan = testChatRoom.copyWith(chosenPlanId: chosenPlanId);
const testPlan = PlanModel(
id: 1,
slug: 'test-plan',
name: 'Test Plan',
image: 'https://example.com/image.jpg',
duration: 7,
state: 'active',
type: PlanType(fit: true, slim: false, behavior: false, wellness: false),
details: 'Test details',
packetsCount: 3,
author: Author(
authorSlug: 'author',
authorName: 'Author Name',
authorProfileIcon: 'https://example.com/icon.jpg',
authorIsMe: false,
),
totalLikes: 10,
totalViews: 100,
totalComments: 5,
isFavorite: false,
minPriceOfOptions: 1000,
maxPriceOfOptions: 2000,
wasLikedByMe: false,
);
final chosenPlan = ChosenPlanModel(
id: chosenPlanId,
optionType: 'options1',
startDate: DateTime.now(),
chosenPlanState: ChosenPlanStateEnum.statePendingPayment,
chatId: 'chat-1',
consultantId: 2,
plan: testPlan,
);
when(
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
).thenAnswer((_) async => resolved);
when(mockChatRepository.getChatRoom(conversationUuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(conversationUuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: chosenPlanId)).thenAnswer((_) async => chosenPlan);
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
when(mockWsManager.directChatStream).thenReturn(null);
when(mockWsManager.clientTypingStream).thenReturn(null);
await chatRoomCubit.initChat(null, orderId: orderId);
await Future.delayed(const Duration(milliseconds: 200));
verify(mockChatRepository.resolveOrderIdToConversation(orderId: orderId)).called(1);
verify(mockChatRepository.getChatRoom(conversationUuid)).called(1);
verify(mockChatRepository.getChosenPlan(id: chosenPlanId)).called(1);
verify(mockChatRepository.getChatMessages(conversationUuid, 1)).called(1);
expect(chatRoomCubit.state, isA<ChatRoomStateLoaded>());
final loaded = chatRoomCubit.state as ChatRoomStateLoaded;
expect(loaded.chatRoom.chosenPlan?.id, chosenPlanId);
});
test('initChat - при ошибке resolve не вызывает getChatRoom', () async {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
when(
mockChatRepository.resolveOrderIdToConversation(orderId: orderId),
).thenThrow(Exception('Resolve failed'));
await chatRoomCubit.initChat(null, orderId: orderId);
await Future.delayed(const Duration(milliseconds: 100));
expect(chatRoomCubit.state, isA<ChatRoomStateError>());
verifyNever(mockChatRepository.getChatRoom(any));
});
test('initChat - обрабатывает ошибку', () async {
@@ -678,7 +762,7 @@ void main() {
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.getPaymentLink(id: planId)).thenAnswer((_) async => PaymentModel(paymentUrl: paymentUrl));
when(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).thenAnswer((_) async => PaymentModel(paymentUrl: paymentUrl));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);
@@ -691,7 +775,7 @@ void main() {
final result = await chatRoomCubit.getPaymentLink();
expect(result, paymentUrl);
verify(mockChatRepository.getPaymentLink(id: planId)).called(1);
verify(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).called(1);
});
test('getPaymentLink - обрабатывает ошибку', () async {
@@ -730,7 +814,7 @@ void main() {
when(mockChatRepository.getChatRoom(uuid)).thenAnswer((_) async => chatRoomWithPlan);
when(mockChatRepository.getChatMessages(uuid, 1)).thenAnswer((_) async => testChatMessages);
when(mockChatRepository.getChosenPlan(id: planId)).thenAnswer((_) async => chosenPlan);
when(mockChatRepository.getPaymentLink(id: planId)).thenThrow(Exception('Payment error'));
when(mockChatRepository.getPaymentLink(id: planId, paymentProvider: 'alfa-bank')).thenThrow(Exception('Payment error'));
when(mockWsManager.client).thenReturn(null);
when(mockWsManager.initPrivateDirectChatChannel(any)).thenAnswer((_) async => Future.value());
when(mockWsManager.subscribeDirectChatChannel()).thenReturn(null);

View File

@@ -149,7 +149,8 @@ void main() {
when(chatRoomCubit.state).thenReturn(state);
when(chatRoomCubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
when(chatRoomCubit.close()).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(chatRoomCubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'), orderId: anyNamed('orderId')))
.thenAnswer((_) async => 'uuid');
when(chatRoomCubit.setTimer()).thenReturn(null);

View File

@@ -33,7 +33,8 @@ void main() {
when(cubit.state).thenReturn(const ChatRoomState.loading());
when(cubit.stream).thenAnswer((_) => const Stream<ChatRoomState>.empty());
when(cubit.close()).thenAnswer((_) async => Future<void>.value());
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'))).thenAnswer((_) async => Future<void>.value());
when(cubit.initChat(any, sendInitialMessage: anyNamed('sendInitialMessage'), orderId: anyNamed('orderId')))
.thenAnswer((_) async => 'uuid');
when(cubit.setTimer()).thenReturn(null);
sl.registerFactory<ChatRoomCubit>(() => cubit);

View File

@@ -5,12 +5,15 @@ import 'package:dio/dio.dart';
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart' as chat_room;
import 'package:eatvio_app/features/chat/models/chat_room_model.dart'
as chat_room;
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/repositories/chat_repository.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
@@ -32,7 +35,9 @@ void main() {
});
test('emitNewMessageToChats - эмитит новое сообщение в поток', () async {
final messageFixture = FixtureReader.readAsMap('chat/chat_message_response.json');
final messageFixture = FixtureReader.readAsMap(
'chat/chat_message_response.json',
);
final message = ChatMessageModel.fromJson(messageFixture);
chatRepository.emitNewMessageToChats(message);
@@ -45,12 +50,14 @@ void main() {
const data = 'Test message';
const socketId = 'socket-id-123';
when(mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: null,
socketId: socketId,
)).thenAnswer((_) async => Future.value());
when(
mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: null,
socketId: socketId,
),
).thenAnswer((_) async => Future.value());
// Note: SocketIdStorage is static and can't be easily mocked
// This test verifies the repository calls the service correctly
@@ -68,12 +75,14 @@ void main() {
const data = 'Test message with files';
const socketId = 'socket-id-123';
when(mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: anyNamed('mediaData'),
socketId: socketId,
)).thenAnswer((_) async => Future.value());
when(
mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: anyNamed('mediaData'),
socketId: socketId,
),
).thenAnswer((_) async => Future.value());
// Создаем временные файлы для теста
final tempFile1 = File('test1.jpg');
@@ -113,11 +122,14 @@ void main() {
test('getChatRoom - получает комнату чата по UUID', () async {
const uuid = 'chat-1';
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
final roomFixture = FixtureReader.readAsMap(
'chat/chat_room_response.json',
);
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
when(mockChatService.getChatRoom(uuid: uuid))
.thenAnswer((_) async => expectedRoom);
when(
mockChatService.getChatRoom(uuid: uuid),
).thenAnswer((_) async => expectedRoom);
final result = await chatRepository.getChatRoom(uuid);
@@ -132,18 +144,20 @@ void main() {
when(mockChatService.getChatRoom(uuid: uuid)).thenThrow(error);
expect(
() => chatRepository.getChatRoom(uuid),
throwsException,
);
expect(() => chatRepository.getChatRoom(uuid), throwsException);
});
test('getChatRooms - получает список комнат чата', () async {
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
final paginationFixture = FixtureReader.readAsMap(
'chat/chat_room_pagination_response.json',
);
final expectedPagination = ChatRoomPaginationModel.fromJson(
paginationFixture,
);
when(mockChatService.getChatRooms(query: null, page: 1))
.thenAnswer((_) async => expectedPagination);
when(
mockChatService.getChatRooms(query: null, page: 1),
).thenAnswer((_) async => expectedPagination);
final result = await chatRepository.getChatRooms();
@@ -154,11 +168,16 @@ void main() {
test('getChatRooms - получает список комнат с запросом', () async {
const query = 'test query';
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
final paginationFixture = FixtureReader.readAsMap(
'chat/chat_room_pagination_response.json',
);
final expectedPagination = ChatRoomPaginationModel.fromJson(
paginationFixture,
);
when(mockChatService.getChatRooms(query: query, page: 1))
.thenAnswer((_) async => expectedPagination);
when(
mockChatService.getChatRooms(query: query, page: 1),
).thenAnswer((_) async => expectedPagination);
final result = await chatRepository.getChatRooms(query: query);
@@ -181,8 +200,9 @@ void main() {
),
);
when(mockChatService.getChatRooms(query: null, page: 2))
.thenAnswer((_) async => expectedPagination);
when(
mockChatService.getChatRooms(query: null, page: 2),
).thenAnswer((_) async => expectedPagination);
final result = await chatRepository.getChatRooms(page: 2);
@@ -193,11 +213,14 @@ void main() {
test('getChatMessages - получает сообщения чата', () async {
const chatId = 'chat-1';
const page = 1;
final messagesFixture = FixtureReader.readAsMap('chat/chat_messages_response.json');
final messagesFixture = FixtureReader.readAsMap(
'chat/chat_messages_response.json',
);
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
when(mockChatService.getChatMessages(id: chatId, page: page))
.thenAnswer((_) async => expectedMessages);
when(
mockChatService.getChatMessages(id: chatId, page: page),
).thenAnswer((_) async => expectedMessages);
final result = await chatRepository.getChatMessages(chatId, page);
@@ -206,42 +229,53 @@ void main() {
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
});
test('getChatRoomIdByUserSlug - получает комнату по slug пользователя', () async {
const slug = 'user-1';
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
test(
'getChatRoomIdByUserSlug - получает комнату по slug пользователя',
() async {
const slug = 'user-1';
final roomFixture = FixtureReader.readAsMap(
'chat/chat_room_response.json',
);
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
when(mockChatService.getChatRoomIdByUserSlug(user: slug))
.thenAnswer((_) async => expectedRoom);
when(
mockChatService.getChatRoomIdByUserSlug(user: slug),
).thenAnswer((_) async => expectedRoom);
final result = await chatRepository.getChatRoomIdByUserSlug(slug: slug);
final result = await chatRepository.getChatRoomIdByUserSlug(slug: slug);
expect(result, isA<chat_room.ChatRoomModel>());
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
});
expect(result, isA<chat_room.ChatRoomModel>());
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
},
);
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
const chatId = 'chat-id-123';
final messageIds = [1, 2, 3];
when(mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
)).thenAnswer((_) async => Future.value());
when(
mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
),
).thenAnswer((_) async => Future.value());
await chatRepository.markMessagesAsRead(chatId, messageIds);
verify(mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
)).called(1);
verify(
mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
),
).called(1);
});
test('cancelChosenPlan - отменяет выбранный план', () async {
const planId = 123;
when(mockChatService.cancelChosenPlan(id: planId))
.thenAnswer((_) async => Future.value());
when(
mockChatService.cancelChosenPlan(id: planId),
).thenAnswer((_) async => Future.value());
await chatRepository.cancelChosenPlan(id: planId);
@@ -252,36 +286,114 @@ void main() {
const planId = 123;
final date = {'start_date': '2024-01-15'};
when(mockChatService.choseStartPlanDate(id: planId, date: date))
.thenAnswer((_) async => Future.value());
when(
mockChatService.choseStartPlanDate(id: planId, date: date),
).thenAnswer((_) async => Future.value());
await chatRepository.choseStartPlanDate(id: planId, date: date);
verify(mockChatService.choseStartPlanDate(id: planId, date: date)).called(1);
verify(
mockChatService.choseStartPlanDate(id: planId, date: date),
).called(1);
});
test('getPaymentLink - получает ссылку на оплату', () async {
const planId = 123;
final paymentFixture = FixtureReader.readAsMap('chat/payment_response.json');
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
when(mockChatService.getPaymentLink(id: planId))
.thenAnswer((_) async => expectedPayment);
when(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'mock'},
),
).thenAnswer((_) async => expectedPayment);
final result = await chatRepository.getPaymentLink(id: planId);
final result = await chatRepository.getPaymentLink(
id: planId,
paymentProvider: 'mock',
);
expect(result, isA<PaymentModel>());
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
verify(mockChatService.getPaymentLink(id: planId)).called(1);
verify(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'mock'},
),
).called(1);
});
test(
'getPaymentLink - передаёт payment_provider в тело запроса alfa-bank',
() async {
const planId = 123;
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response_alfa_bank.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
when(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).thenAnswer((_) async => expectedPayment);
final result = await chatRepository.getPaymentLink(
id: planId,
paymentProvider: 'alfa-bank',
);
expect(result, isA<PaymentModel>());
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
expect(result.paymentProvider, 'alfa-bank');
verify(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).called(1);
},
);
test('resolveOrderIdToConversation - получает модель по orderId', () async {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
final fixture = FixtureReader.readAsMap(
'chat/payments/order_id_to_chat_uuid_response.json',
);
final expected = OrderIdToConversationModel.fromJson(fixture);
when(
mockChatService.resolveOrderIdToConversation(orderId: orderId),
).thenAnswer((_) async => expected);
final result = await chatRepository.resolveOrderIdToConversation(
orderId: orderId,
);
expect(result, isA<OrderIdToConversationModel>());
expect(result.chosenPlanId, 12);
expect(result.conversationUuid, "fbd81429-8019-499a-80e8-e3933cc4d872");
verify(
mockChatService.resolveOrderIdToConversation(orderId: orderId),
).called(1);
});
test('getChosenPlan - получает выбранный план', () async {
const planId = 1;
final chosenPlanFixture = FixtureReader.readAsMap('chat/chosen_plan_response.json');
final chosenPlanFixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_in_progress_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
when(mockChatService.getChosenPlan(id: planId))
.thenAnswer((_) async => expectedPlan);
when(
mockChatService.getChosenPlan(id: planId),
).thenAnswer((_) async => expectedPlan);
final result = await chatRepository.getChosenPlan(id: planId);
@@ -290,66 +402,140 @@ void main() {
verify(mockChatService.getChosenPlan(id: planId)).called(1);
});
group('getChosenPlan payment polling statuses', () {
test('getChosenPlan - возвращает план в статусе pending_payment', () async {
const planId = 1;
final fixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_pending_payment_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(fixture);
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
final result = await chatRepository.getChosenPlan(id: planId);
expect(result.chosenPlanState, ChosenPlanStateEnum.statePendingPayment);
verify(mockChatService.getChosenPlan(id: planId)).called(1);
});
test('getChosenPlan - возвращает план в статусе paid', () async {
const planId = 1;
final fixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_paid_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(fixture);
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
final result = await chatRepository.getChosenPlan(id: planId);
expect(result.chosenPlanState, ChosenPlanStateEnum.statePaid);
verify(mockChatService.getChosenPlan(id: planId)).called(1);
});
test('getChosenPlan - возвращает план в статусе payment_failed', () async {
const planId = 1;
final fixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_payment_failed_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(fixture);
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
final result = await chatRepository.getChosenPlan(id: planId);
expect(result.chosenPlanState, ChosenPlanStateEnum.statePaymentFailed);
verify(mockChatService.getChosenPlan(id: planId)).called(1);
});
test('getChosenPlan - возвращает план в статусе canceled_by_the_initiator', () async {
const planId = 1;
final fixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(fixture);
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
final result = await chatRepository.getChosenPlan(id: planId);
expect(result.chosenPlanState, ChosenPlanStateEnum.stateCanceledByTheInitiator);
verify(mockChatService.getChosenPlan(id: planId)).called(1);
});
});
test('finishChosenPlan - завершает выбранный план', () async {
const planId = 123;
when(mockChatService.finishChosenPlan(id: planId))
.thenAnswer((_) async => Future.value());
when(
mockChatService.finishChosenPlan(id: planId),
).thenAnswer((_) async => Future.value());
await chatRepository.finishChosenPlan(id: planId);
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
});
test('complainAboutConsultation - отправляет жалобу на консультацию', () async {
const planId = 123;
final complainFixture = FixtureReader.readAsMap('chat/plan_complain_response.json');
final complainModel = PlanComplainModel.fromJson(complainFixture);
test(
'complainAboutConsultation - отправляет жалобу на консультацию',
() async {
const planId = 123;
final complainFixture = FixtureReader.readAsMap(
'chat/plan_complain_response.json',
);
final complainModel = PlanComplainModel.fromJson(complainFixture);
when(mockChatService.complainAboutConsultation(id: planId, body: complainModel))
.thenAnswer((_) async => Future.value());
when(
mockChatService.complainAboutConsultation(
id: planId,
body: complainModel,
),
).thenAnswer((_) async => Future.value());
await chatRepository.complainAboutConsultation(
id: planId,
body: complainModel,
);
await chatRepository.complainAboutConsultation(
id: planId,
body: complainModel,
);
verify(mockChatService.complainAboutConsultation(id: planId, body: complainModel))
.called(1);
});
verify(
mockChatService.complainAboutConsultation(
id: planId,
body: complainModel,
),
).called(1);
},
);
test('getChatByUserSlug - возвращает ID чата при успешном ответе', () async {
const slug = 'user-slug';
const chatId = 'chat-id-123';
final response = HttpResponse(
{'id': chatId},
Response(
requestOptions: RequestOptions(path: ''),
statusCode: 200,
),
);
test(
'getChatByUserSlug - возвращает ID чата при успешном ответе',
() async {
const slug = 'user-slug';
const chatId = 'chat-id-123';
final response = HttpResponse({
'id': chatId,
}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
when(mockChatService.getChatByUserSlug(slug: slug))
.thenAnswer((_) async => response);
when(
mockChatService.getChatByUserSlug(slug: slug),
).thenAnswer((_) async => response);
final result = await chatRepository.getChatByUserSlug(slug);
final result = await chatRepository.getChatByUserSlug(slug);
expect(result, chatId);
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
});
expect(result, chatId);
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
},
);
test('getChatByUserSlug - возвращает 404 при отсутствии чата', () async {
const slug = 'user-slug';
final response = HttpResponse(
{},
Response(
requestOptions: RequestOptions(path: ''),
statusCode: 404,
),
Response(requestOptions: RequestOptions(path: ''), statusCode: 404),
);
when(mockChatService.getChatByUserSlug(slug: slug))
.thenAnswer((_) async => response);
when(
mockChatService.getChatByUserSlug(slug: slug),
).thenAnswer((_) async => response);
final result = await chatRepository.getChatByUserSlug(slug);
@@ -357,17 +543,20 @@ void main() {
verify(mockChatService.getChatByUserSlug(slug: slug)).called(1);
});
test('getChatByUserSlug - пробрасывает исключение при ошибке сети', () async {
const slug = 'user-slug';
test(
'getChatByUserSlug - пробрасывает исключение при ошибке сети',
() async {
const slug = 'user-slug';
when(mockChatService.getChatByUserSlug(slug: slug))
.thenThrow(Exception('Network error'));
when(
mockChatService.getChatByUserSlug(slug: slug),
).thenThrow(Exception('Network error'));
await expectLater(
chatRepository.getChatByUserSlug(slug),
throwsA(isA<Exception>()),
);
});
await expectLater(
chatRepository.getChatByUserSlug(slug),
throwsA(isA<Exception>()),
);
},
);
});
}

View File

@@ -1,8 +1,10 @@
import 'package:dio/dio.dart';
import 'package:eatvio_app/features/chat/models/chat_message_model.dart';
import 'package:eatvio_app/features/chat/models/chat_messages_model.dart';
import 'package:eatvio_app/features/chat/models/chat_room_model.dart' as chat_room;
import 'package:eatvio_app/features/chat/models/chat_room_model.dart'
as chat_room;
import 'package:eatvio_app/features/chat/models/chat_room_pagination_model.dart';
import 'package:eatvio_app/features/chat/models/order_id_to_conversation_model.dart';
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/chat/models/plan_complain_model.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
@@ -24,10 +26,16 @@ void main() {
});
test('getChatRooms - возвращает список комнат чата', () async {
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
final paginationFixture = FixtureReader.readAsMap(
'chat/chat_room_pagination_response.json',
);
final expectedPagination = ChatRoomPaginationModel.fromJson(
paginationFixture,
);
when(mockChatService.getChatRooms(query: null, page: 1)).thenAnswer((_) async => expectedPagination);
when(
mockChatService.getChatRooms(query: null, page: 1),
).thenAnswer((_) async => expectedPagination);
final result = await mockChatService.getChatRooms(query: null, page: 1);
@@ -38,10 +46,16 @@ void main() {
test('getChatRooms - возвращает список комнат с запросом', () async {
const query = 'test query';
final paginationFixture = FixtureReader.readAsMap('chat/chat_room_pagination_response.json');
final expectedPagination = ChatRoomPaginationModel.fromJson(paginationFixture);
final paginationFixture = FixtureReader.readAsMap(
'chat/chat_room_pagination_response.json',
);
final expectedPagination = ChatRoomPaginationModel.fromJson(
paginationFixture,
);
when(mockChatService.getChatRooms(query: query, page: 1)).thenAnswer((_) async => expectedPagination);
when(
mockChatService.getChatRooms(query: query, page: 1),
).thenAnswer((_) async => expectedPagination);
final result = await mockChatService.getChatRooms(query: query, page: 1);
@@ -51,10 +65,14 @@ void main() {
test('getChatRoom - возвращает комнату чата по UUID', () async {
const uuid = 'chat-1';
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
final roomFixture = FixtureReader.readAsMap(
'chat/chat_room_response.json',
);
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
when(mockChatService.getChatRoom(uuid: uuid)).thenAnswer((_) async => expectedRoom);
when(
mockChatService.getChatRoom(uuid: uuid),
).thenAnswer((_) async => expectedRoom);
final result = await mockChatService.getChatRoom(uuid: uuid);
@@ -67,62 +85,114 @@ void main() {
const slug = 'user-slug';
const data = 'Test message';
const socketId = 'socket-id-123';
final messageFixture = FixtureReader.readAsMap('chat/chat_message_response.json');
final messageFixture = FixtureReader.readAsMap(
'chat/chat_message_response.json',
);
final expectedMessage = ChatMessageModel.fromJson(messageFixture);
// ignore: null_argument_to_non_null_type
when(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).thenAnswer((_) async => expectedMessage);
when(
mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: null,
socketId: socketId,
),
).thenAnswer((_) async => expectedMessage);
final result = await mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId);
final result = await mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: null,
socketId: socketId,
);
expect(result, isA<ChatMessageModel>());
expect(result.data, data);
verify(mockChatService.sendMessage(slug: slug, data: data, mediaData: null, socketId: socketId)).called(1);
verify(
mockChatService.sendMessage(
slug: slug,
data: data,
mediaData: null,
socketId: socketId,
),
).called(1);
});
test('getChatMessages - возвращает сообщения чата', () async {
const chatId = 'chat-1';
const page = 1;
final messagesFixture = FixtureReader.readAsMap('chat/chat_messages_response.json');
final messagesFixture = FixtureReader.readAsMap(
'chat/chat_messages_response.json',
);
final expectedMessages = ChatMessagesModel.fromJson(messagesFixture);
when(mockChatService.getChatMessages(id: chatId, page: page)).thenAnswer((_) async => expectedMessages);
when(
mockChatService.getChatMessages(id: chatId, page: page),
).thenAnswer((_) async => expectedMessages);
final result = await mockChatService.getChatMessages(id: chatId, page: page);
final result = await mockChatService.getChatMessages(
id: chatId,
page: page,
);
expect(result, isA<ChatMessagesModel>());
expect(result.messagesList.length, 2);
verify(mockChatService.getChatMessages(id: chatId, page: page)).called(1);
});
test('getChatRoomIdByUserSlug - возвращает комнату по slug пользователя', () async {
const slug = 'user-1';
final roomFixture = FixtureReader.readAsMap('chat/chat_room_response.json');
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
test(
'getChatRoomIdByUserSlug - возвращает комнату по slug пользователя',
() async {
const slug = 'user-1';
final roomFixture = FixtureReader.readAsMap(
'chat/chat_room_response.json',
);
final expectedRoom = chat_room.ChatRoomModel.fromJson(roomFixture);
when(mockChatService.getChatRoomIdByUserSlug(user: slug)).thenAnswer((_) async => expectedRoom);
when(
mockChatService.getChatRoomIdByUserSlug(user: slug),
).thenAnswer((_) async => expectedRoom);
final result = await mockChatService.getChatRoomIdByUserSlug(user: slug);
final result = await mockChatService.getChatRoomIdByUserSlug(
user: slug,
);
expect(result, isA<chat_room.ChatRoomModel>());
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
});
expect(result, isA<chat_room.ChatRoomModel>());
verify(mockChatService.getChatRoomIdByUserSlug(user: slug)).called(1);
},
);
test('markMessagesAsRead - отмечает сообщения как прочитанные', () async {
const chatId = 'chat-id-123';
final messageIds = [1, 2, 3];
when(mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds})).thenAnswer((_) async => Future.value());
when(
mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
),
).thenAnswer((_) async => Future.value());
await mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds});
await mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
);
verify(mockChatService.markMessagesAsRead(id: chatId, body: {'messages_ids': messageIds})).called(1);
verify(
mockChatService.markMessagesAsRead(
id: chatId,
body: {'messages_ids': messageIds},
),
).called(1);
});
test('cancelChosenPlan - отменяет выбранный план', () async {
const planId = 123;
when(mockChatService.cancelChosenPlan(id: planId)).thenAnswer((_) async => Future.value());
when(
mockChatService.cancelChosenPlan(id: planId),
).thenAnswer((_) async => Future.value());
await mockChatService.cancelChosenPlan(id: planId);
@@ -133,33 +203,80 @@ void main() {
const planId = 123;
final date = {'start_date': '2024-01-15'};
when(mockChatService.choseStartPlanDate(id: planId, date: date)).thenAnswer((_) async => Future.value());
when(
mockChatService.choseStartPlanDate(id: planId, date: date),
).thenAnswer((_) async => Future.value());
await mockChatService.choseStartPlanDate(id: planId, date: date);
verify(mockChatService.choseStartPlanDate(id: planId, date: date)).called(1);
verify(
mockChatService.choseStartPlanDate(id: planId, date: date),
).called(1);
});
test('getPaymentLink - возвращает ссылку на оплату', () async {
const planId = 123;
final paymentFixture = FixtureReader.readAsMap('chat/payment_response.json');
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
when(mockChatService.getPaymentLink(id: planId)).thenAnswer((_) async => expectedPayment);
when(
mockChatService.getPaymentLink(id: planId, body: {'payment_provider': 'mock'}),
).thenAnswer((_) async => expectedPayment);
final result = await mockChatService.getPaymentLink(id: planId);
final result = await mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'mock'},
);
expect(result, isA<PaymentModel>());
expect(result.paymentUrl, 'https://example.com/checkout/payment-123');
verify(mockChatService.getPaymentLink(id: planId)).called(1);
verify(mockChatService.getPaymentLink(id: planId, body: {'payment_provider': 'mock'})).called(1);
});
test(
'getPaymentLink - возвращает ссылку на оплату alfa-bank',
() async {
const planId = 123;
final requestFixture = FixtureReader.readAsMap(
'chat/payments/payment_alfabank_request.json',
);
final requestBody = Map<String, String>.from(requestFixture);
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response_alfa_bank.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
when(
mockChatService.getPaymentLink(id: planId, body: requestBody),
).thenAnswer((_) async => expectedPayment);
final result = await mockChatService.getPaymentLink(
id: planId,
body: requestBody,
);
expect(result, isA<PaymentModel>());
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
expect(result.paymentProvider, 'alfa-bank');
verify(
mockChatService.getPaymentLink(id: planId, body: requestBody),
).called(1);
},
);
test('getChosenPlan - возвращает выбранный план', () async {
const planId = 1;
final chosenPlanFixture = FixtureReader.readAsMap('chat/chosen_plan_response.json');
final chosenPlanFixture = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_in_progress_response.json',
);
final expectedPlan = ChosenPlanModel.fromJson(chosenPlanFixture);
when(mockChatService.getChosenPlan(id: planId)).thenAnswer((_) async => expectedPlan);
when(
mockChatService.getChosenPlan(id: planId),
).thenAnswer((_) async => expectedPlan);
final result = await mockChatService.getChosenPlan(id: planId);
@@ -171,31 +288,80 @@ void main() {
test('finishChosenPlan - завершает выбранный план', () async {
const planId = 123;
when(mockChatService.finishChosenPlan(id: planId)).thenAnswer((_) async => Future.value());
when(
mockChatService.finishChosenPlan(id: planId),
).thenAnswer((_) async => Future.value());
await mockChatService.finishChosenPlan(id: planId);
verify(mockChatService.finishChosenPlan(id: planId)).called(1);
});
test('complainAboutConsultation - отправляет жалобу на консультацию', () async {
const planId = 123;
final complainFixture = FixtureReader.readAsMap('chat/plan_complain_response.json');
final complainModel = PlanComplainModel.fromJson(complainFixture);
test(
'complainAboutConsultation - отправляет жалобу на консультацию',
() async {
const planId = 123;
final complainFixture = FixtureReader.readAsMap(
'chat/plan_complain_response.json',
);
final complainModel = PlanComplainModel.fromJson(complainFixture);
when(mockChatService.complainAboutConsultation(id: planId, body: complainModel)).thenAnswer((_) async => Future.value());
when(
mockChatService.complainAboutConsultation(
id: planId,
body: complainModel,
),
).thenAnswer((_) async => Future.value());
await mockChatService.complainAboutConsultation(id: planId, body: complainModel);
await mockChatService.complainAboutConsultation(
id: planId,
body: complainModel,
);
verify(mockChatService.complainAboutConsultation(id: planId, body: complainModel)).called(1);
});
verify(
mockChatService.complainAboutConsultation(
id: planId,
body: complainModel,
),
).called(1);
},
);
test(
'resolveOrderIdToConversation - возвращает модель по orderId',
() async {
const orderId = 'alfa_c29_k15138_t1712345678_r1234';
final fixture = FixtureReader.readAsMap(
'chat/payments/order_id_to_chat_uuid_response.json',
);
final expected = OrderIdToConversationModel.fromJson(fixture);
when(
mockChatService.resolveOrderIdToConversation(orderId: orderId),
).thenAnswer((_) async => expected);
final result = await mockChatService.resolveOrderIdToConversation(
orderId: orderId,
);
expect(result, isA<OrderIdToConversationModel>());
expect(result.chosenPlanId, 12);
verify(
mockChatService.resolveOrderIdToConversation(orderId: orderId),
).called(1);
},
);
test('getChatByUserSlug - возвращает HttpResponse', () async {
const slug = 'user-slug';
const chatId = 'chat-id-123';
final response = HttpResponse({'id': chatId}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
final response = HttpResponse({
'id': chatId,
}, Response(requestOptions: RequestOptions(path: ''), statusCode: 200));
when(mockChatService.getChatByUserSlug(slug: slug)).thenAnswer((_) async => response);
when(
mockChatService.getChatByUserSlug(slug: slug),
).thenAnswer((_) async => response);
final result = await mockChatService.getChatByUserSlug(slug: slug);

View File

@@ -200,20 +200,20 @@ void main() {
.thenAnswer((_) async => chosenPlan);
await cubit.getChosenPlan(id: 1);
when(mockChatRepository.getPaymentLink(id: 1))
when(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank'))
.thenAnswer((_) async => paymentModel);
final result = await cubit.getPaymentLink();
expect(result, equals('https://payment.url'));
verify(mockChatRepository.getPaymentLink(id: 1)).called(1);
verify(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank')).called(1);
});
test('getPaymentLink - возвращает null, если план не выбран', () async {
final result = await cubit.getPaymentLink();
expect(result, isNull);
verifyNever(mockChatRepository.getPaymentLink(id: anyNamed('id')));
verifyNever(mockChatRepository.getPaymentLink(id: anyNamed('id'), paymentProvider: anyNamed('paymentProvider')));
});
test('getPaymentLink - обработка ошибки', () async {
@@ -254,7 +254,7 @@ void main() {
.thenAnswer((_) async => chosenPlan);
await cubit.getChosenPlan(id: 1);
when(mockChatRepository.getPaymentLink(id: 1))
when(mockChatRepository.getPaymentLink(id: 1, paymentProvider: 'alfa-bank'))
.thenThrow(Exception('Ошибка'));
final result = await cubit.getPaymentLink();

View File

@@ -0,0 +1,131 @@
import 'package:eatvio_app/generated/l10n.dart';
import 'package:eatvio_app/features/payment/presentation/payment_flow.dart';
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_modal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
Widget createTestApp() {
return const MaterialApp(
localizationsDelegates: [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: [Locale('ru')],
home: Scaffold(body: SizedBox()),
);
}
group('showConsultationPaymentFlow', () {
late bool gateCalled;
late bool? gateResult;
late bool formCalled;
late PromoCodeResult? formResult;
late bool sbpCalled;
late int sbpPlanId;
Future<bool?> mockGate(BuildContext context) async {
gateCalled = true;
return gateResult;
}
Future<PromoCodeResult?> mockForm(
BuildContext context, {
Future<PromoValidation> Function(String)? onValidate,
}) async {
formCalled = true;
return formResult;
}
Future<void> mockSbp(BuildContext context, {required int planId}) async {
sbpCalled = true;
sbpPlanId = planId;
}
setUp(() {
gateCalled = false;
gateResult = null;
formCalled = false;
formResult = null;
sbpCalled = false;
sbpPlanId = 0;
});
testWidgets('Нет → вызывает SbpBanks (не форму)', (tester) async {
gateResult = false;
await tester.pumpWidget(createTestApp());
await showConsultationPaymentFlow(
tester.element(find.byType(SizedBox)),
planId: 42,
showGate: mockGate,
showForm: mockForm,
showSbp: mockSbp,
);
expect(gateCalled, isTrue);
expect(formCalled, isFalse);
expect(sbpCalled, isTrue);
expect(sbpPlanId, 42);
});
testWidgets('Да → форма → Пропустить → вызывает SbpBanks', (tester) async {
gateResult = true;
formResult = const PromoCodeResult();
await tester.pumpWidget(createTestApp());
await showConsultationPaymentFlow(
tester.element(find.byType(SizedBox)),
planId: 42,
showGate: mockGate,
showForm: mockForm,
showSbp: mockSbp,
);
expect(gateCalled, isTrue);
expect(formCalled, isTrue);
expect(sbpCalled, isTrue);
expect(sbpPlanId, 42);
});
testWidgets('Да → форма отменена (null) → Sbp не вызывается', (tester) async {
gateResult = true;
formResult = null;
await tester.pumpWidget(createTestApp());
await showConsultationPaymentFlow(
tester.element(find.byType(SizedBox)),
planId: 42,
showGate: mockGate,
showForm: mockForm,
showSbp: mockSbp,
);
expect(gateCalled, isTrue);
expect(formCalled, isTrue);
expect(sbpCalled, isFalse);
});
testWidgets('гейт отменён (null) → ничего не вызывается', (tester) async {
gateResult = null;
await tester.pumpWidget(createTestApp());
await showConsultationPaymentFlow(
tester.element(find.byType(SizedBox)),
planId: 42,
showGate: mockGate,
showForm: mockForm,
showSbp: mockSbp,
);
expect(gateCalled, isTrue);
expect(formCalled, isFalse);
expect(sbpCalled, isFalse);
});
});
}

View File

@@ -0,0 +1,188 @@
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_state.dart';
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:sbp/models/c2bmembers_model.dart';
import '../../../../fixtures/fixture_reader.dart';
import 'consultation_payment_cubit_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<PaymentRepository>(),
MockSpec<SbpRepository>(),
])
void main() {
group('ConsultationPaymentCubit', () {
late ConsultationPaymentCubit cubit;
late MockPaymentRepository mockPaymentRepository;
late MockSbpRepository mockSbpRepository;
final installedBank = C2bmemberModel.fromJson(const {
'bankName': 'Сбербанк',
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
'schema': 'bank100000000111',
'package_name': 'ru.sberbankmobile',
'icon': 'packages/sbp/assets/icons/sber.png',
});
late PaymentModel alfaPayment;
setUp(() {
mockPaymentRepository = MockPaymentRepository();
mockSbpRepository = MockSbpRepository();
cubit = ConsultationPaymentCubit(
paymentRepository: mockPaymentRepository,
sbpRepository: mockSbpRepository,
);
alfaPayment = PaymentModel.fromJson(
FixtureReader.readAsMap('chat/payments/payment_response_alfa_bank.json'),
);
});
tearDown(() {
cubit.close();
});
test('начальный state - isLoading=false, banks=[], checkoutUrl=null', () {
expect(cubit.state, isA<ConsultationPaymentState>());
expect(cubit.state.isLoading, isFalse);
expect(cubit.state.banks, isEmpty);
expect(cubit.state.checkoutUrl, isNull);
expect(cubit.state.orderId, isNull);
expect(cubit.state.errorMessage, isNull);
});
test(
'startPayment - успешно получает checkout_url и загружает банки',
() async {
const planId = 123;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(
mockSbpRepository.getInstalledBanks(),
).thenAnswer((_) async => [installedBank]);
await cubit.startPayment(planId: planId);
expect(cubit.state.isLoading, isFalse);
expect(cubit.state.checkoutUrl, alfaPayment.paymentUrl);
expect(cubit.state.orderId, alfaPayment.orderId);
expect(cubit.state.banks, [installedBank]);
expect(cubit.state.errorMessage, isNull);
verify(
mockPaymentRepository.startPaymentProcess(planId: planId),
).called(1);
verify(mockSbpRepository.getInstalledBanks()).called(1);
},
);
test('startPayment - при ошибке API emit errorMessage', () async {
const planId = 123;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenThrow(Exception('Payment start failed'));
await cubit.startPayment(planId: planId);
expect(cubit.state.isLoading, isFalse);
expect(cubit.state.errorMessage, isNotNull);
expect(cubit.state.banks, isEmpty);
expect(cubit.state.checkoutUrl, isNull);
verifyNever(mockSbpRepository.getInstalledBanks());
});
test(
'startPayment - при ошибке SBP после успешного payment emit error',
() async {
const planId = 123;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(
mockSbpRepository.getInstalledBanks(),
).thenThrow(Exception('SBP plugin error'));
await cubit.startPayment(planId: planId);
expect(cubit.state.isLoading, isFalse);
expect(cubit.state.errorMessage, isNotNull);
expect(cubit.state.checkoutUrl, alfaPayment.paymentUrl);
expect(cubit.state.orderId, alfaPayment.orderId);
expect(cubit.state.banks, isEmpty);
},
);
test('openBank - успешно открывает банк после startPayment', () async {
const planId = 123;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(
mockSbpRepository.getInstalledBanks(),
).thenAnswer((_) async => [installedBank]);
when(
mockSbpRepository.openBank(
url: alfaPayment.paymentUrl,
bank: installedBank,
),
).thenAnswer((_) async => true);
await cubit.startPayment(planId: planId);
final result = await cubit.openBank(bank: installedBank);
expect(result, isTrue);
verify(
mockSbpRepository.openBank(
url: alfaPayment.paymentUrl,
bank: installedBank,
),
).called(1);
});
test(
'openBank - без checkoutUrl возвращает false и не вызывает SbpRepository',
() async {
final result = await cubit.openBank(bank: installedBank);
expect(result, isFalse);
verifyNever(
mockSbpRepository.openBank(
url: anyNamed('url'),
bank: anyNamed('bank'),
),
);
},
);
test('openBank - возвращает false когда банк не открылся', () async {
const planId = 123;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(
mockSbpRepository.getInstalledBanks(),
).thenAnswer((_) async => [installedBank]);
when(
mockSbpRepository.openBank(
url: alfaPayment.paymentUrl,
bank: installedBank,
),
).thenAnswer((_) async => false);
await cubit.startPayment(planId: planId);
final result = await cubit.openBank(bank: installedBank);
expect(result, isFalse);
});
});
}

View File

@@ -0,0 +1,65 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/bottom_sheet_handler_bar.dart';
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_gate_bottom_sheet.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('PromoCodeGateBottomSheet', () {
Widget createTestWidget({required ValueChanged<bool?> onResult}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(
body: PromoCodeGateBottomSheet(onResult: onResult),
),
);
}
testWidgets('показывает заголовок, BottomSheetHandlerBar и кнопки Да/Нет', (tester) async {
await tester.pumpWidget(createTestWidget(onResult: (_) {}));
await tester.pump();
expect(find.byType(BottomSheetHandlerBar), findsOneWidget);
expect(find.text('У вас есть промокод?'), findsOneWidget);
expect(find.text('Да'), findsOneWidget);
expect(find.text('Нет'), findsOneWidget);
});
testWidgets('tap Да вызывает onResult(true)', (tester) async {
bool? result;
await tester.pumpWidget(
createTestWidget(onResult: (v) => result = v),
);
await tester.pump();
await tester.tap(find.text('Да'));
await tester.pump();
expect(result, isTrue);
});
testWidgets('tap Нет вызывает onResult(false)', (tester) async {
bool? result;
await tester.pumpWidget(
createTestWidget(onResult: (v) => result = v),
);
await tester.pump();
await tester.tap(find.text('Нет'));
await tester.pump();
expect(result, isFalse);
});
});
}

View File

@@ -0,0 +1,101 @@
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/features/payment/presentation/widgets/promo_code_modal.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('ItvioPromoCodeModal', () {
Widget createTestWidget({
Future<PromoValidation> Function(String)? onValidate,
}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Builder(
builder: (context) => ElevatedButton(
onPressed: () => ItvioPromoCodeModal.show(
context,
onValidate: onValidate,
),
child: const Text('Open'),
),
),
);
}
testWidgets('показывает заголовок Есть промокод? и кнопки Применить/Пропустить', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('Есть промокод?'), findsOneWidget);
expect(find.text('Применить'), findsOneWidget);
expect(find.text('Пропустить'), findsOneWidget);
});
testWidgets('Применить с пустым полем показывает ошибку и не закрывает', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Применить'));
await tester.pump();
expect(find.text('Введите промокод'), findsAtLeast(1));
expect(find.text('Применить'), findsOneWidget);
});
testWidgets('Применить с вводом вызывает onValidate и показывает ошибку', (tester) async {
PromoValidation? captured;
await tester.pumpWidget(
createTestWidget(
onValidate: (code) async {
captured ??= const PromoValidation.failure('Промокод не найден или истёк');
return captured!;
},
),
);
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.enterText(find.byType(TextField), 'TEST123');
await tester.tap(find.text('Применить'));
await tester.pump();
expect(captured, isNotNull);
expect(find.text('Промокод не найден или истёк'), findsOneWidget);
expect(find.text('Применить'), findsOneWidget);
});
testWidgets('Пропустить закрывает шторку', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
await tester.tap(find.text('Пропустить'));
await tester.pumpAndSettle();
expect(find.text('Есть промокод?'), findsNothing);
});
testWidgets('отсутствует UI applied-состояния (Промокод применён)', (tester) async {
await tester.pumpWidget(createTestWidget());
await tester.tap(find.text('Open'));
await tester.pumpAndSettle();
expect(find.text('Промокод применён!'), findsNothing);
expect(find.text('Продолжить'), findsNothing);
expect(find.text('Убрать промокод'), findsNothing);
});
});
}

View File

@@ -0,0 +1,187 @@
import 'dart:async';
import 'package:eatvio_app/core/theme/theme_helper.dart';
import 'package:eatvio_app/core/widgets/loader.dart';
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/payment/presentation/cubit/consultation_payment_cubit/consultation_payment_cubit.dart';
import 'package:eatvio_app/features/payment/presentation/widgets/sbp_banks_bottom_sheet.dart';
import 'package:eatvio_app/features/payment/repositories/payment_repository.dart';
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
import 'package:eatvio_app/generated/l10n.dart';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/annotations.dart';
import 'package:mockito/mockito.dart';
import 'package:sbp/models/c2bmembers_model.dart';
import '../../../../fixtures/fixture_reader.dart';
import 'sbp_banks_bottom_sheet_test.mocks.dart';
@GenerateNiceMocks([
MockSpec<PaymentRepository>(),
MockSpec<SbpRepository>(),
])
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
group('SbpBanksBottomSheet', () {
late MockPaymentRepository mockPaymentRepository;
late MockSbpRepository mockSbpRepository;
late ConsultationPaymentCubit cubit;
late PaymentModel alfaPayment;
final installedBank = C2bmemberModel.fromJson(const {
'bankName': 'Сбербанк',
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
'schema': 'bank100000000111',
'package_name': 'ru.sberbankmobile',
'icon': '',
});
setUp(() {
mockPaymentRepository = MockPaymentRepository();
mockSbpRepository = MockSbpRepository();
cubit = ConsultationPaymentCubit(
paymentRepository: mockPaymentRepository,
sbpRepository: mockSbpRepository,
);
alfaPayment = PaymentModel.fromJson(
FixtureReader.readAsMap('chat/payments/payment_response_alfa_bank.json'),
);
});
tearDown(() {
cubit.close();
});
Widget createTestWidget({required int planId}) {
return MaterialApp(
theme: ThemeHelper().themeData(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ru')],
home: Scaffold(
body: SbpBanksBottomSheet(
planId: planId,
cubit: cubit,
),
),
);
}
testWidgets('показывает Loader пока идёт startPayment', (tester) async {
const planId = 12;
final completer = Completer<PaymentModel>();
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) => completer.future);
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pump();
expect(find.byType(Loader), findsOneWidget);
expect(find.text('Сбербанк'), findsNothing);
completer.complete(alfaPayment);
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
await tester.pumpAndSettle();
});
testWidgets('вызывает startPaymentProcess с переданным planId', (tester) async {
const planId = 12;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pumpAndSettle();
verify(mockPaymentRepository.startPaymentProcess(planId: planId)).called(1);
});
testWidgets('после успеха показывает список банков и скрывает Loader', (tester) async {
const planId = 12;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pumpAndSettle();
expect(find.byType(Loader), findsNothing);
expect(find.text('Выберите банк'), findsOneWidget);
expect(find.text('Сбербанк'), findsOneWidget);
});
testWidgets('tap по банку вызывает openBank с checkout_url', (tester) async {
const planId = 12;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => [installedBank]);
when(
mockSbpRepository.openBank(
url: alfaPayment.paymentUrl,
bank: installedBank,
),
).thenAnswer((_) async => true);
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pumpAndSettle();
await tester.tap(find.text('Сбербанк'));
await tester.pumpAndSettle();
verify(
mockSbpRepository.openBank(
url: alfaPayment.paymentUrl,
bank: installedBank,
),
).called(1);
});
testWidgets('при ошибке API показывает сообщение об ошибке', (tester) async {
const planId = 12;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenThrow(Exception('Payment start failed'));
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pumpAndSettle();
expect(find.byType(Loader), findsNothing);
expect(find.text('Сбербанк'), findsNothing);
expect(
find.text('Не удалось начать оплату. Попробуйте позже'),
findsOneWidget,
);
});
testWidgets('при пустом списке банков показывает empty-сообщение', (tester) async {
const planId = 12;
when(
mockPaymentRepository.startPaymentProcess(planId: planId),
).thenAnswer((_) async => alfaPayment);
when(mockSbpRepository.getInstalledBanks()).thenAnswer((_) async => []);
await tester.pumpWidget(createTestWidget(planId: planId));
await tester.pumpAndSettle();
expect(find.byType(Loader), findsNothing);
expect(find.text('Нет установленных банков'), findsOneWidget);
});
});
}

View File

@@ -0,0 +1,97 @@
import 'package:eatvio_app/features/chat/models/payment_model.dart';
import 'package:eatvio_app/features/chat/services/chat_service.dart';
import 'package:eatvio_app/features/payment/repositories/payment_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 'payment_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<ChatService>()])
void main() {
group('PaymentRepository', () {
late PaymentRepository paymentRepository;
late MockChatService mockChatService;
setUp(() {
mockChatService = MockChatService();
paymentRepository = PaymentRepository(chatService: mockChatService);
});
test(
'startPaymentProcess - возвращает PaymentModel из ответа alfa-bank',
() async {
const planId = 123;
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response_alfa_bank.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
final requestBody = Map<String, String>.from(
FixtureReader.readAsMap('chat/payments/payment_alfabank_request.json'),
);
when(
mockChatService.getPaymentLink(id: planId, body: requestBody),
).thenAnswer((_) async => expectedPayment);
final result = await paymentRepository.startPaymentProcess(planId: planId);
expect(result, isA<PaymentModel>());
expect(result.paymentUrl, 'https://qr.nspk.ru/ASDFASDFASDFASDF');
expect(result.orderId, 'alfa_c29_k15138_t1712345678_r1234');
expect(result.paymentProvider, 'alfa-bank');
verify(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).called(1);
},
);
test(
'startPaymentProcess - передаёт payment_provider alfa-bank в тело запроса',
() async {
const planId = 42;
final paymentFixture = FixtureReader.readAsMap(
'chat/payments/payment_response_alfa_bank.json',
);
final expectedPayment = PaymentModel.fromJson(paymentFixture);
when(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).thenAnswer((_) async => expectedPayment);
await paymentRepository.startPaymentProcess(planId: planId);
verify(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).called(1);
},
);
test('startPaymentProcess - пробрасывает ошибку сервиса', () async {
const planId = 123;
final error = Exception('Payment start failed');
when(
mockChatService.getPaymentLink(
id: planId,
body: {'payment_provider': 'alfa-bank'},
),
).thenThrow(error);
expect(
() => paymentRepository.startPaymentProcess(planId: planId),
throwsA(isA<Exception>()),
);
});
});
}

View File

@@ -0,0 +1,118 @@
import 'package:eatvio_app/features/payment/repositories/sbp_repository.dart';
import 'package:eatvio_app/features/payment/services/sbp_service.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:sbp/models/c2bmembers_model.dart';
import 'sbp_repository_test.mocks.dart';
@GenerateNiceMocks([MockSpec<SbpService>()])
void main() {
group('SbpRepository', () {
late SbpRepository sbpRepository;
late MockSbpService mockSbpService;
final installedBank = C2bmemberModel.fromJson(const {
'bankName': 'Сбербанк',
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
'schema': 'bank100000000111',
'package_name': 'ru.sberbankmobile',
'icon': 'packages/sbp/assets/icons/sber.png',
});
setUp(() {
mockSbpService = MockSbpService();
sbpRepository = SbpRepository(sbpService: mockSbpService);
});
group('getInstalledBanks', () {
test(
'делегирует сервису и возвращает список установленных банков',
() async {
when(
mockSbpService.getInstalledBanks(),
).thenAnswer((_) async => [installedBank]);
final result = await sbpRepository.getInstalledBanks();
expect(result, [installedBank]);
verify(mockSbpService.getInstalledBanks()).called(1);
},
);
test(
'возвращает пустой список, когда сервис вернул пустой список',
() async {
when(mockSbpService.getInstalledBanks()).thenAnswer((_) async => []);
final result = await sbpRepository.getInstalledBanks();
expect(result, isEmpty);
},
);
test(
'бросает exception при ошибке нативного плагина (graceful)',
() async {
when(
mockSbpService.getInstalledBanks(),
).thenThrow(MissingPluginException('no sbp handler'));
expect(() => sbpRepository.getInstalledBanks(), throwsException);
},
);
});
group('openBank', () {
const checkoutUrl = 'https://qr.nspk.ru/ASDFASDFASDFASDF';
test(
'делегирует сервису и возвращает true при успешном запуске',
() async {
when(
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
).thenAnswer((_) async => true);
final result = await sbpRepository.openBank(
url: checkoutUrl,
bank: installedBank,
);
expect(result, isTrue);
verify(
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
).called(1);
},
);
test('возвращает false, когда сервис вернул false', () async {
when(
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
).thenAnswer((_) async => false);
final result = await sbpRepository.openBank(
url: checkoutUrl,
bank: installedBank,
);
expect(result, isFalse);
});
test(
'возвращает false при ошибке нативного плагина (graceful)',
() async {
when(
mockSbpService.openBank(url: checkoutUrl, bank: installedBank),
).thenThrow(MissingPluginException('no sbp handler'));
expect(
() => sbpRepository.openBank(url: checkoutUrl, bank: installedBank),
throwsException,
);
},
);
});
});
}

View File

@@ -0,0 +1,119 @@
import 'package:eatvio_app/features/payment/services/sbp_service.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sbp/models/c2bmembers_model.dart';
/// Тесты реализации [SbpServiceImpl] — обёртки над статичным классом [Sbp].
///
/// Поскольку unit-тесты исполняются на хост-машине (не iOS), срабатывает
/// Android-ветка плагина: `getInstalledBanks` возвращает список банков через
/// `package_name`, а `openBank` подставляет схему банка в начало checkout_url.
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
const channel = MethodChannel('sbp');
/// Банк-фикстура: Сбербанк (присутствует в справочнике c2bmembersData пакета).
final sberbank = C2bmemberModel.fromJson(const {
'bankName': 'Сбербанк',
'logoURL': 'https://qr.nspk.ru/proxyapp/logo/bank100000000111.png',
'schema': 'bank100000000111',
'package_name': 'ru.sberbankmobile',
'icon': 'packages/sbp/assets/icons/sber.png',
});
late SbpService sbpService;
setUp(() {
sbpService = const SbpServiceImpl();
});
tearDown(() {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, null);
});
group('SbpServiceImpl.getInstalledBanks', () {
test('возвращает список банков, установленных на устройстве (Android)', () async {
// Плагин (Android) возвращает список установленных банков в формате
// {name, bitmap, package_name}; обёртка сопоставляет со справочником.
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall call) async {
return [
{'name': 'Сбербанк', 'bitmap': null, 'package_name': 'ru.sberbankmobile'},
];
});
final result = await sbpService.getInstalledBanks();
expect(result, isNotEmpty);
expect(
result.any(
(bank) =>
bank.bankName == 'Сбербанк' && bank.packageName == 'ru.sberbankmobile',
),
isTrue,
);
});
test('возвращает пустой список, если нет установленных банков', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall call) async {
return <Map<String, dynamic>>[];
});
final result = await sbpService.getInstalledBanks();
expect(result, isEmpty);
});
test('пробрасывает ошибку плагина при отсутствии нативного обработчика', () async {
// Обработчик не зарегистрирован → invokeMethod выбрасывает
// MissingPluginException. Сервис должен пробросить её наверх.
expect(
sbpService.getInstalledBanks(),
throwsA(isA<MissingPluginException>()),
);
});
});
group('SbpServiceImpl.openBank', () {
test('вызывает нативный openBank с подстановкой схемы банка в url и возвращает true', () async {
const checkoutUrl = 'https://qr.nspk.ru/ASDFASDFASDFASDF';
String? receivedUrl;
String? receivedPackageName;
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall call) async {
if (call.method == 'openBank') {
final args = call.arguments as Map<dynamic, dynamic>;
receivedUrl = args['url'] as String?;
receivedPackageName = args['package_name'] as String?;
return true;
}
return null;
});
final result = await sbpService.openBank(url: checkoutUrl, bank: sberbank);
expect(result, isTrue);
// Android-ветка заменяет схему "https" на schema банка в начале url.
expect(receivedUrl, 'bank100000000111://qr.nspk.ru/ASDFASDFASDFASDF');
expect(receivedPackageName, 'ru.sberbankmobile');
});
test('возвращает false при неудачном запуске банковского приложения', () async {
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
.setMockMethodCallHandler(channel, (MethodCall call) async {
return false;
});
final result = await sbpService.openBank(
url: 'https://qr.nspk.ru/ASDFASDFASDFASDF',
bank: sberbank,
);
expect(result, isFalse);
});
});
}

View File

@@ -4,7 +4,7 @@ import 'package:flutter_test/flutter_test.dart';
void main() {
group('ChosenPlanStateEnum', () {
test('has all expected values', () {
expect(ChosenPlanStateEnum.values.length, 15);
expect(ChosenPlanStateEnum.values.length, 16);
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInited));
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateStartDateHasBeenSet));
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateInCoordination));
@@ -20,6 +20,7 @@ void main() {
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateDone));
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.personalDietSet));
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.stateChargeWasCollected));
expect(ChosenPlanStateEnum.values, contains(ChosenPlanStateEnum.statePaymentFailed));
});
test('getShowableChatBadgeStates returns correct states', () {
@@ -35,6 +36,7 @@ void main() {
expect(states, contains(ChosenPlanStateEnum.stateDone));
expect(states, contains(ChosenPlanStateEnum.stateInProgress));
expect(states, contains(ChosenPlanStateEnum.personalDietSet));
expect(states, isNot(contains(ChosenPlanStateEnum.statePaymentFailed)));
});
test('isStateAfterPaidState returns false for states before paid', () {
@@ -43,6 +45,7 @@ void main() {
expect(ChosenPlanStateEnum.stateInCoordination.isStateAfterPaidState(), isFalse);
expect(ChosenPlanStateEnum.stateStartDateHasBeenSet.isStateAfterPaidState(), isFalse);
expect(ChosenPlanStateEnum.statePendingPayment.isStateAfterPaidState(), isFalse);
expect(ChosenPlanStateEnum.statePaymentFailed.isStateAfterPaidState(), isFalse);
});
test('isStateAfterPaidState returns true for states after paid', () {
@@ -70,6 +73,7 @@ void main() {
expect(activeStates, contains(ChosenPlanStateEnum.stateComplained));
expect(activeStates, contains(ChosenPlanStateEnum.stateDone));
expect(activeStates, contains(ChosenPlanStateEnum.personalDietSet));
expect(activeStates, isNot(contains(ChosenPlanStateEnum.statePaymentFailed)));
});
});
}

View File

@@ -4,6 +4,8 @@ import 'package:eatvio_app/features/plans/enums/chosen_plan_state_enum.dart';
import 'package:eatvio_app/features/plans/models/chosen_plan_model.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../../fixtures/fixture_reader.dart';
void main() {
group('ChosenPlanModel', () {
test('создает ChosenPlanModel с правильными значениями', () {
@@ -140,6 +142,60 @@ void main() {
expect(json['consultant_id'], 1);
expect(json.containsKey('plan'), isTrue);
});
group('fromJson payment polling statuses', () {
test('fromJson парсит pending_payment из фикстуры', () {
final json = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_pending_payment_response.json',
);
final model = ChosenPlanModel.fromJson(json);
expect(model.id, 1);
expect(model.optionType, '1');
expect(model.chosenPlanState, ChosenPlanStateEnum.statePendingPayment);
expect(model.chatId, 'chat-1');
});
test('fromJson парсит paid из фикстуры', () {
final json = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_paid_response.json',
);
final model = ChosenPlanModel.fromJson(json);
expect(model.id, 1);
expect(model.optionType, '1');
expect(model.chosenPlanState, ChosenPlanStateEnum.statePaid);
expect(model.chatId, 'chat-1');
});
test('fromJson парсит payment_failed из фикстуры', () {
final json = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_payment_failed_response.json',
);
final model = ChosenPlanModel.fromJson(json);
expect(model.id, 1);
expect(model.optionType, '1');
expect(model.chosenPlanState, ChosenPlanStateEnum.statePaymentFailed);
expect(model.chatId, 'chat-1');
});
test('fromJson парсит canceled_by_the_initiator из фикстуры', () {
final json = FixtureReader.readAsMap(
'chat/chosen_plan/chosen_plan_canceled_by_the_initiator_response.json',
);
final model = ChosenPlanModel.fromJson(json);
expect(model.id, 1);
expect(model.optionType, '1');
expect(model.chosenPlanState, ChosenPlanStateEnum.stateCanceledByTheInitiator);
expect(model.chatId, 'chat-1');
});
});
});
group('parseChosenPlan', () {

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "canceled_by_the_initiator",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "charge_was_collected",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "complained",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "confirmed_by_consultant",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "done",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "in_progress",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "initied",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "last_day_finished",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "last_day_started",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "paid",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "payment_failed",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "pending_payment",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "personal_diet_setted",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "start_date_has_been_set",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,47 @@
{
"id": 1,
"option_type": "1",
"start_date": "2024-01-01T00:00:00Z",
"last_state": "started",
"chat_id": "chat-1",
"consultant_id": 1,
"plan": {
"id": 1,
"slug": "test-plan",
"name": "Test Plan",
"image": "https://example.com/plan.jpg",
"duration": 30,
"state": "active",
"type": {
"fit": true,
"slim": false,
"behavior": false,
"wellness": false
},
"description_md": "Plan description",
"packets_count": 1,
"packet1_options": {
"items": ["Item 1", "Item 2"],
"price": 1500.0,
"short": "Option 1",
"description_md": "Description",
"has_personal_diet": false
},
"packet2_options": null,
"packet3_options": null,
"author": {
"author_slug": "author-1",
"author_name": "Author Name",
"author_profile_icon": "https://example.com/author.jpg",
"author_is_me": false
},
"total_likes": 10,
"total_views": 100,
"total_comments": 5,
"is_favorite": false,
"min_price_of_options": 1000.0,
"max_price_of_options": 2000.0,
"was_liked_by_me": false
}
}

View File

@@ -0,0 +1,5 @@
{
"chosen_plan_id": 12,
"conversation_uuid": "fbd81429-8019-499a-80e8-e3933cc4d872"
}

View File

@@ -0,0 +1,3 @@
{
"payment_provider": "alfa-bank"
}

View File

@@ -0,0 +1,3 @@
{
"payment_provider": "mock"
}

View File

@@ -0,0 +1,5 @@
{
"checkout_url": "https://qr.nspk.ru/ASDFASDFASDFASDF",
"payment_provider": "alfa-bank",
"order_id": "alfa_c29_k15138_t1712345678_r1234"
}