- 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
296 lines
11 KiB
Python
296 lines
11 KiB
Python
# 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()
|