cledar-sdk 2.0.2__py3-none-any.whl → 2.0.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cledar/__init__.py +0 -0
- cledar/kafka/README.md +239 -0
- cledar/kafka/__init__.py +40 -0
- cledar/kafka/clients/base.py +98 -0
- cledar/kafka/clients/consumer.py +110 -0
- cledar/kafka/clients/producer.py +80 -0
- cledar/kafka/config/schemas.py +178 -0
- cledar/kafka/exceptions.py +22 -0
- cledar/kafka/handlers/dead_letter.py +82 -0
- cledar/kafka/handlers/parser.py +49 -0
- cledar/kafka/logger.py +3 -0
- cledar/kafka/models/input.py +13 -0
- cledar/kafka/models/message.py +10 -0
- cledar/kafka/models/output.py +8 -0
- cledar/kafka/tests/.env.test.kafka +3 -0
- cledar/kafka/tests/README.md +216 -0
- cledar/kafka/tests/conftest.py +104 -0
- cledar/kafka/tests/integration/__init__.py +1 -0
- cledar/kafka/tests/integration/conftest.py +78 -0
- cledar/kafka/tests/integration/helpers.py +47 -0
- cledar/kafka/tests/integration/test_consumer_integration.py +375 -0
- cledar/kafka/tests/integration/test_integration.py +394 -0
- cledar/kafka/tests/integration/test_producer_consumer_interaction.py +388 -0
- cledar/kafka/tests/integration/test_producer_integration.py +217 -0
- cledar/kafka/tests/unit/__init__.py +1 -0
- cledar/kafka/tests/unit/test_base_kafka_client.py +391 -0
- cledar/kafka/tests/unit/test_config_validation.py +609 -0
- cledar/kafka/tests/unit/test_dead_letter_handler.py +443 -0
- cledar/kafka/tests/unit/test_error_handling.py +674 -0
- cledar/kafka/tests/unit/test_input_parser.py +310 -0
- cledar/kafka/tests/unit/test_input_parser_comprehensive.py +489 -0
- cledar/kafka/tests/unit/test_utils.py +25 -0
- cledar/kafka/tests/unit/test_utils_comprehensive.py +408 -0
- cledar/kafka/utils/callbacks.py +19 -0
- cledar/kafka/utils/messages.py +28 -0
- cledar/kafka/utils/topics.py +2 -0
- cledar/kserve/README.md +352 -0
- cledar/kserve/__init__.py +3 -0
- cledar/kserve/tests/__init__.py +0 -0
- cledar/kserve/tests/test_utils.py +64 -0
- cledar/kserve/utils.py +27 -0
- cledar/logging/README.md +53 -0
- cledar/logging/__init__.py +3 -0
- cledar/logging/tests/test_universal_plaintext_formatter.py +249 -0
- cledar/logging/universal_plaintext_formatter.py +94 -0
- cledar/monitoring/README.md +71 -0
- cledar/monitoring/__init__.py +3 -0
- cledar/monitoring/monitoring_server.py +112 -0
- cledar/monitoring/tests/integration/test_monitoring_server_int.py +162 -0
- cledar/monitoring/tests/test_monitoring_server.py +59 -0
- cledar/nonce/README.md +99 -0
- cledar/nonce/__init__.py +3 -0
- cledar/nonce/nonce_service.py +36 -0
- cledar/nonce/tests/__init__.py +0 -0
- cledar/nonce/tests/test_nonce_service.py +136 -0
- cledar/redis/README.md +536 -0
- cledar/redis/__init__.py +15 -0
- cledar/redis/async_example.py +111 -0
- cledar/redis/example.py +37 -0
- cledar/redis/exceptions.py +22 -0
- cledar/redis/logger.py +3 -0
- cledar/redis/model.py +10 -0
- cledar/redis/redis.py +525 -0
- cledar/redis/redis_config_store.py +252 -0
- cledar/redis/tests/test_async_integration_redis.py +158 -0
- cledar/redis/tests/test_async_redis_service.py +380 -0
- cledar/redis/tests/test_integration_redis.py +119 -0
- cledar/redis/tests/test_redis_service.py +319 -0
- cledar/storage/README.md +529 -0
- cledar/storage/__init__.py +4 -0
- cledar/storage/constants.py +3 -0
- cledar/storage/exceptions.py +50 -0
- cledar/storage/models.py +19 -0
- cledar/storage/object_storage.py +955 -0
- cledar/storage/tests/conftest.py +18 -0
- cledar/storage/tests/test_abfs.py +164 -0
- cledar/storage/tests/test_integration_filesystem.py +359 -0
- cledar/storage/tests/test_integration_s3.py +453 -0
- cledar/storage/tests/test_local.py +384 -0
- cledar/storage/tests/test_s3.py +521 -0
- {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/METADATA +1 -1
- cledar_sdk-2.0.3.dist-info/RECORD +84 -0
- cledar_sdk-2.0.2.dist-info/RECORD +0 -4
- {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/WHEEL +0 -0
- {cledar_sdk-2.0.2.dist-info → cledar_sdk-2.0.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Comprehensive tests for error handling, retry mechanisms, and edge cases
|
|
3
|
+
in the Kafka service module.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import time
|
|
7
|
+
from unittest.mock import MagicMock, patch
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from confluent_kafka import KafkaError, KafkaException
|
|
11
|
+
|
|
12
|
+
from cledar.kafka.clients.consumer import KafkaConsumer
|
|
13
|
+
from cledar.kafka.clients.producer import KafkaProducer
|
|
14
|
+
from cledar.kafka.config.schemas import KafkaConsumerConfig, KafkaProducerConfig
|
|
15
|
+
from cledar.kafka.exceptions import (
|
|
16
|
+
KafkaConnectionError,
|
|
17
|
+
KafkaConsumerError,
|
|
18
|
+
KafkaConsumerNotConnectedError,
|
|
19
|
+
KafkaProducerNotConnectedError,
|
|
20
|
+
)
|
|
21
|
+
from cledar.kafka.handlers.dead_letter import DeadLetterHandler
|
|
22
|
+
from cledar.kafka.handlers.parser import IncorrectMessageValueError, InputParser
|
|
23
|
+
from cledar.kafka.models.message import KafkaMessage
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.fixture
|
|
27
|
+
def producer_config() -> KafkaProducerConfig:
|
|
28
|
+
"""Create producer configuration for error tests."""
|
|
29
|
+
return KafkaProducerConfig(
|
|
30
|
+
kafka_servers="localhost:9092",
|
|
31
|
+
kafka_group_id="error-test-producer",
|
|
32
|
+
kafka_topic_prefix="error-test.",
|
|
33
|
+
kafka_block_buffer_time_sec=5,
|
|
34
|
+
kafka_connection_check_timeout_sec=1,
|
|
35
|
+
kafka_connection_check_interval_sec=30,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.fixture
|
|
40
|
+
def consumer_config() -> KafkaConsumerConfig:
|
|
41
|
+
"""Create consumer configuration for error tests."""
|
|
42
|
+
return KafkaConsumerConfig(
|
|
43
|
+
kafka_servers="localhost:9092",
|
|
44
|
+
kafka_group_id="error-test-consumer",
|
|
45
|
+
kafka_offset="earliest",
|
|
46
|
+
kafka_topic_prefix="error-test.",
|
|
47
|
+
kafka_block_consumer_time_sec=2,
|
|
48
|
+
kafka_connection_check_timeout_sec=1,
|
|
49
|
+
kafka_auto_commit_interval_ms=1000,
|
|
50
|
+
kafka_connection_check_interval_sec=30,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_producer_connection_error(producer_config: KafkaProducerConfig) -> None:
|
|
55
|
+
"""Test producer connection error handling."""
|
|
56
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
57
|
+
mock_prod_instance = MagicMock()
|
|
58
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
59
|
+
|
|
60
|
+
# Mock connection check to fail
|
|
61
|
+
mock_prod_instance.list_topics.side_effect = KafkaException("Connection failed")
|
|
62
|
+
|
|
63
|
+
producer = KafkaProducer(producer_config)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(KafkaConnectionError) as exc_info:
|
|
66
|
+
producer.connect()
|
|
67
|
+
|
|
68
|
+
assert isinstance(exc_info.value.__cause__, KafkaException)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_consumer_connection_error(consumer_config: KafkaConsumerConfig) -> None:
|
|
72
|
+
"""Test consumer connection error handling."""
|
|
73
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
74
|
+
mock_cons_instance = MagicMock()
|
|
75
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
76
|
+
|
|
77
|
+
# Mock connection check to fail
|
|
78
|
+
mock_cons_instance.list_topics.side_effect = KafkaException("Connection failed")
|
|
79
|
+
|
|
80
|
+
consumer = KafkaConsumer(consumer_config)
|
|
81
|
+
|
|
82
|
+
with pytest.raises(KafkaConnectionError) as exc_info:
|
|
83
|
+
consumer.connect()
|
|
84
|
+
|
|
85
|
+
assert isinstance(exc_info.value.__cause__, KafkaException)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_producer_send_not_connected_error(
|
|
89
|
+
producer_config: KafkaProducerConfig,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Test producer send when not connected."""
|
|
92
|
+
producer = KafkaProducer(producer_config)
|
|
93
|
+
|
|
94
|
+
with pytest.raises(KafkaProducerNotConnectedError):
|
|
95
|
+
producer.send(topic="test", value="test", key="key")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_consumer_consume_not_connected_error(
|
|
99
|
+
consumer_config: KafkaConsumerConfig,
|
|
100
|
+
) -> None:
|
|
101
|
+
"""Test consumer consume when not connected."""
|
|
102
|
+
consumer = KafkaConsumer(consumer_config)
|
|
103
|
+
|
|
104
|
+
with pytest.raises(KafkaConsumerNotConnectedError):
|
|
105
|
+
consumer.consume_next()
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_consumer_subscribe_not_connected_error(
|
|
109
|
+
consumer_config: KafkaConsumerConfig,
|
|
110
|
+
) -> None:
|
|
111
|
+
"""Test consumer subscribe when not connected."""
|
|
112
|
+
consumer = KafkaConsumer(consumer_config)
|
|
113
|
+
|
|
114
|
+
with pytest.raises(KafkaConsumerNotConnectedError):
|
|
115
|
+
consumer.subscribe(["test-topic"])
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_consumer_commit_not_connected_error(
|
|
119
|
+
consumer_config: KafkaConsumerConfig,
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Test consumer commit when not connected."""
|
|
122
|
+
consumer = KafkaConsumer(consumer_config)
|
|
123
|
+
|
|
124
|
+
message = KafkaMessage(
|
|
125
|
+
topic="test",
|
|
126
|
+
value="test",
|
|
127
|
+
key="key",
|
|
128
|
+
offset=100,
|
|
129
|
+
partition=0,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
with pytest.raises(KafkaConsumerNotConnectedError):
|
|
133
|
+
consumer.commit(message)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_producer_send_kafka_exception(producer_config: KafkaProducerConfig) -> None:
|
|
137
|
+
"""Test producer send with KafkaException."""
|
|
138
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
139
|
+
mock_prod_instance = MagicMock()
|
|
140
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
141
|
+
|
|
142
|
+
# Mock connection check to succeed
|
|
143
|
+
mock_prod_instance.list_topics.return_value = None
|
|
144
|
+
|
|
145
|
+
producer = KafkaProducer(producer_config)
|
|
146
|
+
producer.connect()
|
|
147
|
+
|
|
148
|
+
# Mock produce to raise KafkaException
|
|
149
|
+
mock_prod_instance.produce.side_effect = KafkaException("Produce failed")
|
|
150
|
+
|
|
151
|
+
with pytest.raises(KafkaException):
|
|
152
|
+
producer.send(topic="test", value="test", key="key")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def test_consumer_consume_kafka_exception(consumer_config: KafkaConsumerConfig) -> None:
|
|
156
|
+
"""Test consumer consume with KafkaException."""
|
|
157
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
158
|
+
mock_cons_instance = MagicMock()
|
|
159
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
160
|
+
|
|
161
|
+
# Mock connection check to succeed
|
|
162
|
+
mock_cons_instance.list_topics.return_value = None
|
|
163
|
+
|
|
164
|
+
consumer = KafkaConsumer(consumer_config)
|
|
165
|
+
consumer.connect()
|
|
166
|
+
|
|
167
|
+
# Mock poll to raise KafkaException
|
|
168
|
+
mock_cons_instance.poll.side_effect = KafkaException("Poll failed")
|
|
169
|
+
|
|
170
|
+
with pytest.raises(KafkaException):
|
|
171
|
+
consumer.consume_next()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def test_consumer_subscribe_kafka_exception(
|
|
175
|
+
consumer_config: KafkaConsumerConfig,
|
|
176
|
+
) -> None:
|
|
177
|
+
"""Test consumer subscribe with KafkaException."""
|
|
178
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
179
|
+
mock_cons_instance = MagicMock()
|
|
180
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
181
|
+
|
|
182
|
+
# Mock connection check to succeed
|
|
183
|
+
mock_cons_instance.list_topics.return_value = None
|
|
184
|
+
|
|
185
|
+
consumer = KafkaConsumer(consumer_config)
|
|
186
|
+
consumer.connect()
|
|
187
|
+
|
|
188
|
+
# Mock subscribe to raise KafkaException
|
|
189
|
+
mock_cons_instance.subscribe.side_effect = KafkaException("Subscribe failed")
|
|
190
|
+
|
|
191
|
+
with pytest.raises(KafkaException):
|
|
192
|
+
consumer.subscribe(["test-topic"])
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_consumer_commit_kafka_exception(consumer_config: KafkaConsumerConfig) -> None:
|
|
196
|
+
"""Test consumer commit with KafkaException."""
|
|
197
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
198
|
+
mock_cons_instance = MagicMock()
|
|
199
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
200
|
+
|
|
201
|
+
# Mock connection check to succeed
|
|
202
|
+
mock_cons_instance.list_topics.return_value = None
|
|
203
|
+
|
|
204
|
+
consumer = KafkaConsumer(consumer_config)
|
|
205
|
+
consumer.connect()
|
|
206
|
+
|
|
207
|
+
# Mock commit to raise KafkaException
|
|
208
|
+
mock_cons_instance.commit.side_effect = KafkaException("Commit failed")
|
|
209
|
+
|
|
210
|
+
message = KafkaMessage(
|
|
211
|
+
topic="test",
|
|
212
|
+
value="test",
|
|
213
|
+
key="key",
|
|
214
|
+
offset=100,
|
|
215
|
+
partition=0,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
with pytest.raises(KafkaException):
|
|
219
|
+
consumer.commit(message)
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def test_producer_buffer_error_retry(producer_config: KafkaProducerConfig) -> None:
|
|
223
|
+
"""Test producer buffer error retry mechanism."""
|
|
224
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
225
|
+
mock_prod_instance = MagicMock()
|
|
226
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
227
|
+
|
|
228
|
+
# Mock connection check to succeed
|
|
229
|
+
mock_prod_instance.list_topics.return_value = None
|
|
230
|
+
|
|
231
|
+
producer = KafkaProducer(producer_config)
|
|
232
|
+
producer.connect()
|
|
233
|
+
|
|
234
|
+
# Mock produce to raise BufferError first, then succeed
|
|
235
|
+
mock_prod_instance.produce.side_effect = [
|
|
236
|
+
BufferError("Buffer full"),
|
|
237
|
+
None, # Success on retry
|
|
238
|
+
]
|
|
239
|
+
|
|
240
|
+
# Should not raise exception due to retry mechanism
|
|
241
|
+
producer.send(topic="test", value="test", key="key")
|
|
242
|
+
|
|
243
|
+
# Verify produce was called twice (retry)
|
|
244
|
+
assert mock_prod_instance.produce.call_count == 2
|
|
245
|
+
assert (
|
|
246
|
+
mock_prod_instance.poll.call_count == 1
|
|
247
|
+
) # Only called once in retry block
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_producer_buffer_error_retry_failure(
|
|
251
|
+
producer_config: KafkaProducerConfig,
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Test producer buffer error retry mechanism with persistent failure."""
|
|
254
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
255
|
+
mock_prod_instance = MagicMock()
|
|
256
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
257
|
+
|
|
258
|
+
# Mock connection check to succeed
|
|
259
|
+
mock_prod_instance.list_topics.return_value = None
|
|
260
|
+
|
|
261
|
+
producer = KafkaProducer(producer_config)
|
|
262
|
+
producer.connect()
|
|
263
|
+
|
|
264
|
+
# Mock produce to always raise BufferError
|
|
265
|
+
mock_prod_instance.produce.side_effect = BufferError("Buffer full")
|
|
266
|
+
|
|
267
|
+
# Should raise BufferError after retry
|
|
268
|
+
with pytest.raises(BufferError):
|
|
269
|
+
producer.send(topic="test", value="test", key="key")
|
|
270
|
+
|
|
271
|
+
# Verify produce was called twice (retry)
|
|
272
|
+
assert mock_prod_instance.produce.call_count == 2
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def test_consumer_message_error(consumer_config: KafkaConsumerConfig) -> None:
|
|
276
|
+
"""Test consumer message error handling."""
|
|
277
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
278
|
+
mock_cons_instance = MagicMock()
|
|
279
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
280
|
+
|
|
281
|
+
# Mock connection check to succeed
|
|
282
|
+
mock_cons_instance.list_topics.return_value = None
|
|
283
|
+
|
|
284
|
+
consumer = KafkaConsumer(consumer_config)
|
|
285
|
+
consumer.connect()
|
|
286
|
+
|
|
287
|
+
# Mock poll to return message with error
|
|
288
|
+
mock_message = MagicMock()
|
|
289
|
+
mock_message.error.return_value = KafkaError(1, "Message error")
|
|
290
|
+
mock_cons_instance.poll.return_value = mock_message
|
|
291
|
+
|
|
292
|
+
with pytest.raises(KafkaConsumerError) as exc_info:
|
|
293
|
+
consumer.consume_next()
|
|
294
|
+
|
|
295
|
+
assert isinstance(exc_info.value.args[0], KafkaError)
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_dead_letter_handler_producer_error(
|
|
299
|
+
producer_config: KafkaProducerConfig,
|
|
300
|
+
) -> None:
|
|
301
|
+
"""Test DeadLetterHandler with producer error."""
|
|
302
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
303
|
+
mock_prod_instance = MagicMock()
|
|
304
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
305
|
+
|
|
306
|
+
# Mock connection check to succeed
|
|
307
|
+
mock_prod_instance.list_topics.return_value = None
|
|
308
|
+
|
|
309
|
+
producer = KafkaProducer(producer_config)
|
|
310
|
+
producer.connect()
|
|
311
|
+
|
|
312
|
+
# Mock send to raise KafkaException
|
|
313
|
+
mock_prod_instance.produce.side_effect = KafkaException("DLQ send failed")
|
|
314
|
+
|
|
315
|
+
dlq_handler = DeadLetterHandler(producer=producer, dlq_topic="dlq-topic")
|
|
316
|
+
|
|
317
|
+
message = KafkaMessage(
|
|
318
|
+
topic="test",
|
|
319
|
+
value="test",
|
|
320
|
+
key="key",
|
|
321
|
+
offset=100,
|
|
322
|
+
partition=0,
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
with pytest.raises(KafkaException):
|
|
326
|
+
dlq_handler.handle(message, None)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def test_input_parser_incorrect_message_value_error() -> None:
|
|
330
|
+
"""Test InputParser with incorrect message value."""
|
|
331
|
+
from pydantic import BaseModel
|
|
332
|
+
|
|
333
|
+
class SimpleModel(BaseModel):
|
|
334
|
+
id: str
|
|
335
|
+
|
|
336
|
+
parser = InputParser(SimpleModel)
|
|
337
|
+
|
|
338
|
+
message = KafkaMessage(
|
|
339
|
+
topic="test",
|
|
340
|
+
value=None, # None value should cause error
|
|
341
|
+
key="key",
|
|
342
|
+
offset=100,
|
|
343
|
+
partition=0,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
with pytest.raises(IncorrectMessageValueError):
|
|
347
|
+
parser.parse_message(message)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def test_input_parser_json_validation_error() -> None:
|
|
351
|
+
"""Test InputParser with JSON validation error."""
|
|
352
|
+
from pydantic import BaseModel
|
|
353
|
+
|
|
354
|
+
class TestModel(BaseModel):
|
|
355
|
+
id: str
|
|
356
|
+
value: int
|
|
357
|
+
|
|
358
|
+
parser = InputParser(TestModel)
|
|
359
|
+
|
|
360
|
+
message = KafkaMessage(
|
|
361
|
+
topic="test",
|
|
362
|
+
value='{"id": "123", "value": "not-a-number"}', # Invalid type
|
|
363
|
+
key="key",
|
|
364
|
+
offset=100,
|
|
365
|
+
partition=0,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
from pydantic import ValidationError
|
|
369
|
+
|
|
370
|
+
with pytest.raises(ValidationError): # Pydantic validation error
|
|
371
|
+
parser.parse_message(message)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def test_input_parser_invalid_json_error() -> None:
|
|
375
|
+
"""Test InputParser with invalid JSON."""
|
|
376
|
+
from pydantic import BaseModel
|
|
377
|
+
|
|
378
|
+
class TestModel(BaseModel):
|
|
379
|
+
id: str
|
|
380
|
+
value: int
|
|
381
|
+
|
|
382
|
+
parser = InputParser(TestModel)
|
|
383
|
+
|
|
384
|
+
message = KafkaMessage(
|
|
385
|
+
topic="test",
|
|
386
|
+
value='{"id": "123", "value": 42', # Invalid JSON
|
|
387
|
+
key="key",
|
|
388
|
+
offset=100,
|
|
389
|
+
partition=0,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
with pytest.raises(IncorrectMessageValueError): # JSON parsing error
|
|
393
|
+
parser.parse_message(message)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
def test_connection_monitoring_error_handling(
|
|
397
|
+
producer_config: KafkaProducerConfig,
|
|
398
|
+
) -> None:
|
|
399
|
+
"""Test connection monitoring error handling."""
|
|
400
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
401
|
+
mock_prod_instance = MagicMock()
|
|
402
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
403
|
+
|
|
404
|
+
# Mock connection check to succeed initially
|
|
405
|
+
mock_prod_instance.list_topics.return_value = None
|
|
406
|
+
|
|
407
|
+
producer = KafkaProducer(producer_config)
|
|
408
|
+
producer.connect()
|
|
409
|
+
|
|
410
|
+
# Mock connection check to fail during monitoring
|
|
411
|
+
mock_prod_instance.list_topics.side_effect = KafkaException("Connection lost")
|
|
412
|
+
|
|
413
|
+
# Start monitoring thread
|
|
414
|
+
producer.start_connection_check_thread()
|
|
415
|
+
|
|
416
|
+
# Let it run for a short time
|
|
417
|
+
time.sleep(0.1)
|
|
418
|
+
|
|
419
|
+
# Stop monitoring
|
|
420
|
+
producer.shutdown()
|
|
421
|
+
|
|
422
|
+
# Should not raise exception, should handle gracefully
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
def test_connection_monitoring_success_recovery(
|
|
426
|
+
producer_config: KafkaProducerConfig,
|
|
427
|
+
) -> None:
|
|
428
|
+
"""Test connection monitoring success recovery."""
|
|
429
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
430
|
+
mock_prod_instance = MagicMock()
|
|
431
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
432
|
+
|
|
433
|
+
# Mock connection check to succeed
|
|
434
|
+
mock_prod_instance.list_topics.return_value = None
|
|
435
|
+
|
|
436
|
+
producer = KafkaProducer(producer_config)
|
|
437
|
+
producer.connect()
|
|
438
|
+
|
|
439
|
+
# Start monitoring thread
|
|
440
|
+
producer.start_connection_check_thread()
|
|
441
|
+
|
|
442
|
+
# Let it run for a short time
|
|
443
|
+
time.sleep(0.1)
|
|
444
|
+
|
|
445
|
+
# Stop monitoring
|
|
446
|
+
producer.shutdown()
|
|
447
|
+
|
|
448
|
+
# Should not raise exception, should handle gracefully
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def test_producer_send_with_headers_error(producer_config: KafkaProducerConfig) -> None:
|
|
452
|
+
"""Test producer send with headers error handling."""
|
|
453
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
454
|
+
mock_prod_instance = MagicMock()
|
|
455
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
456
|
+
|
|
457
|
+
# Mock connection check to succeed
|
|
458
|
+
mock_prod_instance.list_topics.return_value = None
|
|
459
|
+
|
|
460
|
+
producer = KafkaProducer(producer_config)
|
|
461
|
+
producer.connect()
|
|
462
|
+
|
|
463
|
+
# Mock produce to raise KafkaException
|
|
464
|
+
mock_prod_instance.produce.side_effect = KafkaException("Headers error")
|
|
465
|
+
|
|
466
|
+
headers = [("header1", b"value1"), ("header2", b"value2")]
|
|
467
|
+
|
|
468
|
+
with pytest.raises(KafkaException):
|
|
469
|
+
producer.send(topic="test", value="test", key="key", headers=headers)
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_consumer_consume_with_timeout(consumer_config: KafkaConsumerConfig) -> None:
|
|
473
|
+
"""Test consumer consume with timeout."""
|
|
474
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
475
|
+
mock_cons_instance = MagicMock()
|
|
476
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
477
|
+
|
|
478
|
+
# Mock connection check to succeed
|
|
479
|
+
mock_cons_instance.list_topics.return_value = None
|
|
480
|
+
|
|
481
|
+
consumer = KafkaConsumer(consumer_config)
|
|
482
|
+
consumer.connect()
|
|
483
|
+
|
|
484
|
+
# Mock poll to return None (timeout)
|
|
485
|
+
mock_cons_instance.poll.return_value = None
|
|
486
|
+
|
|
487
|
+
message = consumer.consume_next()
|
|
488
|
+
|
|
489
|
+
assert message is None
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
def test_consumer_consume_with_partial_message(
|
|
493
|
+
consumer_config: KafkaConsumerConfig,
|
|
494
|
+
) -> None:
|
|
495
|
+
"""Test consumer consume with partial message data."""
|
|
496
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
497
|
+
mock_cons_instance = MagicMock()
|
|
498
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
499
|
+
|
|
500
|
+
# Mock connection check to succeed
|
|
501
|
+
mock_cons_instance.list_topics.return_value = None
|
|
502
|
+
|
|
503
|
+
consumer = KafkaConsumer(consumer_config)
|
|
504
|
+
consumer.connect()
|
|
505
|
+
|
|
506
|
+
# Mock poll to return message with partial data
|
|
507
|
+
mock_message = MagicMock()
|
|
508
|
+
mock_message.error.return_value = None
|
|
509
|
+
mock_message.topic.return_value = "test-topic"
|
|
510
|
+
mock_message.value.return_value = b"test-value"
|
|
511
|
+
mock_message.key.return_value = None # None key
|
|
512
|
+
mock_message.offset.return_value = 100
|
|
513
|
+
mock_message.partition.return_value = 0
|
|
514
|
+
|
|
515
|
+
mock_cons_instance.poll.return_value = mock_message
|
|
516
|
+
|
|
517
|
+
message = consumer.consume_next()
|
|
518
|
+
|
|
519
|
+
assert message is not None
|
|
520
|
+
assert message.value == "test-value"
|
|
521
|
+
assert message.key is None
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def test_producer_send_with_none_values(producer_config: KafkaProducerConfig) -> None:
|
|
525
|
+
"""Test producer send with None values."""
|
|
526
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
527
|
+
mock_prod_instance = MagicMock()
|
|
528
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
529
|
+
|
|
530
|
+
# Mock connection check to succeed
|
|
531
|
+
mock_prod_instance.list_topics.return_value = None
|
|
532
|
+
|
|
533
|
+
producer = KafkaProducer(producer_config)
|
|
534
|
+
producer.connect()
|
|
535
|
+
|
|
536
|
+
# Should not raise exception with None values
|
|
537
|
+
producer.send(topic="test", value=None, key=None)
|
|
538
|
+
|
|
539
|
+
# Verify produce was called with None values
|
|
540
|
+
mock_prod_instance.produce.assert_called_once()
|
|
541
|
+
call_args = mock_prod_instance.produce.call_args
|
|
542
|
+
assert call_args[1]["value"] is None
|
|
543
|
+
assert call_args[1]["key"] is None
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
def test_consumer_commit_with_none_message(
|
|
547
|
+
consumer_config: KafkaConsumerConfig,
|
|
548
|
+
) -> None:
|
|
549
|
+
"""Test consumer commit with None message."""
|
|
550
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
551
|
+
mock_cons_instance = MagicMock()
|
|
552
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
553
|
+
|
|
554
|
+
# Mock connection check to succeed
|
|
555
|
+
mock_cons_instance.list_topics.return_value = None
|
|
556
|
+
|
|
557
|
+
consumer = KafkaConsumer(consumer_config)
|
|
558
|
+
consumer.connect()
|
|
559
|
+
|
|
560
|
+
# Mock poll to return None
|
|
561
|
+
mock_cons_instance.poll.return_value = None
|
|
562
|
+
|
|
563
|
+
message = consumer.consume_next()
|
|
564
|
+
|
|
565
|
+
assert message is None
|
|
566
|
+
|
|
567
|
+
# Should not be able to commit None message
|
|
568
|
+
# (This would be caught by type checking in real usage)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def test_producer_send_with_empty_strings(producer_config: KafkaProducerConfig) -> None:
|
|
572
|
+
"""Test producer send with empty strings."""
|
|
573
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
574
|
+
mock_prod_instance = MagicMock()
|
|
575
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
576
|
+
|
|
577
|
+
# Mock connection check to succeed
|
|
578
|
+
mock_prod_instance.list_topics.return_value = None
|
|
579
|
+
|
|
580
|
+
producer = KafkaProducer(producer_config)
|
|
581
|
+
producer.connect()
|
|
582
|
+
|
|
583
|
+
# Should not raise exception with empty strings
|
|
584
|
+
producer.send(topic="", value="", key="")
|
|
585
|
+
|
|
586
|
+
# Verify produce was called with empty strings
|
|
587
|
+
mock_prod_instance.produce.assert_called_once()
|
|
588
|
+
call_args = mock_prod_instance.produce.call_args
|
|
589
|
+
assert call_args[1]["value"] == ""
|
|
590
|
+
assert call_args[1]["key"] == ""
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
def test_consumer_subscribe_with_empty_topic_list(
|
|
594
|
+
consumer_config: KafkaConsumerConfig,
|
|
595
|
+
) -> None:
|
|
596
|
+
"""Test consumer subscribe with empty topic list."""
|
|
597
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
598
|
+
mock_cons_instance = MagicMock()
|
|
599
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
600
|
+
|
|
601
|
+
# Mock connection check to succeed
|
|
602
|
+
mock_cons_instance.list_topics.return_value = None
|
|
603
|
+
|
|
604
|
+
consumer = KafkaConsumer(consumer_config)
|
|
605
|
+
consumer.connect()
|
|
606
|
+
|
|
607
|
+
# Should not raise exception with empty topic list
|
|
608
|
+
consumer.subscribe([])
|
|
609
|
+
|
|
610
|
+
# Verify subscribe was called with empty list
|
|
611
|
+
mock_cons_instance.subscribe.assert_called_once_with([])
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def test_producer_send_with_special_characters(
|
|
615
|
+
producer_config: KafkaProducerConfig,
|
|
616
|
+
) -> None:
|
|
617
|
+
"""Test producer send with special characters."""
|
|
618
|
+
with patch("cledar.kafka.clients.producer.Producer") as mock_prod_class:
|
|
619
|
+
mock_prod_instance = MagicMock()
|
|
620
|
+
mock_prod_class.return_value = mock_prod_instance
|
|
621
|
+
|
|
622
|
+
# Mock connection check to succeed
|
|
623
|
+
mock_prod_instance.list_topics.return_value = None
|
|
624
|
+
|
|
625
|
+
producer = KafkaProducer(producer_config)
|
|
626
|
+
producer.connect()
|
|
627
|
+
|
|
628
|
+
special_topic = "topic-with-special-chars: @#$%^&*()"
|
|
629
|
+
special_value = "value-with-special-chars: @#$%^&*()"
|
|
630
|
+
special_key = "key-with-special-chars: @#$%^&*()"
|
|
631
|
+
|
|
632
|
+
# Should not raise exception with special characters
|
|
633
|
+
producer.send(topic=special_topic, value=special_value, key=special_key)
|
|
634
|
+
|
|
635
|
+
# Verify produce was called with special characters
|
|
636
|
+
mock_prod_instance.produce.assert_called_once()
|
|
637
|
+
call_args = mock_prod_instance.produce.call_args
|
|
638
|
+
assert call_args[1]["value"] == special_value
|
|
639
|
+
assert call_args[1]["key"] == special_key
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
def test_consumer_consume_with_unicode_characters(
|
|
643
|
+
consumer_config: KafkaConsumerConfig,
|
|
644
|
+
) -> None:
|
|
645
|
+
"""Test consumer consume with unicode characters."""
|
|
646
|
+
with patch("cledar.kafka.clients.consumer.Consumer") as mock_cons_class:
|
|
647
|
+
mock_cons_instance = MagicMock()
|
|
648
|
+
mock_cons_class.return_value = mock_cons_instance
|
|
649
|
+
|
|
650
|
+
# Mock connection check to succeed
|
|
651
|
+
mock_cons_instance.list_topics.return_value = None
|
|
652
|
+
|
|
653
|
+
consumer = KafkaConsumer(consumer_config)
|
|
654
|
+
consumer.connect()
|
|
655
|
+
|
|
656
|
+
# Mock poll to return message with unicode
|
|
657
|
+
unicode_value = "测试消息"
|
|
658
|
+
unicode_key = "测试键"
|
|
659
|
+
|
|
660
|
+
mock_message = MagicMock()
|
|
661
|
+
mock_message.error.return_value = None
|
|
662
|
+
mock_message.topic.return_value = "test-topic"
|
|
663
|
+
mock_message.value.return_value = unicode_value.encode("utf-8")
|
|
664
|
+
mock_message.key.return_value = unicode_key.encode("utf-8")
|
|
665
|
+
mock_message.offset.return_value = 100
|
|
666
|
+
mock_message.partition.return_value = 0
|
|
667
|
+
|
|
668
|
+
mock_cons_instance.poll.return_value = mock_message
|
|
669
|
+
|
|
670
|
+
message = consumer.consume_next()
|
|
671
|
+
|
|
672
|
+
assert message is not None
|
|
673
|
+
assert message.value == unicode_value
|
|
674
|
+
assert message.key == unicode_key
|