DealDocumentScreening/tests/unit/test_mq_management.py
febux 0b2e7665f4 fixup(pipeline-hardening M4/M5): purge count, shared toast helper, clean imports
- Fix RabbitMQ purge_queue to report the pre-purge message count instead of
  the post-purge depth (which was always 0).
- Add shared attach_toast helper in admin templating; remove duplicated inline
  _attach_toast implementations in review.py and mq.py.
- Keep purge confirmation case-sensitive so the typed safeguard works as intended.
- Fix test imports to use known-first-party 'contract_check' per isort config.
2026-09-06 01:18:33 +03:00

242 lines
8.4 KiB
Python

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