Skip to content

Commit e0b1e71

Browse files
committed
chore: extend test coverage
Signed-off-by: Manish Dait <daitmanish88@gmail.com>
1 parent 2418b80 commit e0b1e71

2 files changed

Lines changed: 187 additions & 4 deletions

File tree

src/hiero_sdk_python/query/topic_message_query.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,11 +79,17 @@ def set_max_backoff(self, backoff: float) -> TopicMessageQuery:
7979

8080
def set_completion_handler(self, handler: Callable[[], None]) -> TopicMessageQuery:
8181
"""Sets a completion handler that is called when the subscription completes."""
82+
if not callable(handler):
83+
raise TypeError("handler must be a callable object")
84+
8285
self._completion_handler = handler
8386
return self
8487

8588
def set_error_handler(self, handler: Callable[[Exception], None]) -> TopicMessageQuery:
8689
"""Sets an error handler that is called when the subscription causes an error."""
90+
if not callable(handler):
91+
raise TypeError("handler must be a callable object")
92+
8793
self._error_handler = handler
8894
return self
8995

tests/unit/topic_message_query_test.py

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from hiero_sdk_python.hapi.mirror import consensus_service_pb2 as mirror_proto
1414
from hiero_sdk_python.hapi.services import timestamp_pb2 as hapi_timestamp_pb2
1515
from hiero_sdk_python.hapi.services.consensus_submit_message_pb2 import ConsensusMessageChunkInfo
16-
from hiero_sdk_python.query.topic_message_query import TopicMessageQuery
16+
from hiero_sdk_python.query.topic_message_query import SubscriptionState, TopicMessageQuery
1717
from hiero_sdk_python.transaction.transaction_id import TransactionId
1818
from tests.unit.mock_server import RealRpcError
1919

@@ -48,15 +48,35 @@ def mock_subscription_response():
4848
)
4949

5050

51+
# Initialization
52+
53+
5154
def test_topic_message_query_initialization():
5255
"""Test initializing the query with various parameter types and setters."""
5356
start = datetime(2023, 1, 1, tzinfo=timezone.utc)
54-
query = TopicMessageQuery().set_topic_id("0.0.123").set_start_time(start).set_limit(5).set_chunking_enabled(True)
57+
58+
def mock_complete():
59+
pass
60+
61+
def mock_error(e):
62+
pass
63+
64+
query = (
65+
TopicMessageQuery()
66+
.set_topic_id("0.0.123")
67+
.set_start_time(start)
68+
.set_limit(5)
69+
.set_chunking_enabled(True)
70+
.set_completion_handler(mock_complete)
71+
.set_error_handler(mock_error)
72+
)
5573

5674
assert query._topic_id.topicNum == 123
5775
assert query._start_time.seconds == int(start.timestamp())
5876
assert query._limit == 5
5977
assert query._chunking_enabled is True
78+
assert query._completion_handler == mock_complete
79+
assert query._error_handler == mock_error
6080

6181

6282
def test_topic_message_query_invalid_max_backoff():
@@ -102,6 +122,138 @@ def test_subscribe_missing_config(mock_client):
102122
query_ok.subscribe(mock_client, on_message=MagicMock())
103123

104124

125+
@pytest.mark.parametrize(
126+
"handler",
127+
["string", 1, True, None, [], {}],
128+
)
129+
def test_topic_message_query_invalid_handler_param(handler):
130+
"""Test that a non-callable handler raises a TypeError."""
131+
query = TopicMessageQuery()
132+
133+
# For complete_handler
134+
with pytest.raises(TypeError, match="handler must be a callable object"):
135+
query.set_completion_handler(handler)
136+
# For error_handler
137+
with pytest.raises(TypeError, match="handler must be a callable object"):
138+
query.set_error_handler(handler)
139+
140+
141+
# build_query_request
142+
143+
144+
def test_build_query_request_uses_provided_start_time():
145+
"""Test that the request uses the provided start_time when no last_message exists."""
146+
start = datetime(2023, 1, 1, tzinfo=timezone.utc)
147+
query = TopicMessageQuery(topic_id="0.0.123").set_start_time(start)
148+
state = SubscriptionState()
149+
150+
state.last_message = None
151+
152+
expected_start = query._start_time
153+
request = query._build_query_request(state)
154+
155+
assert request.consensusStartTime.seconds == expected_start.seconds
156+
assert request.consensusStartTime.nanos == expected_start.nanos
157+
158+
159+
def test_build_query_request_from_last_message_timestamp():
160+
"""Test that a reconnection request overrides start_time using the last message timestamp + 1 nano"""
161+
start = datetime(2023, 1, 1, tzinfo=timezone.utc)
162+
query = TopicMessageQuery(topic_id="0.0.123").set_start_time(start)
163+
state = SubscriptionState()
164+
165+
state.last_message = state.last_message = mirror_proto.ConsensusTopicResponse(
166+
consensusTimestamp=hapi_timestamp_pb2.Timestamp(seconds=50, nanos=10)
167+
)
168+
169+
request = query._build_query_request(state)
170+
171+
assert request.consensusStartTime.seconds == 50
172+
assert request.consensusStartTime.nanos == 11
173+
174+
175+
def test_build_query_request_nanosecond_rollover():
176+
"""Test that nanos reaching 1_000_000_000 correctly increments the seconds field."""
177+
query = TopicMessageQuery(topic_id="0.0.123")
178+
state = SubscriptionState()
179+
180+
# Mock message arriving at the 999_999_999 nanosecond of second 50
181+
state.last_message = mirror_proto.ConsensusTopicResponse(
182+
consensusTimestamp=hapi_timestamp_pb2.Timestamp(seconds=50, nanos=999_999_999)
183+
)
184+
185+
request = query._build_query_request(state)
186+
187+
assert request.consensusStartTime.seconds == 51
188+
assert request.consensusStartTime.nanos == 0
189+
190+
191+
def test_build_query_request_limit_decrements_on_retry():
192+
"""Test that retry requests ask only for the remaining messages within the limit."""
193+
query = TopicMessageQuery(topic_id="0.0.123").set_limit(10)
194+
state = SubscriptionState()
195+
196+
# Mock state already have collect 4 message
197+
state.count = 4
198+
state.last_message = mirror_proto.ConsensusTopicResponse(
199+
consensusTimestamp=hapi_timestamp_pb2.Timestamp(seconds=100, nanos=500)
200+
)
201+
202+
request = query._build_query_request(state)
203+
204+
# New request only ask for the remaining 6 message (10 - 4)
205+
assert request.limit == 6
206+
207+
208+
def test_build_query_request_limit_floor_at_zero():
209+
"""Test that remaining request limits never drop below zero."""
210+
query = TopicMessageQuery(topic_id="0.0.123").set_limit(5)
211+
state = SubscriptionState()
212+
213+
state.count = 6
214+
state.last_message = mirror_proto.ConsensusTopicResponse(
215+
consensusTimestamp=hapi_timestamp_pb2.Timestamp(seconds=100, nanos=500)
216+
)
217+
218+
request = query._build_query_request(state)
219+
220+
# The limit should be 0 rather than becoming negative
221+
assert request.limit == 0
222+
223+
224+
def test_build_query_request_set_end_time_if_provided():
225+
"""Test that request is created with the end_time if provided."""
226+
end = datetime(2023, 1, 1, tzinfo=timezone.utc)
227+
query = TopicMessageQuery(topic_id="0.0.123").set_end_time(end)
228+
229+
state = SubscriptionState()
230+
state.last_message = None
231+
232+
expected_end = query._end_time
233+
request = query._build_query_request(state)
234+
235+
assert request.consensusEndTime.seconds == expected_end.seconds
236+
assert request.consensusEndTime.nanos == expected_end.nanos
237+
238+
239+
def test_build_query_request_set_start_end_time_to_default():
240+
"""Test that request is created with start_time and end_time as None if not present."""
241+
query = TopicMessageQuery(topic_id="0.0.123")
242+
state = SubscriptionState()
243+
state.last_message = None
244+
245+
request = query._build_query_request(state)
246+
247+
assert request.consensusStartTime.seconds == 0
248+
assert request.consensusStartTime.nanos == 0
249+
250+
assert request.consensusEndTime.seconds == 0
251+
assert request.consensusEndTime.nanos == 0
252+
253+
254+
# handle_response / subscribe
255+
256+
105257
def test_topic_message_query_subscription(mock_client, mock_topic_id, mock_subscription_response):
106258
"""Test subscribing to topic messages using TopicMessageQuery."""
107259
query = TopicMessageQuery().set_topic_id(mock_topic_id).set_start_time(datetime.now(tz=timezone.utc))
@@ -128,7 +280,7 @@ def side_effect(client, on_message, on_error): # noqa: ARG001
128280
on_error.assert_not_called()
129281

130282

131-
def test_chunk_message_handling_improved(mock_client):
283+
def test_chunk_message_handling(mock_client):
132284
"""Test that multiple chunks are correctly buffered and released as a single message."""
133285
query = TopicMessageQuery(topic_id="0.0.123", chunking_enabled=True)
134286

@@ -154,6 +306,32 @@ def test_chunk_message_handling_improved(mock_client):
154306
assert b"chunk-2" in received_messages[0].contents
155307

156308

309+
def test_chunk_message_handling_when_chunking_is_disabled(mock_client):
310+
"""Test that when chunking is disabled only single chunk is released as a single message."""
311+
query = TopicMessageQuery(topic_id="0.0.123", chunking_enabled=False)
312+
313+
tx_id = TransactionId.generate(mock_client.operator_account_id)
314+
tx_id_proto = tx_id._to_proto()
315+
316+
chunk1 = mirror_proto.ConsensusTopicResponse(
317+
message=b"chunk-1", chunkInfo=ConsensusMessageChunkInfo(initialTransactionID=tx_id_proto, total=2, number=1)
318+
)
319+
chunk2 = mirror_proto.ConsensusTopicResponse(
320+
message=b"chunk-2", chunkInfo=ConsensusMessageChunkInfo(initialTransactionID=tx_id_proto, total=2, number=2)
321+
)
322+
323+
mock_client.mirror_stub.subscribeTopic.return_value = iter([chunk1, chunk2])
324+
325+
received_messages = []
326+
handle = query.subscribe(mock_client, on_message=lambda m: received_messages.append(m))
327+
328+
handle._thread.join(timeout=1.0)
329+
330+
assert len(received_messages) == 2 # since we will get 2 seperate message
331+
assert b"chunk-1" in received_messages[0].contents
332+
assert b"chunk-2" not in received_messages[0].contents
333+
334+
157335
@pytest.mark.parametrize(
158336
"error",
159337
[
@@ -169,7 +347,6 @@ def test_retry_logic_on_retryable_error(mock_client, error):
169347
query = TopicMessageQuery(topic_id="0.0.123").set_max_attempts(2).set_max_backoff(0.5)
170348

171349
mock_client.mirror_stub.subscribeTopic.side_effect = [error, error]
172-
173350
handle = query.subscribe(mock_client, on_message=MagicMock(), on_error=MagicMock())
174351

175352
handle._thread.join(timeout=2.0)

0 commit comments

Comments
 (0)