Files

235 lines
8.8 KiB
Python

import asyncio
import json
import smtplib
from unittest.mock import AsyncMock, MagicMock
import allure
import pytest
import src.messaging.rabbitmq_client as rabbitmq_client_module
from src.messaging.consumers.consumers import ResetEmailConsumer, WelcomeEmailConsumer
from src.messaging.rabbitmq_client import RabbitMQClient
@pytest.mark.unit
class TestRabbitMQClient:
@pytest.mark.parametrize("",[
pytest.param(id="Connect_retries_then_succeeds")
])
async def test_connect_retries_then_succeeds(self, monkeypatch):
with allure.step("Mocking connection to the rabbitmq"):
fake_connection = MagicMock()
fake_connection.is_closed = False
fake_connection.channel = AsyncMock(return_value=MagicMock())
mock_connect = AsyncMock(
side_effect=[ConnectionError(), ConnectionError(), fake_connection]
)
monkeypatch.setattr(rabbitmq_client_module.aio_pika, "connect_robust", mock_connect)
monkeypatch.setattr(asyncio, "sleep", AsyncMock()) # чтобы не ждать реально
with allure.step("Test client connection"):
client = RabbitMQClient()
await client.connect()
assert mock_connect.await_count == 3
assert client.connection is fake_connection
@pytest.mark.parametrize("",[
pytest.param(id="test_connect_gives_up_after_5_attempts")
])
async def test_connect_gives_up_after_5_attempts(self, monkeypatch):
with allure.step("Mocking connection to the rabbitmq"):
mock_connect = AsyncMock(side_effect=ConnectionError("still down"))
monkeypatch.setattr(rabbitmq_client_module.aio_pika, "connect_robust", mock_connect)
monkeypatch.setattr(asyncio, "sleep", AsyncMock())
with allure.step("Test client connection"):
client = RabbitMQClient()
with pytest.raises(ConnectionError):
await client.connect()
assert mock_connect.await_count == 5
@pytest.mark.parametrize("",[
pytest.param(id="test_get_channel_negative")
])
async def test_get_channel_negative(self, monkeypatch)->None:
with allure.step("Mocking connection to the rabbitmq"):
fake_connection = MagicMock()
fake_connection.is_closed = False
fake_connection.channel = AsyncMock(return_value=None)
mock_connect = AsyncMock(return_value=fake_connection)
monkeypatch.setattr(rabbitmq_client_module.aio_pika, "connect_robust", mock_connect)
with allure.step("Test client connection"):
client = RabbitMQClient()
with pytest.raises(RuntimeError):
await client.get_channel()
@pytest.mark.parametrize("",[
pytest.param(id="test_get_channel_positive")
])
async def test_get_channel_positive(self, monkeypatch)->None:
with allure.step("Mocking connection to the rabbitmq"):
fake_connection = MagicMock()
fake_connection.is_closed = False
fake_connection.channel = AsyncMock(return_value=MagicMock())
mock_connect = AsyncMock(return_value=fake_connection)
monkeypatch.setattr(rabbitmq_client_module.aio_pika, "connect_robust", mock_connect)
with allure.step("Test client connection"):
client = RabbitMQClient()
await client.get_channel()
assert client.channel is fake_connection.channel.return_value
def make_fake_message(body: dict, routing_key: str = "email.welcome") -> MagicMock:
"""Строим 'фальшивое' RabbitMQ-сообщение вручную, без реального брокера."""
message = MagicMock()
message.body = json.dumps(body).encode()
message.routing_key = routing_key
message.nack = AsyncMock() # запоминает, вызвали ли nack и с какими аргументами
# message.process(...) в реальном aio_pika — async context manager,
# поэтому нужен объект с асинхронными __aenter__/__aexit__.
process_cm = MagicMock()
process_cm.__aenter__ = AsyncMock(return_value=None)
process_cm.__aexit__ = AsyncMock(return_value=False)
message.process = MagicMock(return_value=process_cm)
return message
@pytest.mark.unit
class TestEmailConsumers:
@pytest.mark.parametrize("",[
pytest.param(id="test_start_consuming_negative")
])
async def test_start_consuming_negative(self, monkeypatch)->None:
with allure.step("Mocking connection to the rabbitmq"):
consumer = WelcomeEmailConsumer()
consumer.setup=AsyncMock()
with allure.step("Test client connection"), pytest.raises(RuntimeError):
await consumer.start_consuming()
@pytest.mark.parametrize("error", [
pytest.param(smtplib.SMTPServerDisconnected(), id="smtplib.SMTPServerDisconnected"),
pytest.param(smtplib.SMTPConnectError(421, "Service not available"), id="smtplib.SMTPConnectError"),
pytest.param(TimeoutError(), id="TimeoutError"),
pytest.param(ConnectionRefusedError(), id="ConnectionRefusedError"),
])
async def test_process_message_transient_errors_requeue(self, error) -> None:
consumer = WelcomeEmailConsumer()
consumer.daemon.send_email = AsyncMock(side_effect=error)
message = make_fake_message({"email": "user@example.com"})
await consumer.process_message(message)
message.nack.assert_awaited_once_with(requeue=True)
@pytest.mark.parametrize("",[
pytest.param(id="test_process_message_positive")
])
async def test_process_message_positive(self) -> None:
with allure.step("Mocking dependencies"):
consumer = WelcomeEmailConsumer()
consumer.daemon.send_email = AsyncMock()
with allure.step("Test process_message"):
message = make_fake_message({"email": "user@example.com"})
await consumer.process_message(message)
consumer.daemon.send_email.assert_awaited_once_with("user@example.com")
message.nack.assert_not_called()
@pytest.mark.unit
class TestResetEmailConsumer:
@pytest.mark.parametrize("",[
pytest.param(id="test_reset_start_consuming_negative")
])
async def test_start_consuming_negative(self, monkeypatch) -> None:
with allure.step("Mocking connection to the rabbitmq"):
consumer = ResetEmailConsumer()
consumer.setup = AsyncMock()
with allure.step("Test client connection"), pytest.raises(RuntimeError):
await consumer.start_consuming()
@pytest.mark.parametrize("error", [
pytest.param(smtplib.SMTPServerDisconnected(), id="smtplib.SMTPServerDisconnected"),
pytest.param(smtplib.SMTPConnectError(421, "Service not available"), id="smtplib.SMTPConnectError"),
pytest.param(TimeoutError(), id="TimeoutError"),
pytest.param(ConnectionRefusedError(), id="ConnectionRefusedError"),
])
async def test_process_message_transient_errors_requeue(self, error) -> None:
consumer = ResetEmailConsumer()
consumer.daemon.send_email = AsyncMock(side_effect=error)
message = make_fake_message(
{"email": "user@example.com", "temp_password": "Temp1234!"},
routing_key="email.reset",
)
await consumer.process_message(message)
message.nack.assert_awaited_once_with(requeue=True)
@pytest.mark.parametrize("",[
pytest.param(id="test_reset_process_message_permanent_error_goes_to_dlq")
])
async def test_process_message_permanent_error_goes_to_dlq(self) -> None:
consumer = ResetEmailConsumer()
consumer.daemon.send_email = AsyncMock(side_effect=ValueError("bad payload"))
message = make_fake_message(
{"email": "user@example.com", "temp_password": "Temp1234!"},
routing_key="email.reset",
)
await consumer.process_message(message)
message.nack.assert_awaited_once_with(requeue=False)
@pytest.mark.parametrize("",[
pytest.param(id="test_reset_process_message_positive")
])
async def test_process_message_positive(self) -> None:
with allure.step("Mocking dependencies"):
consumer = ResetEmailConsumer()
consumer.daemon.send_email = AsyncMock()
with allure.step("Test process_message"):
message = make_fake_message(
{"email": "user@example.com", "temp_password": "Temp1234!"},
routing_key="email.reset",
)
await consumer.process_message(message)
consumer.daemon.send_email.assert_awaited_once_with("user@example.com", "Temp1234!")
message.nack.assert_not_called()