refactor(chat): message dedup

This commit is contained in:
Anatoliy
2026-09-15 23:06:12 +03:00
parent c4f5cae36d
commit 077cfb3a67
3 changed files with 131 additions and 2 deletions

View File

@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'package:eatvio_app/core/enums/base_state.dart';
import 'package:eatvio_app/core/models/pagination_model.dart';
import 'package:eatvio_app/core/websockets/ws_manager.dart';
@@ -43,6 +44,39 @@ void main() {
}
});
ChatRoom buildChatRoom(String chatId, {int unread = 0}) {
return ChatRoom(
id: chatId,
lastMessage: null,
users: const Users(
fromChatUser: ChatUser(id: 1, name: 'User 1', profilePhotoPath: null, profilePhotoUrl: ''),
toChatUser: ChatUser(id: 2, name: 'User 2', profilePhotoPath: null, profilePhotoUrl: ''),
),
unreadMessagesCount: unread,
chosenPlan: null,
);
}
ChannelReadEvent socketEvent({required int id, required int authorId, required String roomId}) {
return _FakeChannelReadEvent(jsonEncode(<String, dynamic>{
'id': id,
'data': 'socket message',
'media_data': null,
'author_id': authorId,
'updated_at': DateTime.now().toIso8601String(),
'read_by_to_user': false,
'is_author_message_auth_user': false,
'room_id': roomId,
}));
}
Future<StreamController<ChannelReadEvent>> connectToSocketStream() async {
final controller = StreamController<ChannelReadEvent>.broadcast();
when(mockWsManager.newMessagesStream).thenAnswer((_) => controller.stream);
await chatsCubit.connectToWebSocket();
return controller;
}
test('начальное состояние - initial', () {
expect(chatsCubit.state, isA<ChatsState>());
expect(chatsCubit.state.state, BaseState.initial);
@@ -180,6 +214,76 @@ void main() {
verify(mockWsManager.newMessagesStream).called(1);
});
test('_handleNewMessageViaSocket - собственное сообщение обновляет lastMessage без непрочитанного', () async {
const chatId = 'chat-id';
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: [buildChatRoom(chatId)]));
when(mockWsManager.userId).thenReturn(42);
final controller = await connectToSocketStream();
controller.add(socketEvent(id: 100, authorId: 42, roomId: chatId));
await Future.delayed(const Duration(milliseconds: 100));
final room = chatsCubit.state.chatRooms.firstWhere((r) => r.id == chatId);
expect(room.lastMessage?.id, 100);
expect(room.unreadMessagesCount, 0);
expect(chatsCubit.state.chatsWithUnreadMessagesCount, 0);
await controller.close();
});
test('_handleNewMessageViaSocket - чужое сообщение увеличивает непрочитанное', () async {
const chatId = 'chat-id';
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: [buildChatRoom(chatId)]));
when(mockWsManager.userId).thenReturn(42);
final controller = await connectToSocketStream();
controller.add(socketEvent(id: 200, authorId: 7, roomId: chatId));
await Future.delayed(const Duration(milliseconds: 100));
final room = chatsCubit.state.chatRooms.firstWhere((r) => r.id == chatId);
expect(room.lastMessage?.id, 200);
expect(room.unreadMessagesCount, 1);
expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1);
await controller.close();
});
test('_handleNewMessageViaSocket - повторная доставка того же id не двоит непрочитанное', () async {
const chatId = 'chat-id';
chatsCubit.emit(ChatsState(state: BaseState.success, chatRooms: [buildChatRoom(chatId)]));
when(mockWsManager.userId).thenReturn(42);
final controller = await connectToSocketStream();
controller.add(socketEvent(id: 300, authorId: 7, roomId: chatId));
await Future.delayed(const Duration(milliseconds: 100));
controller.add(socketEvent(id: 300, authorId: 7, roomId: chatId));
await Future.delayed(const Duration(milliseconds: 100));
final room = chatsCubit.state.chatRooms.firstWhere((r) => r.id == chatId);
expect(room.unreadMessagesCount, 1);
expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1);
await controller.close();
});
test('_handleNewMessageViaSocket - чужое сообщение в непрочитанном чате не поднимает счётчик чатов', () async {
const chatId = 'chat-id';
chatsCubit.emit(
ChatsState(state: BaseState.success, chatRooms: [buildChatRoom(chatId, unread: 2)], chatsWithUnreadMessagesCount: 1),
);
when(mockWsManager.userId).thenReturn(42);
final controller = await connectToSocketStream();
controller.add(socketEvent(id: 400, authorId: 7, roomId: chatId));
await Future.delayed(const Duration(milliseconds: 100));
final room = chatsCubit.state.chatRooms.firstWhere((r) => r.id == chatId);
expect(room.unreadMessagesCount, 3);
expect(chatsCubit.state.chatsWithUnreadMessagesCount, 1);
await controller.close();
});
test('close - закрывает cubit и отписывается от подписок', () async {
final mockSubscription1 = MockStreamSubscription();
@@ -476,3 +580,12 @@ void main() {
});
});
}
/// Облегчённый фейк события канала: ChatsCubit использует только event.data,
/// поэтому реальный ChannelReadEvent (требует Channel) не нужен.
class _FakeChannelReadEvent extends Fake implements ChannelReadEvent {
_FakeChannelReadEvent(this.data);
@override
final dynamic data;
}