Corp/backend/tests/realtime/test_user_consumer.py

463 lines
18 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_user_consumer.py
"""
Тесты для UserConsumer - универсального WebSocket consumer'а.
Проверяем:
- Подключение/отключение
- Аутентификацию
- Подписку на чаты и каналы
- Ping/pong keepalive
- Открытие/закрытие активного чата
"""
import pytest
from channels.layers import get_channel_layer
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from communications.models import ActiveChatSession, Chat, ChatMembership
from commerce.realtime import COMMERCE_CONNECTIONS_GROUP
from notifications.models import Notification
pytestmark = pytest.mark.django_db(transaction=True)
@pytest.mark.asyncio
class TestUserConsumerConnection:
"""Тесты подключения и отключения WebSocket."""
async def test_anonymous_user_rejected(self, ws_communicator):
"""Анонимный пользователь не может подключиться."""
communicator = await ws_communicator(user=AnonymousUser())
connected, subprotocol = await communicator.connect()
assert not connected, "Анонимный пользователь не должен подключиться"
await communicator.disconnect()
async def test_authenticated_user_connects(self, ws_communicator, user):
"""Аутентифицированный пользователь успешно подключается."""
communicator = await ws_communicator(user=user)
connected, subprotocol = await communicator.connect()
assert connected, "Аутентифицированный пользователь должен подключиться"
await communicator.disconnect()
async def test_user_subscribes_to_chats(self, ws_communicator, user, test_chat):
"""При подключении пользователь подписывается на свои чаты."""
# Создаем дополнительный чат
chat2 = await database_sync_to_async(Chat.objects.create)(
name="Second Chat", type="group"
)
await database_sync_to_async(ChatMembership.objects.create)(
chat=chat2, user=user, role="member"
)
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Проверяем, что пользователь подписан на группы чатов
# (это можно проверить через логи или mock channel_layer)
await communicator.disconnect()
async def test_disconnect_cleans_up(self, ws_communicator, user, test_chat):
"""При отключении происходит отписка от всех групп."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Отключаемся
await communicator.disconnect()
# После отключения не должно быть ошибок
@pytest.mark.asyncio
class TestUserConsumerTaskBoardEvents:
"""Тесты realtime-событий доски задач."""
async def test_task_board_update_event(self, ws_communicator, user):
"""Пользователь получает событие обновления доски через личный канал."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
channel_layer = get_channel_layer()
await channel_layer.group_send(
f"user_{user.id}",
{
"type": "task_board_update",
"event": "moved",
"data": {
"board_id": 15,
"model": "task",
"object_id": 42,
"column_id": 7,
},
},
)
response = await communicator.receive_json_from(timeout=5)
assert response["type"] == "task_board_update"
assert response["event"] == "moved"
assert response["data"]["board_id"] == 15
assert response["data"]["model"] == "task"
assert response["data"]["object_id"] == 42
assert response["data"]["column_id"] == 7
await communicator.disconnect()
@pytest.mark.asyncio
async def test_user_receives_commerce_connection_update(ws_communicator, user):
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
await get_channel_layer().group_send(
COMMERCE_CONNECTIONS_GROUP,
{
"type": "commerce_connection_updated",
"connection": {"id": "connection-1", "status": "ready"},
},
)
response = await communicator.receive_json_from(timeout=5)
assert response == {
"type": "commerce_connection_updated",
"connection": {"id": "connection-1", "status": "ready"},
}
await communicator.disconnect()
@pytest.mark.asyncio
class TestUserConsumerChatManagement:
"""Тесты управления активным чатом."""
async def test_open_chat(self, ws_communicator, user, test_chat):
"""Открытие чата устанавливает active_chat_id."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Отправляем команду открытия чата
await communicator.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id,
"load_history": False
})
# Ожидаем подтверждение
response = await communicator.receive_json_from(timeout=5)
assert response["type"] == "chat_opened"
assert response["chat_id"] == test_chat.id
presence_exists = await database_sync_to_async(
ActiveChatSession.objects.filter(
user=user,
chat=test_chat,
).exists
)()
assert presence_exists is True
await communicator.disconnect()
presence_exists = await database_sync_to_async(
ActiveChatSession.objects.filter(
user=user,
chat=test_chat,
).exists
)()
assert presence_exists is False
async def test_open_chat_marks_related_notifications_as_read(self, ws_communicator, user, test_chat):
"""Открытие конкретного чата читает только связанные с ним уведомления."""
related_notification = await database_sync_to_async(Notification.objects.create)(
recipient=user,
verb='chat_new_message',
description='Chat notification',
action_url=f'/messages/{test_chat.id}',
data={'chat_id': test_chat.id, 'message_id': 10},
)
unrelated_notification = await database_sync_to_async(Notification.objects.create)(
recipient=user,
verb='chat_new_message',
description='Other chat notification',
action_url='/messages/999',
data={'chat_id': 999, 'message_id': 11},
)
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({
'action': 'open_chat',
'chat_id': test_chat.id,
'load_history': False,
})
response = await communicator.receive_json_from(timeout=5)
assert response['type'] == 'chat_opened'
assert response['chat_id'] == test_chat.id
read_all_event = await communicator.receive_json_from(timeout=5)
assert read_all_event['type'] == 'notifications_read_all'
assert related_notification.id in read_all_event['notification_ids']
assert unrelated_notification.id not in read_all_event['notification_ids']
count_event = await communicator.receive_json_from(timeout=5)
assert count_event['type'] == 'unread_count'
assert count_event['count'] == 1
related_notification = await database_sync_to_async(Notification.objects.get)(id=related_notification.id)
unrelated_notification = await database_sync_to_async(Notification.objects.get)(id=unrelated_notification.id)
assert related_notification.unread is False
assert unrelated_notification.unread is True
await communicator.disconnect()
async def test_open_chat_marks_related_commented_notifications_as_read(self, ws_communicator, user, test_chat):
"""Открытие comments-чата должно читать уведомления verb=commented этого чата."""
related_notification = await database_sync_to_async(Notification.objects.create)(
recipient=user,
verb='commented',
description='Department chat comment notification',
action_url=f'/messages/{test_chat.id}',
data={'chat_id': test_chat.id, 'message_id': 12, 'object_type': 'Department'},
)
unrelated_notification = await database_sync_to_async(Notification.objects.create)(
recipient=user,
verb='commented',
description='Other comments chat notification',
action_url='/messages/999',
data={'chat_id': 999, 'message_id': 13, 'object_type': 'Department'},
)
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
await communicator.send_json_to({
'action': 'open_chat',
'chat_id': test_chat.id,
'load_history': False,
})
response = await communicator.receive_json_from(timeout=5)
assert response['type'] == 'chat_opened'
assert response['chat_id'] == test_chat.id
read_all_event = await communicator.receive_json_from(timeout=5)
assert read_all_event['type'] == 'notifications_read_all'
assert related_notification.id in read_all_event['notification_ids']
assert unrelated_notification.id not in read_all_event['notification_ids']
count_event = await communicator.receive_json_from(timeout=5)
assert count_event['type'] == 'unread_count'
assert count_event['count'] == 1
related_notification = await database_sync_to_async(Notification.objects.get)(id=related_notification.id)
unrelated_notification = await database_sync_to_async(Notification.objects.get)(id=unrelated_notification.id)
assert related_notification.unread is False
assert unrelated_notification.unread is True
await communicator.disconnect()
async def test_open_nonexistent_chat(self, ws_communicator, user):
"""Попытка открыть несуществующий чат возвращает ошибку."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Пытаемся открыть несуществующий чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": 99999
})
# Ожидаем ошибку
response = await communicator.receive_json_from(timeout=5)
assert response["type"] == "error"
assert "not found" in response["error"].lower()
await communicator.disconnect()
async def test_open_chat_without_access(self, ws_communicator, user, user2):
"""Попытка открыть чат без доступа возвращает ошибку."""
# Создаем чат только для user2
chat = await database_sync_to_async(Chat.objects.create)(
name="Private Chat", type="private"
)
await database_sync_to_async(ChatMembership.objects.create)(
chat=chat, user=user2, role="member"
)
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Пытаемся открыть чужой чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": chat.id
})
# Ожидаем ошибку
response = await communicator.receive_json_from(timeout=5)
assert response["type"] == "error"
assert "access denied" in response["error"].lower()
await communicator.disconnect()
async def test_close_chat(self, ws_communicator, user, test_chat):
"""Закрытие чата сбрасывает active_chat_id."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Открываем чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id
})
await communicator.receive_json_from(timeout=5)
# Закрываем чат
await communicator.send_json_to({
"action": "close_chat",
"chat_id": test_chat.id
})
# Ожидаем подтверждение
response = await communicator.receive_json_from(timeout=5)
assert response["type"] == "chat_closed"
assert response["chat_id"] == test_chat.id
presence_exists = await database_sync_to_async(
ActiveChatSession.objects.filter(
user=user,
chat=test_chat,
).exists
)()
assert presence_exists is False
await communicator.disconnect()
@pytest.mark.asyncio
class TestUserConsumerTypingIndicator:
"""Тесты индикатора печати."""
async def test_typing_indicator(self, ws_communicator, user, test_chat):
"""Отправка события 'typing' для активного чата."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Открываем чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id
})
await communicator.receive_json_from(timeout=5)
# Отправляем typing
await communicator.send_json_to({
"action": "typing",
"chat_id": test_chat.id
})
# Можем проверить через channel layer что событие отправлено
# (требует mock или интеграционный тест)
await communicator.disconnect()
async def test_stop_typing_on_disconnect(self, ws_communicator, user, test_chat):
"""При отключении статус 'typing' сбрасывается."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Открываем чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id
})
await communicator.receive_json_from(timeout=5)
# Устанавливаем typing
await communicator.send_json_to({
"action": "typing",
"chat_id": test_chat.id
})
# Отключаемся (должно сбросить typing)
await communicator.disconnect()
@pytest.mark.asyncio
class TestUserConsumerMessageActions:
"""Тесты действий с сообщениями через WebSocket."""
async def test_send_message_requires_open_chat(self, ws_communicator, user, test_chat):
"""Отправка сообщения требует открытого чата."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Пытаемся отправить сообщение без открытого чата
await communicator.send_json_to({
"action": "send_message",
"content": "Test message"
})
# Должна быть ошибка или сообщение игнорируется
# (зависит от реализации)
await communicator.disconnect()
async def test_mark_read(self, ws_communicator, user, test_chat):
"""Отметка сообщений как прочитанных."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Открываем чат
await communicator.send_json_to({
"action": "open_chat",
"chat_id": test_chat.id
})
await communicator.receive_json_from(timeout=5)
# Отправляем mark_read
await communicator.send_json_to({
"action": "mark_read",
"chat_id": test_chat.id
})
# Проверяем, что ChatReadState обновлен
# (требует дополнительной проверки в БД)
await communicator.disconnect()