test(General): finished unit test, coverage 88%
Unit test coverage / pytest (push) Successful in 1m8s
Unit test coverage / pytest (pull_request) Successful in 1m6s

ISSUES CLOSED: clevermicro/amq-adapter-python#16
This commit is contained in:
2025-05-16 18:16:04 +08:00
parent aec53f5b34
commit 2e12d5ab57
4 changed files with 618 additions and 2 deletions
+7 -2
View File
@@ -17,7 +17,7 @@ from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import DataMessage
from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
from amqp.service.amq_message_handler import DataMessageHandler
from amqp.service.tracing import initialize_trace
from amqp.service.tracing import TracingConfig, initialize_telemetry
logging.basicConfig(level=logging.DEBUG)
logging.info(f"CWD = {os.getcwd()}")
@@ -46,7 +46,12 @@ class AMQService:
self.amq_configuration.amq_adapter.generator_id
)
self.rabbit_mq_client = RabbitMQClient(self.amq_configuration)
self.tracer = initialize_trace(name=amq_configuration.amq_adapter.service_name)
self.tracer = initialize_telemetry(
tracing_config=TracingConfig(
service_name=amq_configuration.amq_adapter.service_name,
service_version=os.getenv("SERVICE_VERSION", "unknown"),
)
)
self.amq_data_message_handler = None
self.service_message_factory = ServiceMessageFactory(
self.amq_configuration.amq_adapter.service_name
+210
View File
@@ -0,0 +1,210 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from aio_pika.abc import (
AbstractIncomingMessage,
AbstractRobustChannel,
AbstractRobustQueue,
)
from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import AMQRoute, ServiceMessage
from amqp.model.service_message_type import ServiceMessageType
from amqp.router.router_base import (
CM_C_REQ_QUEUE,
CM_REQUEST_EXCHANGE,
)
from amqp.router.router_consumer import RouterConsumer
class TestRouterConsumer(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Create a mock for amq_adapter
self.amq_adapter = MagicMock()
self.amq_adapter.adapter_prefix.return_value = "test_"
self.amq_adapter.service_name = "test_service"
# Create the config mock with amq_adapter
self.config = MagicMock(spec=AMQConfiguration)
self.config.amq_adapter = self.amq_adapter
self.consumer = RouterConsumer(self.config)
# Mock channel
self.channel = AsyncMock(spec=AbstractRobustChannel)
self.consumer.channel = self.channel
# Mock queue
self.queue = AsyncMock(spec=AbstractRobustQueue)
self.channel.declare_queue.return_value = self.queue
def test_init(self):
"""Test initialization of RouterConsumer"""
self.assertIsNotNone(self.consumer)
self.assertEqual(self.consumer.amq_configuration, self.config)
@patch("amqp.router.router_consumer.logging_info")
async def test_init_routing_paths_consumer_success(self, mock_logging):
"""Test successful initialization of routing paths consumer"""
self.consumer.is_open = MagicMock(return_value=True)
await self.consumer.init_routing_paths_consumer(self.channel)
# Verify queue declaration
self.channel.declare_queue.assert_called_once_with(
name="test_" + CM_C_REQ_QUEUE, durable=True, exclusive=False, auto_delete=False
)
# Verify queue binding
self.queue.bind.assert_called_once_with(exchange=CM_REQUEST_EXCHANGE, routing_key="")
# Verify consumer setup
self.queue.consume.assert_called_once()
@patch("amqp.router.router_consumer.logging_error")
async def test_init_routing_paths_consumer_channel_closed(self, mock_logging):
"""Test initialization when channel is closed"""
self.consumer.is_open = MagicMock(return_value=False)
await self.consumer.init_routing_paths_consumer(self.channel)
mock_logging.assert_called_once()
self.channel.declare_queue.assert_not_called()
@patch("amqp.router.router_consumer.logging_debug")
@patch("amqp.router.router_consumer.logging_info")
async def test_handle_routing_data_request_message_valid(
self, mock_logging_info, mock_logging_debug
):
"""Test handling of valid routing data request message"""
# Setup
message = AsyncMock(spec=AbstractIncomingMessage)
message.delivery_tag = 1
message.consumer_tag = "test_consumer"
message.properties = MagicMock()
message.body = b"test_body"
# Mock service message factory
mock_service_message = ServiceMessage(
id=None,
payload_type=0,
message_type=ServiceMessageType.ROUTING_DATA_REQUEST,
recipient_name="test_service",
reply_to="other_service",
trace_info={},
message="test_payload",
)
self.consumer.service_message_factory = MagicMock()
self.consumer.service_message_factory.from_bytes = MagicMock(
return_value=mock_service_message
)
self.consumer.service_message_factory.of = MagicMock()
self.consumer.wrap_own_routes = MagicMock(return_value="test_routes")
self.consumer.publish_service_message = AsyncMock(return_value=True)
# Execute
await self.consumer.handle_routing_data_request_message(message)
# Verify
message.ack.assert_called_once_with(False)
self.consumer.publish_service_message.assert_called_once()
@patch("amqp.router.router_consumer.logging_info")
async def test_register_route_success(self, mock_logging):
"""Test successful route registration"""
# Setup
route = AMQRoute(
component_name="test_service",
exchange="test_exchange",
queue="test_queue",
key="test_key",
timeout=1000,
valid_until=0,
)
self.consumer.is_open = MagicMock(return_value=True)
self.consumer.publish_service_message = AsyncMock(return_value=True)
self.consumer.add_own_route = MagicMock()
# Execute
await self.consumer.register_route(route)
# Verify
self.consumer.publish_service_message.assert_called_once()
self.consumer.add_own_route.assert_called_once_with(route)
@patch("amqp.router.router_consumer.logging_warning")
async def test_register_route_channel_closed(self, mock_logging):
"""Test route registration when channel is closed"""
# Setup
route = AMQRoute(
component_name="test_service",
exchange="test_exchange",
queue="test_queue",
key="test_key",
timeout=1000,
valid_until=0,
)
self.consumer.is_open = MagicMock(return_value=False)
# Execute
await self.consumer.register_route(route)
# Verify
mock_logging.assert_called_once()
@patch("amqp.router.router_consumer.logging_info")
@patch("amqp.router.router_consumer.logging_warning")
async def test_remove_route_on_cleanup(self, mock_logging_warning, mock_logging_info):
"""Test route removal during cleanup"""
# Setup
route = AMQRoute(
component_name="test_service",
exchange="test_exchange",
queue="test_queue",
key="test_key",
timeout=1000,
valid_until=0,
)
self.consumer.publish_service_message = AsyncMock(return_value=True)
self.consumer.remove_route = MagicMock(return_value=True)
# Execute
result = await self.consumer.remove_route_on_cleanup(route)
# Verify
self.consumer.publish_service_message.assert_called_once()
self.consumer.remove_route.assert_called_once_with(route)
self.assertTrue(result)
@patch("amqp.router.router_consumer.logging_info")
@patch("amqp.router.router_consumer.logging_warning")
def test_cleanup(self, mock_logging_warning, mock_logging_info):
"""Test cleanup process"""
# Setup
routes = {
AMQRoute(
component_name="test_service",
exchange="test_exchange1",
queue="test_queue1",
key="test_key1",
timeout=1000,
valid_until=0,
),
AMQRoute(
component_name="test_service",
exchange="test_exchange2",
queue="test_queue2",
key="test_key2",
timeout=1000,
valid_until=0,
),
}
self.consumer.get_routes = MagicMock(return_value=routes)
self.consumer.remove_route_on_cleanup = AsyncMock(return_value=True)
self.consumer.wrap_routes = MagicMock(return_value="test_routes")
# Execute
self.consumer.cleanup()
# Verify
self.assertEqual(self.consumer.remove_route_on_cleanup.call_count, 2)
mock_logging_info.assert_called()
+216
View File
@@ -0,0 +1,216 @@
import asyncio
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
from aio_pika import Message
from aio_pika.abc import AbstractIncomingMessage, AbstractRobustExchange
from opentelemetry.trace import Tracer
from amqp.adapter.backpressure_handler import BackpressureHandler
from amqp.adapter.cleverthis_service_adapter import CleverThisServiceAdapter
from amqp.adapter.data_message_factory import DataMessageFactory
from amqp.adapter.data_response_factory import DataResponseFactory
from amqp.adapter.file_handler import FileHandler
from amqp.adapter.service_message_factory import ServiceMessageFactory
from amqp.config.amq_configuration import AMQConfiguration
from amqp.model.model import (
DataMessage,
DataMessageMagicByte,
DataResponse,
MultipartDataMessage,
)
from amqp.rabbitmq.rabbit_mq_client import RabbitMQClient
from amqp.service.amq_message_handler import DataMessageHandler
class TestDataMessageHandler(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Create mock objects for dependencies
self.service_adapter = MagicMock(spec=CleverThisServiceAdapter)
self.tracer = MagicMock(spec=Tracer)
self.message_factory = MagicMock(spec=DataMessageFactory)
self.service_message_factory = MagicMock(spec=ServiceMessageFactory)
self.reply_to_exchange = MagicMock(spec=AbstractRobustExchange)
self.rabbit_mq_client = MagicMock(spec=RabbitMQClient)
# Create a proper mock configuration with nested attributes
self.amq_configuration = MagicMock(spec=AMQConfiguration)
dispatch_mock = MagicMock()
dispatch_mock.rabbit_mq_url = "amqp://test"
dispatch_mock.download_dir = "/test/dir"
self.amq_configuration.dispatch = dispatch_mock
self.loop = asyncio.new_event_loop()
self.backpressure_handler = MagicMock(spec=BackpressureHandler)
self.backpressure_handler.parallel_workers = 10
self.backpressure_handler.current_parallel_executions = 0
self.file_handler = MagicMock(spec=FileHandler)
# Create the handler instance
self.handler = DataMessageHandler(
service_adapter=self.service_adapter,
tracer=self.tracer,
message_factory=self.message_factory,
service_message_factory=self.service_message_factory,
reply_to_exchange=self.reply_to_exchange,
rabbit_mq_client=self.rabbit_mq_client,
amq_configuration=self.amq_configuration,
loop=self.loop,
backpressure_handler=self.backpressure_handler,
file_handler=self.file_handler,
)
def tearDown(self):
self.loop.close()
@patch("amqp.service.amq_message_handler.threading.Thread")
async def test_inbound_data_message_callback_as_thread(self, mock_thread):
# Arrange
message = MagicMock(spec=AbstractIncomingMessage)
mock_thread.return_value.start = MagicMock()
# Act
await self.handler.inbound_data_message_callback_as_thread(message)
# Assert
mock_thread.assert_called_once()
mock_thread.return_value.start.assert_called_once()
async def test_reconstitute_data_message_simple(self):
# Arrange
message = MagicMock(spec=AbstractIncomingMessage)
message.headers = {"clevermicro.addressing": 0}
message.body = (
b"\002\027A64f3b5af4d00000a4000|m=POST|d=test.domain|p=/test|h={}|t={}|b=test_body"
)
message.delivery_tag = 123
message.consumer_tag = "test_consumer"
expected_message = MagicMock(spec=DataMessage)
self.message_factory.from_bytes = MagicMock(return_value=expected_message)
# Act
result = await self.handler.reconstitute_data_message(message)
# Assert
self.assertEqual(result, expected_message)
async def test_reconstitute_data_message_multipart(self):
# Arrange
message = MagicMock(spec=AbstractIncomingMessage)
message.headers = {"clevermicro.addressing": 1}
message.body = b'{"req-info": "test_queue"}'
message.delivery_tag = 123
message.consumer_tag = "test_consumer"
base_message = MagicMock(spec=DataMessage)
base_message.magic.return_value = DataMessageMagicByte.HTTP_REQUEST.value
base_message.id.return_value = MagicMock(as_string=lambda: "test_id")
base_message.method.return_value = "POST"
base_message.domain.return_value = "test.domain"
base_message.path.return_value = "/test"
base_message.headers.return_value = {}
base_message.trace_info.return_value = {}
base_message.base64body.return_value = b"test_body"
base_message.body.return_value = b"test_body"
# Mock the static methods
with (
patch(
"amqp.adapter.data_message_factory.DataMessageFactory.from_bytes",
return_value=base_message,
),
patch(
"amqp.adapter.data_message_factory.DataMessageFactory.from_stream",
new_callable=AsyncMock,
return_value=base_message,
),
):
self.file_handler.on_message = AsyncMock(return_value={"file1": "path1"})
self.file_handler.download_buffer = AsyncMock(return_value=(b"test_body", 1))
# Act
result = await self.handler.reconstitute_data_message(message)
# Assert
self.assertIsInstance(result, MultipartDataMessage)
self.file_handler.on_message.assert_called_once()
async def test_send_reply(self):
# Arrange
reply_to = "test_queue"
response = MagicMock(spec=DataResponse)
snowflake_id = MagicMock()
snowflake_id.as_string.return_value = "test_id"
response.id = MagicMock(return_value=snowflake_id)
response.content_type = MagicMock(return_value="application/json")
response.body = MagicMock(return_value=b"test_response")
response.base64body = MagicMock(return_value=b"test_response")
response.headers = MagicMock(return_value={"Content-Type": ["application/json"]})
response.trace_info = MagicMock(return_value={})
response.response_code = MagicMock(return_value="200")
response.error = MagicMock(return_value="")
response.error_cause = MagicMock(return_value="")
self.reply_to_exchange.publish = AsyncMock()
self.reply_to_exchange.name = "test_exchange"
# Act
await self.handler.send_reply(reply_to, response)
# Assert
self.reply_to_exchange.publish.assert_called_once()
call_args = self.reply_to_exchange.publish.call_args[1]
self.assertEqual(call_args["routing_key"], reply_to)
self.assertIsInstance(call_args["message"], Message)
async def test_reply_received_callback(self):
# Arrange
message = MagicMock(spec=AbstractIncomingMessage)
message.correlation_id = "test_id"
# Create a properly formatted response message body with magic byte
message.body = (
b"\002\027A64f3b5af4d00000a4000|r=200|c=application/json|e=|f=|t={}|b=test_response"
)
message.delivery_tag = 123
message.headers = {}
message.properties = {}
message.ack = AsyncMock()
future = asyncio.Future()
self.handler.outstanding["test_id"] = future
expected_body = MagicMock()
with patch.object(DataResponseFactory, "from_bytes") as mock_from_bytes:
mock_from_bytes.return_value = expected_body
# Act
await self.handler.reply_received_callback(message)
# Assert
message.ack.assert_called_once()
self.assertNotIn("test_id", self.handler.outstanding)
self.assertEqual(future.result(), expected_body.body())
async def test_run_http_request_magic(self):
# Arrange
message = MagicMock(spec=AbstractIncomingMessage)
message.reply_to = "test_reply_to"
message.delivery_tag = 123
amq_message = MagicMock(spec=DataMessage)
amq_message.magic.return_value = DataMessageMagicByte.HTTP_REQUEST.value
self.service_adapter.on_message = AsyncMock(return_value=MagicMock(spec=DataResponse))
self.backpressure_handler.check_overload_condition = AsyncMock()
self.handler.send_reply = AsyncMock()
self.rabbit_mq_client.connection = MagicMock()
# Act
await self.handler.run_http_request_magic(message, amq_message, self.loop)
# Assert
self.backpressure_handler.increase_parallel_executions.assert_called_once()
self.backpressure_handler.decrease_parallel_executions.assert_called_once()
self.service_adapter.on_message.assert_called_once()
self.handler.send_reply.assert_called_once()
+185
View File
@@ -0,0 +1,185 @@
import unittest
from unittest.mock import AsyncMock, MagicMock, Mock, patch
from amqp.config.amq_configuration import AMQAdapter, AMQConfiguration
from amqp.model.model import DataMessage
from amqp.service.amq_service import AMQService
class TestAMQService(unittest.IsolatedAsyncioTestCase):
def setUp(self):
# Create a mock configuration
self.mock_adapter = Mock(spec=AMQAdapter)
self.mock_adapter.generator_id = 12345
self.mock_adapter.service_name = "test-service"
self.mock_adapter.route_mapping = {}
self.mock_adapter.swarm_service_id = "test-swarm-id"
self.mock_adapter.swarm_task_id = "test-task-id"
self.mock_config = Mock(spec=AMQConfiguration)
self.mock_config.amq_adapter = self.mock_adapter
# Add dispatch configuration
self.mock_dispatch = Mock()
self.mock_dispatch.amq_host = "localhost"
self.mock_dispatch.amq_port = 5672
self.mock_dispatch.rabbit_mq_user = "guest"
self.mock_dispatch.rabbit_mq_password = "guest"
self.mock_dispatch.use_dlq = False
self.mock_config.dispatch = self.mock_dispatch
# Add backpressure configuration
self.mock_backpressure = Mock()
self.mock_backpressure.threshold_threads = 10
self.mock_config.backpressure = self.mock_backpressure
# Create service instance with mocked configuration
self.service = AMQService(self.mock_config)
# Mock the event loop
self.mock_loop = Mock()
self.service.loop = self.mock_loop
def tearDown(self):
# Clean up any resources
if hasattr(self, "service"):
self.service.cleanup()
@patch("amqp.service.amq_service.RabbitMQClient")
@patch("amqp.service.amq_service.DataMessageFactory")
@patch("amqp.service.amq_service.ServiceMessageFactory")
@patch("amqp.service.amq_service.initialize_telemetry")
def test_init(
self, mock_telemetry, mock_service_factory, mock_data_factory, mock_rabbit_client
):
# Test initialization of AMQService
service = AMQService(self.mock_config)
# Verify that all required components are initialized
self.assertIsNotNone(service.amq_configuration)
self.assertIsNotNone(service.data_message_factory)
self.assertIsNotNone(service.rabbit_mq_client)
self.assertIsNotNone(service.tracer)
self.assertIsNotNone(service.service_message_factory)
@patch("amqp.service.amq_service.RabbitMQClient")
async def test_register_routes_valid(self, mock_rabbit_client):
# Setup mock route mapping
self.mock_adapter.route_mapping = "test-service:test-exchange:test-queue:test.#:5:0"
# Setup mock rabbit client
mock_client = Mock()
mock_client.register_inbound_routes = AsyncMock()
mock_client.register_data_reply_callback = AsyncMock()
mock_client.init = AsyncMock()
mock_client.request_routes = AsyncMock()
mock_client.channel = Mock()
mock_client.channel.is_closed = False
self.service.rabbit_mq_client = mock_client
# Setup mock service adapter
mock_service_adapter = Mock()
mock_service_adapter.loop = self.mock_loop
# Setup mock message handler
mock_handler = Mock()
mock_handler.inbound_data_message_callback_no_thread = AsyncMock()
mock_handler.reply_received_callback = AsyncMock()
self.service.amq_data_message_handler = mock_handler
# Initialize the service
await self.service.init(self.mock_loop, mock_service_adapter)
# Call register_routes
await self.service.register_routes()
# Verify that routes were registered twice (once in init, once in register_routes)
self.assertEqual(mock_client.register_inbound_routes.call_count, 2)
self.assertEqual(mock_client.register_data_reply_callback.call_count, 2)
@patch("amqp.service.amq_service.RabbitMQClient")
async def test_register_routes_invalid(self, mock_rabbit_client):
# Setup invalid route mapping
self.mock_adapter.route_mapping = None
# Setup mock rabbit client
mock_client = Mock()
mock_client.register_inbound_routes = AsyncMock()
mock_client.register_data_reply_callback = AsyncMock()
self.service.rabbit_mq_client = mock_client
# Call register_routes
result = await self.service.register_routes()
# Verify that no routes were registered
mock_client.register_inbound_routes.assert_not_called()
mock_client.register_data_reply_callback.assert_not_called()
self.assertIsNone(result)
@patch("amqp.service.amq_service.RabbitMQClient")
async def test_reinitialize_if_disconnected(self, mock_rabbit_client):
# Setup mock route mapping
self.mock_adapter.route_mapping = "test-service:test-exchange:test-queue:test.#:5:0"
# Setup mock rabbit client with closed channel
mock_client = AsyncMock()
mock_client.channel = Mock()
mock_client.channel.is_closed = True
mock_client.init = AsyncMock()
mock_client.register_inbound_routes = AsyncMock()
mock_client.register_data_reply_callback = AsyncMock()
mock_client.request_routes = AsyncMock()
# Make the constructor return our mock client
mock_rabbit_client.return_value = mock_client
self.service.rabbit_mq_client = mock_client
# Setup mock message handler
mock_handler = Mock()
mock_handler.inbound_data_message_callback_no_thread = AsyncMock()
mock_handler.reply_received_callback = AsyncMock()
self.service.amq_data_message_handler = mock_handler
# Call reinitialize_if_disconnected
await self.service.reinitialize_if_disconnected()
# Verify that client was reinitialized
mock_client.init.assert_called_once()
def test_send_message(self):
# Create mock message and future
mock_message = Mock(spec=DataMessage)
mock_message.id.return_value = "test-id"
mock_future = MagicMock()
# Setup mock message handler
self.service.amq_data_message_handler = Mock()
self.service.amq_data_message_handler.outstanding = {}
# Send message
self.service.send_message(mock_message, mock_future)
# Verify message was added to outstanding
self.assertEqual(self.service.amq_data_message_handler.outstanding["test-id"], mock_future)
def test_remove_outstanding_future(self):
# Setup mock message handler with outstanding message
self.service.amq_data_message_handler = Mock()
self.service.amq_data_message_handler.outstanding = {"test-id": Mock()}
# Remove future
self.service.remove_outstanding_future("test-id")
# Verify message was removed from outstanding
self.assertNotIn("test-id", self.service.amq_data_message_handler.outstanding)
def test_cleanup(self):
# Setup mock rabbit client
mock_client = Mock()
self.service.rabbit_mq_client = mock_client
# Call cleanup
self.service.cleanup()
# Verify rabbit client cleanup was called
mock_client.cleanup.assert_called_once()