|
2 | 2 | from unittest.mock import Mock |
3 | 3 |
|
4 | 4 | import pytest |
| 5 | +from aiokafka.structs import ConsumerRecord |
5 | 6 | from kafka.admin import KafkaAdminClient, NewTopic |
6 | 7 | from taskiq import BrokerMessage |
7 | 8 |
|
8 | 9 | from taskiq_aio_kafka.broker import AioKafkaBroker |
| 10 | +from taskiq_aio_kafka.constants import TASK_STREAM_LABEL |
9 | 11 | from taskiq_aio_kafka.topic import Topic |
10 | 12 |
|
11 | 13 |
|
@@ -40,13 +42,53 @@ async def stop(self) -> None: |
40 | 42 | """Stop consumer.""" |
41 | 43 |
|
42 | 44 |
|
| 45 | +class _ConsumerMock: |
| 46 | + """Kafka consumer mock.""" |
| 47 | + |
| 48 | + def __init__(self, messages: list[ConsumerRecord[None, bytes]]) -> None: |
| 49 | + self._messages = iter(messages) |
| 50 | + |
| 51 | + def __aiter__(self) -> "_ConsumerMock": |
| 52 | + return self |
| 53 | + |
| 54 | + async def __anext__(self) -> ConsumerRecord[None, bytes]: |
| 55 | + """Return next message.""" |
| 56 | + try: |
| 57 | + return next(self._messages) |
| 58 | + except StopIteration as exc: |
| 59 | + raise StopAsyncIteration from exc |
| 60 | + |
| 61 | + |
43 | 62 | def get_admin_client_mock() -> KafkaAdminClient: |
44 | 63 | """Get kafka admin client mock.""" |
45 | 64 | admin_client = Mock(spec=KafkaAdminClient) |
46 | 65 | admin_client.list_topics.return_value = [] |
47 | 66 | return admin_client |
48 | 67 |
|
49 | 68 |
|
| 69 | +def build_consumer_record(topic: str, value: bytes) -> ConsumerRecord[None, bytes]: |
| 70 | + """Build Kafka consumer record.""" |
| 71 | + return ConsumerRecord( |
| 72 | + topic=topic, |
| 73 | + partition=0, |
| 74 | + offset=0, |
| 75 | + timestamp=0, |
| 76 | + timestamp_type=0, |
| 77 | + key=None, |
| 78 | + value=value, |
| 79 | + checksum=None, |
| 80 | + serialized_key_size=0, |
| 81 | + serialized_value_size=len(value), |
| 82 | + headers=[], |
| 83 | + ) |
| 84 | + |
| 85 | + |
| 86 | +async def get_first_task(broker: AioKafkaBroker) -> bytes: # type: ignore[return] |
| 87 | + """Get first message from the topic.""" |
| 88 | + async for message in broker.listen(): |
| 89 | + return message |
| 90 | + |
| 91 | + |
50 | 92 | async def test_task_topic_is_used_for_kick() -> None: |
51 | 93 | """Test that task is sent to its declared topic.""" |
52 | 94 | broker = AioKafkaBroker( |
@@ -252,3 +294,99 @@ def create_consumer( |
252 | 294 | await broker.shutdown() |
253 | 295 |
|
254 | 296 | assert consumer_topics == ("default-topic", "extra-topic") |
| 297 | + |
| 298 | + |
| 299 | +async def test_subscribe_wraps_raw_topic_message() -> None: |
| 300 | + """Test that raw Kafka messages are wrapped as taskiq messages.""" |
| 301 | + broker = AioKafkaBroker( |
| 302 | + bootstrap_servers="localhost", |
| 303 | + kafka_topic="default-topic", |
| 304 | + kafka_admin_client=get_admin_client_mock(), |
| 305 | + ) |
| 306 | + |
| 307 | + @broker.task |
| 308 | + async def test_task(message: str) -> None: |
| 309 | + assert message |
| 310 | + |
| 311 | + broker.subscribe( |
| 312 | + "stream-topic", |
| 313 | + test_task, |
| 314 | + decoder=lambda message: message.decode(), |
| 315 | + ) |
| 316 | + broker._aiokafka_consumer = _ConsumerMock( |
| 317 | + [build_consumer_record("stream-topic", b"raw-message")], |
| 318 | + ) |
| 319 | + broker._is_consumer_started = True |
| 320 | + |
| 321 | + received_message = await get_first_task(broker) |
| 322 | + taskiq_message = broker.formatter.loads(received_message) |
| 323 | + |
| 324 | + assert taskiq_message.task_name == test_task.task_name |
| 325 | + assert taskiq_message.args == ["raw-message"] |
| 326 | + assert taskiq_message.kwargs == {} |
| 327 | + assert taskiq_message.labels[TASK_STREAM_LABEL] == "stream-topic" |
| 328 | + |
| 329 | + |
| 330 | +async def test_subscribe_accepts_topic_object_and_custom_labels() -> None: |
| 331 | + """Test that stream subscriptions can use Topic objects.""" |
| 332 | + broker = AioKafkaBroker( |
| 333 | + bootstrap_servers="localhost", |
| 334 | + kafka_topic="default-topic", |
| 335 | + kafka_admin_client=get_admin_client_mock(), |
| 336 | + ) |
| 337 | + stream_topic = Topic("stream-topic") |
| 338 | + |
| 339 | + @broker.task |
| 340 | + async def test_task(message: bytes) -> None: |
| 341 | + assert message |
| 342 | + |
| 343 | + broker.subscribe(stream_topic, test_task, source="external") |
| 344 | + broker._aiokafka_consumer = _ConsumerMock( |
| 345 | + [build_consumer_record(stream_topic.name, b"raw-message")], |
| 346 | + ) |
| 347 | + broker._is_consumer_started = True |
| 348 | + |
| 349 | + received_message = await get_first_task(broker) |
| 350 | + taskiq_message = broker.formatter.loads(received_message) |
| 351 | + |
| 352 | + assert taskiq_message.task_name == test_task.task_name |
| 353 | + assert taskiq_message.args == ["raw-message"] |
| 354 | + assert taskiq_message.labels == { |
| 355 | + TASK_STREAM_LABEL: stream_topic.name, |
| 356 | + "source": "external", |
| 357 | + } |
| 358 | + |
| 359 | + |
| 360 | +def test_subscribe_adds_topic_to_broker_topics() -> None: |
| 361 | + """Test that subscribed topic is included in listened topics.""" |
| 362 | + broker = AioKafkaBroker( |
| 363 | + bootstrap_servers="localhost", |
| 364 | + kafka_topic="default-topic", |
| 365 | + kafka_admin_client=get_admin_client_mock(), |
| 366 | + ) |
| 367 | + |
| 368 | + @broker.task |
| 369 | + async def test_task(message: bytes) -> None: |
| 370 | + assert message |
| 371 | + |
| 372 | + broker.subscribe("stream-topic", test_task) |
| 373 | + |
| 374 | + assert set(broker._kafka_topics) == {"default-topic", "stream-topic"} |
| 375 | + |
| 376 | + |
| 377 | +def test_subscribe_rejects_duplicate_topics() -> None: |
| 378 | + """Test that stream topic cannot be subscribed twice.""" |
| 379 | + broker = AioKafkaBroker( |
| 380 | + bootstrap_servers="localhost", |
| 381 | + kafka_topic="default-topic", |
| 382 | + kafka_admin_client=get_admin_client_mock(), |
| 383 | + ) |
| 384 | + |
| 385 | + @broker.task |
| 386 | + async def test_task(message: bytes) -> None: |
| 387 | + assert message |
| 388 | + |
| 389 | + broker.subscribe("stream-topic", test_task) |
| 390 | + |
| 391 | + with pytest.raises(ValueError, match="already subscribed"): |
| 392 | + broker.subscribe("stream-topic", test_task) |
0 commit comments