Corp/backend/tests/realtime/test_user_consumer.py
igor_nadein a9f38afd55 test(realtime): add comprehensive WebSocket consumer tests
- Add tests/realtime/ module with test suite for UserConsumer
- test_user_consumer.py: connection, auth, ping, chat management, typing
- test_routing.py: WebSocket URL routing validation
- test_integration.py: broadcast, notifications, multi-user scenarios
- conftest.py: fixtures for WebSocket testing (ws_communicator, test_chat)

Tests cover:
- Authentication (anonymous rejection, authenticated access)
- Chat subscription and management (open/close, access control)
- Ping/pong keepalive mechanism
- Typing indicators
- Message actions (send, mark_read)
- Integration with channel layer for broadcasts
- Multi-user WebSocket scenarios
2026-01-21 18:04:40 +03:00

296 lines
11 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.testing import WebsocketCommunicator
from django.contrib.auth.models import AnonymousUser
from communications.models import Chat, ChatMembership
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 = Chat.objects.create(name="Second Chat", chat_type="group")
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 TestUserConsumerPing:
"""Тесты ping/pong механизма для keepalive."""
async def test_receives_ping_messages(self, ws_communicator, user):
"""Клиент получает ping сообщения для keepalive."""
communicator = await ws_communicator(user=user)
connected, _ = await communicator.connect()
assert connected
# Ждем ping сообщение (с таймаутом)
try:
response = await communicator.receive_json_from(timeout=25)
assert response["type"] == "ping"
assert "timestamp" in response
except TimeoutError:
pytest.skip("Ping не получен в течение таймаута")
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
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 = Chat.objects.create(name="Private Chat", chat_type="private")
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
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()