"""Unit tests for the RabbitMQ management client. All real HTTP traffic is intercepted with ``respx``. We do not need a running broker here — the integration suite covers live RabbitMQ behaviour. """ from __future__ import annotations import json import httpx import pytest import respx from contract_check.core.mq.management import ( MqManagementDisabledError, MqManagementNotFoundError, MqManagementResponseError, MqManagementUnreachableError, PurgeResult, QueueInfo, QueueMessage, RabbitMQManagementClient, RequeueResult, ) from contract_check.core.mq.topology import DLQ_FOR @pytest.fixture def mgmt_client(monkeypatch: pytest.MonkeyPatch) -> RabbitMQManagementClient: # Pin AMQP URL before Settings is imported/looked up anywhere else. monkeypatch.setenv("RABBITMQ_URL", "amqp://admin:secret@rabbitmq:5672//") from src.contract_check.core.config import get_settings get_settings.cache_clear() return RabbitMQManagementClient() def _mgmt_root(base: str) -> str: return base.rstrip("/") @pytest.mark.anyio async def test_get_queue_info_ok(mgmt_client: RabbitMQManagementClient) -> None: with respx.mock: route = respx.get("http://rabbitmq:15672/api/queues/%2F/extract.q").mock( return_value=httpx.Response( 200, json={"name": "extract.q", "messages": 7, "state": "running"} ) ) await mgmt_client.connect() info = await mgmt_client.get_queue_info("extract.q") assert info == QueueInfo(name="extract.q", messages=7, state="running") assert route.called await mgmt_client.aclose() @pytest.mark.anyio async def test_get_queue_info_not_found(mgmt_client: RabbitMQManagementClient) -> None: with respx.mock: respx.get("http://rabbitmq:15672/api/queues/%2F/missing").mock( return_value=httpx.Response(404) ) await mgmt_client.connect() with pytest.raises(MqManagementNotFoundError): await mgmt_client.get_queue_info("missing") await mgmt_client.aclose() @pytest.mark.anyio async def test_get_queue_info_unreachable(mgmt_client: RabbitMQManagementClient) -> None: with respx.mock: respx.get("http://rabbitmq:15672/api/queues/%2F/extract.q").mock( side_effect=httpx.ConnectError("nope") ) await mgmt_client.connect() with pytest.raises(MqManagementUnreachableError): await mgmt_client.get_queue_info("extract.q") await mgmt_client.aclose() @pytest.mark.anyio async def test_disabled_client_raises( mgmt_client: RabbitMQManagementClient, monkeypatch: pytest.MonkeyPatch ) -> None: monkeypatch.setattr(mgmt_client, "_enabled", False) with pytest.raises(MqManagementDisabledError): await mgmt_client.connect() @pytest.mark.anyio async def test_peek_messages(mgmt_client: RabbitMQManagementClient) -> None: payload = json.dumps({"document_id": "d1"}).encode() with respx.mock: respx.post("http://rabbitmq:15672/api/queues/%2F/extract.dlq/get").mock( return_value=httpx.Response( 200, json=[ { "routing_key": "extract.q", "payload": payload.decode(), "properties": { "correlation_id": "c1", "headers": {"x-failure-class": "infra", "x-failure-error": "boom"}, }, } ], ) ) await mgmt_client.connect() msgs = await mgmt_client.peek_messages("extract.dlq", 5) assert len(msgs) == 1 msg = msgs[0] assert isinstance(msg, QueueMessage) assert msg.correlation_id == "c1" assert msg.routing_key == "extract.q" assert msg.failure_class == "infra" assert msg.failure_error == "boom" assert msg.payload == payload await mgmt_client.aclose() @pytest.mark.anyio async def test_peek_messages_empty_non_list(mgmt_client: RabbitMQManagementClient) -> None: with respx.mock: respx.post("http://rabbitmq:15672/api/queues/%2F/extract.dlq/get").mock( return_value=httpx.Response(200, json={"garbage": True}) ) await mgmt_client.connect() assert await mgmt_client.peek_messages("extract.dlq", 5) == [] await mgmt_client.aclose() @pytest.mark.anyio async def test_purge_queue_reports_pre_purge_count(mgmt_client: RabbitMQManagementClient) -> None: with respx.mock: respx.delete("http://rabbitmq:15672/api/queues/%2F/extract.dlq/contents").mock( return_value=httpx.Response(204) ) respx.get("http://rabbitmq:15672/api/queues/%2F/extract.dlq").mock( return_value=httpx.Response( 200, json={"name": "extract.dlq", "messages": 3, "state": "running"} ) ) await mgmt_client.connect() assert await mgmt_client.purge_queue("extract.dlq") == PurgeResult(messages_removed=3) await mgmt_client.aclose() @pytest.mark.anyio async def test_requeue_batch_unknown_dlq(mgmt_client: RabbitMQManagementClient) -> None: await mgmt_client.connect() with pytest.raises(MqManagementNotFoundError): await mgmt_client.requeue_batch("not-a-known-dlq") await mgmt_client.aclose() @pytest.mark.anyio async def test_requeue_batch_empty(mgmt_client: RabbitMQManagementClient) -> None: dlq = list(DLQ_FOR.values())[0] with respx.mock: respx.post(f"http://rabbitmq:15672/api/queues/%2F/{dlq}/get").mock( return_value=httpx.Response(200, json=[]) ) await mgmt_client.connect() assert await mgmt_client.requeue_batch(dlq) == RequeueResult(messages_requeued=0, errors=0) await mgmt_client.aclose() @pytest.mark.anyio async def test_requeue_batch_success(mgmt_client: RabbitMQManagementClient) -> None: dlq = "extract.dlq" main_queue = "extract.q" payload = json.dumps({"document_id": "d1"}) with respx.mock: respx.post("http://rabbitmq:15672/api/queues/%2F/extract.dlq/get").mock( return_value=httpx.Response( 200, json=[ { "routing_key": main_queue, "payload": payload, "properties": { "correlation_id": "c1", "headers": {"x-attempt": 5, "x-failure-class": "infra"}, }, } ], ) ) respx.post("http://rabbitmq:15672/api/exchanges/%2F/contracts.x/publish").mock( return_value=httpx.Response(200, json={"routed": True}) ) await mgmt_client.connect() result = await mgmt_client.requeue_batch(dlq, batch_size=10) assert result == RequeueResult(messages_requeued=1, errors=0) await mgmt_client.aclose() @pytest.mark.anyio async def test_requeue_batch_publish_failure_counts_as_error( mgmt_client: RabbitMQManagementClient, ) -> None: dlq = "extract.dlq" main_queue = "extract.q" with respx.mock: respx.post("http://rabbitmq:15672/api/queues/%2F/extract.dlq/get").mock( return_value=httpx.Response( 200, json=[ { "routing_key": main_queue, "payload": "{}", "properties": {"headers": {"x-attempt": 5}}, } ], ) ) respx.post("http://rabbitmq:15672/api/exchanges/%2F/contracts.x/publish").mock( return_value=httpx.Response(500, text="broker error") ) await mgmt_client.connect() result = await mgmt_client.requeue_batch(dlq, batch_size=10) assert result == RequeueResult(messages_requeued=0, errors=1) await mgmt_client.aclose() @pytest.mark.anyio async def test_response_error_includes_status_and_body( mgmt_client: RabbitMQManagementClient, ) -> None: with respx.mock: respx.get("http://rabbitmq:15672/api/queues/%2F/extract.q").mock( return_value=httpx.Response(418, text="teapot") ) await mgmt_client.connect() with pytest.raises(MqManagementResponseError) as exc_info: await mgmt_client.get_queue_info("extract.q") err = exc_info.value assert err.status_code == 418 assert err.body == "teapot" await mgmt_client.aclose()