Skip to content

Commit 73a2683

Browse files
committed
test: improve Redis session test coverage from 82% to 87%
Add comprehensive tests for previously uncovered code paths: - test_get_next_id_method: Test atomic counter functionality for message IDs - test_corrupted_data_handling: Test graceful handling of malformed JSON data - test_ping_connection_failure: Test network failure scenarios with mocked exceptions - test_close_method_coverage: Test client ownership edge cases in close() method
1 parent 05d8681 commit 73a2683

File tree

1 file changed

+153
-0
lines changed

1 file changed

+153
-0
lines changed

tests/extensions/memory/test_redis_session.py

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -628,3 +628,156 @@ async def test_real_redis_decode_responses_compatibility():
628628
except Exception:
629629
pass
630630
await session.close()
631+
632+
633+
async def test_get_next_id_method():
634+
"""Test the _get_next_id atomic counter functionality."""
635+
session = await _create_test_session("counter_test")
636+
637+
try:
638+
await session.clear_session()
639+
640+
# Test atomic counter increment
641+
id1 = await session._get_next_id()
642+
id2 = await session._get_next_id()
643+
id3 = await session._get_next_id()
644+
645+
# IDs should be sequential
646+
assert id1 == 1
647+
assert id2 == 2
648+
assert id3 == 3
649+
650+
# Test that counter persists across session instances with same session_id
651+
if USE_FAKE_REDIS:
652+
session2 = RedisSession(
653+
session_id="counter_test",
654+
redis_client=fake_redis,
655+
key_prefix="test:",
656+
)
657+
else:
658+
session2 = RedisSession.from_url("counter_test", url=REDIS_URL, key_prefix="test:")
659+
660+
try:
661+
id4 = await session2._get_next_id()
662+
assert id4 == 4 # Should continue from previous session's counter
663+
finally:
664+
await session2.close()
665+
666+
finally:
667+
await session.close()
668+
669+
670+
async def test_corrupted_data_handling():
671+
"""Test that corrupted JSON data is handled gracefully."""
672+
if not USE_FAKE_REDIS:
673+
pytest.skip("This test requires fakeredis for direct data manipulation")
674+
675+
session = await _create_test_session("corruption_test")
676+
677+
try:
678+
await session.clear_session()
679+
680+
# Add some valid data first
681+
await session.add_items([{"role": "user", "content": "valid message"}])
682+
683+
# Inject corrupted data directly into Redis
684+
messages_key = "test:corruption_test:messages"
685+
686+
# Add invalid JSON
687+
await fake_redis.rpush(messages_key, "invalid json data")
688+
await fake_redis.rpush(messages_key, "{incomplete json")
689+
690+
# get_items should skip corrupted data and return valid items
691+
items = await session.get_items()
692+
assert len(items) == 1 # Only the original valid item
693+
694+
# Now add a properly formatted valid item using the session's serialization
695+
valid_item = {"role": "user", "content": "valid after corruption"}
696+
await session.add_items([valid_item])
697+
698+
# Should now have 2 valid items (corrupted ones skipped)
699+
items = await session.get_items()
700+
assert len(items) == 2
701+
assert items[0]["content"] == "valid message"
702+
assert items[1]["content"] == "valid after corruption"
703+
704+
# Test pop_item with corrupted data at the end
705+
await fake_redis.rpush(messages_key, "corrupted at end")
706+
707+
# The corrupted item should be handled gracefully
708+
# Since it's at the end, pop_item will encounter it first and return None
709+
# But first, let's pop the valid items to get to the corrupted one
710+
popped1 = await session.pop_item()
711+
assert popped1 is not None
712+
assert popped1["content"] == "valid after corruption"
713+
714+
popped2 = await session.pop_item()
715+
assert popped2 is not None
716+
assert popped2["content"] == "valid message"
717+
718+
# Now we should hit the corrupted data - this should gracefully handle it
719+
# by returning None (and removing the corrupted item)
720+
popped_corrupted = await session.pop_item()
721+
assert popped_corrupted is None
722+
723+
finally:
724+
await session.close()
725+
726+
727+
async def test_ping_connection_failure():
728+
"""Test ping method when Redis connection fails."""
729+
if not USE_FAKE_REDIS:
730+
pytest.skip("This test requires fakeredis for connection mocking")
731+
732+
import unittest.mock
733+
734+
session = await _create_test_session("ping_failure_test")
735+
736+
try:
737+
# First verify ping works normally
738+
assert await session.ping() is True
739+
740+
# Mock the ping method to raise an exception
741+
with unittest.mock.patch.object(
742+
session._redis, "ping", side_effect=Exception("Connection failed")
743+
):
744+
# ping should return False when connection fails
745+
assert await session.ping() is False
746+
747+
finally:
748+
await session.close()
749+
750+
751+
async def test_close_method_coverage():
752+
"""Test complete coverage of close() method behavior."""
753+
if not USE_FAKE_REDIS:
754+
pytest.skip("This test requires fakeredis for client state verification")
755+
756+
# Test 1: External client (should NOT be closed)
757+
external_client = fake_redis
758+
session1 = RedisSession(
759+
session_id="close_test_1",
760+
redis_client=external_client,
761+
key_prefix="test:",
762+
)
763+
764+
# Verify _owns_client is False for external client
765+
assert session1._owns_client is False
766+
767+
# Close should not close the external client
768+
await session1.close()
769+
770+
# Verify external client is still usable
771+
assert await external_client.ping() is True
772+
773+
# Test 2: Internal client (should be closed)
774+
# Create a session that owns its client
775+
session2 = RedisSession(
776+
session_id="close_test_2",
777+
redis_client=fake_redis,
778+
key_prefix="test:",
779+
)
780+
session2._owns_client = True # Simulate ownership
781+
782+
# This should trigger the close path for owned clients
783+
await session2.close()

0 commit comments

Comments
 (0)