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