# 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()