Corp/backend/tests/realtime/test_websocket_events.py
igor_nadein da7bf54c6c feat: Реализация полной функциональности опросов в чатах
- Исправлен PollSerializer для создания опросов с опциями
- Добавлено поле read_only_fields для корректной валидации
- Реализована отправка WebSocket уведомлений при создании опроса
- Исправлен импорт serialize_message из communications.serialization
- Добавлен обработчик poll_update в UserConsumer для WebSocket событий
- Исправлен эндпоинт /results/ для возврата user_voted_option_ids
- Добавлено поле voters в PollOptionSerializer для списка проголосовавших
- Реализована инициализация poll виджетов после рендера сообщений
- Добавлено игнорирование poll_update в notification-manager
- Исправлены стили для request list badges (height, word-break, flex)
- Восстановлены стили team-wheel (.wheel-focus, .scroll-arc) из git истории
- Реализовано отображение forwarded message info в messageRendererV2
- Множественные улучшения UI и UX для опросов

Fixes: создание опросов, голосование, отображение результатов, список проголосовавших
2026-01-22 01:33:42 +03:00

547 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# tests/realtime/test_websocket_events.py
"""
Тесты для WebSocket событий из API.
Проверяем что Consumer правильно обрабатывает события от channel_layer:
- Создание сообщения
- Редактирование сообщения
- Удаление сообщения
- Добавление/удаление реакций
- Голосование в опросе
- Отметка прочтения
Тесты не создают реальные Django объекты, а проверяют только
правильность обработки событий Consumer'ом.
"""
import pytest
from channels.layers import get_channel_layer
pytestmark = pytest.mark.django_db(transaction=True)
class TestMessageWebSocketEvents:
"""Тесты WebSocket событий для сообщений."""
@pytest.mark.asyncio
async def test_new_message_broadcast(self, ws_communicator, user, user2, test_chat_with_users):
"""При создании нового сообщения оба участника получают событие."""
# Подключаем обоих пользователей
comm1 = await ws_communicator(user=user)
comm2 = await ws_communicator(user=user2)
connected1, _ = await comm1.connect()
connected2, _ = await comm2.connect()
assert connected1 and connected2
# User1 открывает чат
await comm1.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
# User2 тоже открывает чат
await comm2.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
# Подтверждения открытия
await comm1.receive_json_from(timeout=5)
await comm2.receive_json_from(timeout=5)
# Отправляем событие через channel layer (имитируем API)
channel_layer = get_channel_layer()
mock_payload = {
'id': 999,
'content': 'Test message',
'author_id': user.id,
'author_name': user.get_full_name(),
'chat_id': test_chat_with_users.id,
'created_at': '2026-01-21T10:00:00Z'
}
await channel_layer.group_send(
f'chat_{test_chat_with_users.id}',
{
'type': 'chat.message',
'chat_id': test_chat_with_users.id,
'payload': mock_payload
}
)
# User1 должен получить new_message (активный чат)
response1 = await comm1.receive_json_from(timeout=5)
assert response1["type"] == "new_message"
assert response1["chat_id"] == test_chat_with_users.id
assert response1["message"]["content"] == "Test message"
# И list_update для списка чатов
response1_list = await comm1.receive_json_from(timeout=5)
assert response1_list["type"] == "list_update"
# User2 тоже должен получить оба события
response2 = await comm2.receive_json_from(timeout=5)
assert response2["type"] == "new_message"
response2_list = await comm2.receive_json_from(timeout=5)
assert response2_list["type"] == "list_update"
await comm1.disconnect()
await comm2.disconnect()
@pytest.mark.asyncio
async def test_new_message_list_update_only(self, ws_communicator, user, user2, test_chat_with_users):
"""Если чат неактивен, пользователь получает только list_update."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# НЕ открываем чат (чтобы он был неактивным)
# Отправляем событие (имитируем API)
channel_layer = get_channel_layer()
mock_payload = {
'id': 998,
'content': 'Message when chat is inactive',
'author_id': user2.id,
'author_name': user2.get_full_name(),
'chat_id': test_chat_with_users.id
}
await channel_layer.group_send(
f'chat_{test_chat_with_users.id}',
{
'type': 'chat.message',
'chat_id': test_chat_with_users.id,
'payload': mock_payload
}
)
# Когда чат неактивен, Consumer отправляет только list_update
response = await comm.receive_json_from(timeout=5)
assert response["type"] == "list_update"
assert response["chat_id"] == test_chat_with_users.id
@pytest.mark.asyncio
async def test_message_edited_broadcast(self, ws_communicator, user, test_chat):
"""При редактировании сообщения участники получают событие."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# Открываем чат
await comm.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id,
"load_history": False
})
await comm.receive_json_from(timeout=5) # chat_opened
# Отправляем событие редактирования (имитируем API)
channel_layer = get_channel_layer()
mock_payload = {
'id': 997,
'content': 'Edited content',
'is_edited': True,
'author_id': user.id,
'chat_id': test_chat.id
}
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.message_edited',
'chat_id': test_chat.id,
'payload': mock_payload
}
)
# Должны получить message_updated для активного чата
response1 = await comm.receive_json_from(timeout=5)
assert response1["type"] == "message_updated"
assert response1["message"]["content"] == "Edited content"
assert response1["message"]["is_edited"] is True
# И message_edited для списка
response2 = await comm.receive_json_from(timeout=5)
assert response2["type"] == "message_edited"
await comm.disconnect()
@pytest.mark.asyncio
async def test_message_deleted_broadcast(self, ws_communicator, user, test_chat):
"""При удалении сообщения участники получают событие."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# Открываем чат
await comm.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id,
"load_history": False
})
await comm.receive_json_from(timeout=5) # chat_opened
# Отправляем событие удаления (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 996
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.message_deleted',
'chat_id': test_chat.id,
'message_id': mock_message_id
}
)
# Должны получить message_deleted
response = await comm.receive_json_from(timeout=5)
assert response["type"] == "message_deleted"
assert response["chat_id"] == test_chat.id
assert response["message_id"] == mock_message_id
await comm.disconnect()
@pytest.mark.asyncio
async def test_message_deleted_inactive_chat(self, ws_communicator, user, test_chat):
"""Если чат неактивен, событие удаления НЕ отправляется клиенту."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# НЕ открываем чат
# Отправляем событие удаления (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 995
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.message_deleted',
'chat_id': test_chat.id,
'message_id': mock_message_id
}
)
# НЕ должны получить событие (чат неактивен)
# Проверяем что не получим ничего в течение короткого таймаута
import asyncio
with pytest.raises(asyncio.TimeoutError):
await comm.receive_json_from(timeout=2)
class TestReactionWebSocketEvents:
"""Тесты WebSocket событий для реакций."""
@pytest.mark.asyncio
async def test_reaction_added_broadcast(self, ws_communicator, user, user2, test_chat_with_users):
"""При добавлении реакции участники получают событие."""
# Подключаем обоих пользователей
comm1 = await ws_communicator(user=user)
comm2 = await ws_communicator(user=user2)
connected1, _ = await comm1.connect()
connected2, _ = await comm2.connect()
assert connected1 and connected2
# Оба открывают чат
await comm1.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
await comm2.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
await comm1.receive_json_from(timeout=5) # chat_opened
await comm2.receive_json_from(timeout=5) # chat_opened
# Отправляем событие реакции (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 994
reactions_summary = {
'👍': {
'count': 1,
'users': [user2.id],
'user_names': [user2.get_full_name()]
}
}
await channel_layer.group_send(
f'chat_{test_chat_with_users.id}',
{
'type': 'chat.reaction_added',
'chat_id': test_chat_with_users.id,
'message_id': mock_message_id,
'user_id': user2.id,
'emoji': '👍',
'reactions_summary': reactions_summary
}
)
# User1 должен получить reaction_added
response1 = await comm1.receive_json_from(timeout=5)
assert response1["type"] == "reaction_added"
assert response1["message_id"] == mock_message_id
assert response1["emoji"] == '👍'
assert response1["user_id"] == user2.id
assert '👍' in response1["reactions_summary"]
# User2 тоже должен получить
response2 = await comm2.receive_json_from(timeout=5)
assert response2["type"] == "reaction_added"
assert response2["emoji"] == '👍'
await comm1.disconnect()
await comm2.disconnect()
@pytest.mark.asyncio
async def test_reaction_removed_broadcast(self, ws_communicator, user, test_chat):
"""При удалении реакции участники получают событие."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# Открываем чат
await comm.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id,
"load_history": False
})
await comm.receive_json_from(timeout=5) # chat_opened
# Отправляем событие удаления реакции (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 993
reactions_summary = {} # Пустой - все реакции удалены
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.reaction_removed',
'chat_id': test_chat.id,
'message_id': mock_message_id,
'user_id': user.id,
'emoji': '👍',
'reactions_summary': reactions_summary
}
)
# Должны получить reaction_removed
response = await comm.receive_json_from(timeout=5)
assert response["type"] == "reaction_removed"
assert response["message_id"] == mock_message_id
assert response["emoji"] == '👍'
assert response["reactions_summary"] == {}
await comm.disconnect()
@pytest.mark.asyncio
async def test_reaction_inactive_chat_no_event(self, ws_communicator, user, test_chat):
"""Если чат неактивен, событие реакции НЕ отправляется."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# НЕ открываем чат
# Отправляем событие реакции (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 992
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.reaction_added',
'chat_id': test_chat.id,
'message_id': mock_message_id,
'user_id': user.id,
'emoji': '👍',
'reactions_summary': {}
}
)
# НЕ должны получить событие
import asyncio
with pytest.raises(asyncio.TimeoutError):
await comm.receive_json_from(timeout=2)
class TestPollWebSocketEvents:
"""Тесты WebSocket событий для голосований."""
@pytest.mark.asyncio
async def test_poll_vote_broadcast(self, ws_communicator, user, user2, test_chat_with_users):
"""При голосовании участники получают обновление."""
comm1 = await ws_communicator(user=user)
comm2 = await ws_communicator(user=user2)
connected1, _ = await comm1.connect()
connected2, _ = await comm2.connect()
assert connected1 and connected2
# Оба открывают чат
await comm1.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
await comm2.send_json_to({
"action": "open_chat",
"chat_id": test_chat_with_users.id,
"load_history": False
})
await comm1.receive_json_from(timeout=5)
await comm2.receive_json_from(timeout=5)
# Отправляем событие обновления опроса (имитируем API)
channel_layer = get_channel_layer()
mock_poll_id = 991
mock_message_id = 990
await channel_layer.group_send(
f'chat_{test_chat_with_users.id}',
{
'type': 'chat.poll_update',
'chat_id': test_chat_with_users.id,
'payload': {
'poll_id': mock_poll_id,
'message_id': mock_message_id,
'total_voters': 1,
'results': {
'options': [
{'id': 1, 'vote_count': 1}
]
}
}
}
)
# User1 должен получить poll_update
response1 = await comm1.receive_json_from(timeout=5)
assert response1["type"] == "poll_update"
assert response1["poll_id"] == mock_poll_id
assert response1["message_id"] == mock_message_id
# User2 тоже
response2 = await comm2.receive_json_from(timeout=5)
assert response2["type"] == "poll_update"
await comm1.disconnect()
await comm2.disconnect()
@pytest.mark.asyncio
async def test_poll_update_inactive_chat(self, ws_communicator, user, test_chat):
"""Если чат неактивен, событие опроса НЕ отправляется."""
comm = await ws_communicator(user=user)
connected, _ = await comm.connect()
assert connected
# НЕ открываем чат
# Отправляем событие (имитируем API)
channel_layer = get_channel_layer()
mock_poll_id = 989
await channel_layer.group_send(
f'chat_{test_chat.id}',
{
'type': 'chat.poll_update',
'chat_id': test_chat.id,
'payload': {
'poll_id': mock_poll_id
}
}
)
# НЕ должны получить
import asyncio
with pytest.raises(asyncio.TimeoutError):
await comm.receive_json_from(timeout=2)
class TestChatMarkedReadEvent:
"""Тесты WebSocket события отметки прочтения."""
@pytest.mark.asyncio
async def test_marked_read_sync_between_tabs(self, ws_communicator, user, test_chat):
"""Отметка прочтения синхронизируется между вкладками пользователя."""
# Имитируем две вкладки одного пользователя
comm1 = await ws_communicator(user=user)
comm2 = await ws_communicator(user=user)
connected1, _ = await comm1.connect()
connected2, _ = await comm2.connect()
assert connected1 and connected2
# Отправляем событие marked_read в канал пользователя (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 988
await channel_layer.group_send(
f'user_{user.id}',
{
'type': 'chat_marked_read',
'chat_id': test_chat.id,
'last_read_at': '2026-01-21T10:00:00Z',
'last_read_message_id': mock_message_id
}
)
# Обе вкладки должны получить событие
response1 = await comm1.receive_json_from(timeout=5)
assert response1["type"] == "marked_read"
assert response1["chat_id"] == test_chat.id
assert response1["last_read_message_id"] == mock_message_id
response2 = await comm2.receive_json_from(timeout=5)
assert response2["type"] == "marked_read"
assert response2["chat_id"] == test_chat.id
await comm1.disconnect()
await comm2.disconnect()
@pytest.mark.asyncio
async def test_marked_read_not_received_by_other_users(self, ws_communicator, user, user2, test_chat_with_users):
"""Событие marked_read получает только тот пользователь, который отметил."""
comm1 = await ws_communicator(user=user)
comm2 = await ws_communicator(user=user2)
connected1, _ = await comm1.connect()
connected2, _ = await comm2.connect()
assert connected1 and connected2
# User1 отмечает как прочитанное (имитируем API)
channel_layer = get_channel_layer()
mock_message_id = 987
await channel_layer.group_send(
f'user_{user.id}', # ТОЛЬКО user1
{
'type': 'chat_marked_read',
'chat_id': test_chat_with_users.id,
'last_read_at': '2026-01-21T10:00:00Z',
'last_read_message_id': mock_message_id
}
)
# User1 должен получить
response1 = await comm1.receive_json_from(timeout=5)
assert response1["type"] == "marked_read"
# User2 НЕ должен получить
import asyncio
with pytest.raises(asyncio.TimeoutError):
await comm2.receive_json_from(timeout=2)