|
| 1 | +"""Tests for Redis key decoding functionality.""" |
| 2 | + |
| 3 | +import os |
| 4 | +import time |
| 5 | +import uuid |
| 6 | +from typing import Any, Dict, Optional |
| 7 | + |
| 8 | +import pytest |
| 9 | +from redis import Redis |
| 10 | + |
| 11 | +from langgraph.checkpoint.redis.util import safely_decode |
| 12 | + |
| 13 | + |
| 14 | +def test_safely_decode_basic_types(): |
| 15 | + """Test safely_decode function with basic type inputs.""" |
| 16 | + # Test with bytes |
| 17 | + assert safely_decode(b"test") == "test" |
| 18 | + |
| 19 | + # Test with string |
| 20 | + assert safely_decode("test") == "test" |
| 21 | + |
| 22 | + # Test with None |
| 23 | + assert safely_decode(None) is None |
| 24 | + |
| 25 | + # Test with other types |
| 26 | + assert safely_decode(123) == 123 |
| 27 | + assert safely_decode(1.23) == 1.23 |
| 28 | + assert safely_decode(True) is True |
| 29 | + |
| 30 | + |
| 31 | +def test_safely_decode_nested_structures(): |
| 32 | + """Test safely_decode function with nested data structures.""" |
| 33 | + # Test with dictionary |
| 34 | + assert safely_decode({b"key": b"value"}) == {"key": "value"} |
| 35 | + assert safely_decode({b"key1": b"value1", "key2": 123}) == { |
| 36 | + "key1": "value1", |
| 37 | + "key2": 123, |
| 38 | + } |
| 39 | + |
| 40 | + # Test with nested dictionary |
| 41 | + nested_dict = {b"outer": {b"inner": b"value"}} |
| 42 | + assert safely_decode(nested_dict) == {"outer": {"inner": "value"}} |
| 43 | + |
| 44 | + # Test with list |
| 45 | + assert safely_decode([b"item1", b"item2"]) == ["item1", "item2"] |
| 46 | + |
| 47 | + # Test with tuple |
| 48 | + assert safely_decode((b"item1", b"item2")) == ("item1", "item2") |
| 49 | + |
| 50 | + # Test with set |
| 51 | + decoded_set = safely_decode({b"item1", b"item2"}) |
| 52 | + assert isinstance(decoded_set, set) |
| 53 | + assert "item1" in decoded_set |
| 54 | + assert "item2" in decoded_set |
| 55 | + |
| 56 | + # Test with complex nested structure |
| 57 | + complex_struct = { |
| 58 | + b"key1": [b"list_item1", {b"nested_key": b"nested_value"}], |
| 59 | + b"key2": (b"tuple_item", 123), |
| 60 | + b"key3": {b"set_item1", b"set_item2"}, |
| 61 | + } |
| 62 | + decoded = safely_decode(complex_struct) |
| 63 | + assert decoded["key1"][0] == "list_item1" |
| 64 | + assert decoded["key1"][1]["nested_key"] == "nested_value" |
| 65 | + assert decoded["key2"][0] == "tuple_item" |
| 66 | + assert decoded["key2"][1] == 123 |
| 67 | + assert isinstance(decoded["key3"], set) |
| 68 | + assert "set_item1" in decoded["key3"] |
| 69 | + assert "set_item2" in decoded["key3"] |
| 70 | + |
| 71 | + |
| 72 | +@pytest.mark.parametrize("decode_responses", [True, False]) |
| 73 | +def test_safely_decode_with_redis(decode_responses: bool, redis_url): |
| 74 | + """Test safely_decode function with actual Redis responses using TestContainers.""" |
| 75 | + r = Redis.from_url(redis_url, decode_responses=decode_responses) |
| 76 | + |
| 77 | + try: |
| 78 | + # Clean up before test to ensure a clean state |
| 79 | + r.delete("test:string") |
| 80 | + r.delete("test:hash") |
| 81 | + r.delete("test:list") |
| 82 | + r.delete("test:set") |
| 83 | + |
| 84 | + # Set up test data |
| 85 | + r.set("test:string", "value") |
| 86 | + r.hset("test:hash", mapping={"field1": "value1", "field2": "value2"}) |
| 87 | + r.rpush("test:list", "item1", "item2", "item3") |
| 88 | + r.sadd("test:set", "member1", "member2") |
| 89 | + |
| 90 | + # Test string value |
| 91 | + string_val = r.get("test:string") |
| 92 | + decoded_string = safely_decode(string_val) |
| 93 | + assert decoded_string == "value" |
| 94 | + |
| 95 | + # Test hash value |
| 96 | + hash_val = r.hgetall("test:hash") |
| 97 | + decoded_hash = safely_decode(hash_val) |
| 98 | + assert decoded_hash == {"field1": "value1", "field2": "value2"} |
| 99 | + |
| 100 | + # Test list value |
| 101 | + list_val = r.lrange("test:list", 0, -1) |
| 102 | + decoded_list = safely_decode(list_val) |
| 103 | + assert decoded_list == ["item1", "item2", "item3"] |
| 104 | + |
| 105 | + # Test set value |
| 106 | + set_val = r.smembers("test:set") |
| 107 | + decoded_set = safely_decode(set_val) |
| 108 | + assert isinstance(decoded_set, set) |
| 109 | + assert "member1" in decoded_set |
| 110 | + assert "member2" in decoded_set |
| 111 | + |
| 112 | + # Test key fetching |
| 113 | + keys = r.keys("test:*") |
| 114 | + decoded_keys = safely_decode(keys) |
| 115 | + assert sorted(decoded_keys) == sorted( |
| 116 | + ["test:string", "test:hash", "test:list", "test:set"] |
| 117 | + ) |
| 118 | + |
| 119 | + finally: |
| 120 | + # Clean up after test |
| 121 | + r.delete("test:string") |
| 122 | + r.delete("test:hash") |
| 123 | + r.delete("test:list") |
| 124 | + r.delete("test:set") |
| 125 | + r.close() |
| 126 | + |
| 127 | + |
| 128 | +def test_safely_decode_unicode_error_handling(): |
| 129 | + """Test safely_decode function with invalid UTF-8 bytes.""" |
| 130 | + # Create bytes that will cause UnicodeDecodeError |
| 131 | + invalid_utf8 = b"\xff\xfe\xfd" |
| 132 | + |
| 133 | + # Should return the original bytes if it can't be decoded |
| 134 | + result = safely_decode(invalid_utf8) |
| 135 | + assert result == invalid_utf8 |
| 136 | + |
| 137 | + # Test with mixed valid and invalid in a complex structure |
| 138 | + mixed = { |
| 139 | + b"valid": b"This is valid UTF-8", |
| 140 | + b"invalid": invalid_utf8, |
| 141 | + b"nested": [b"valid", invalid_utf8], |
| 142 | + } |
| 143 | + |
| 144 | + result = safely_decode(mixed) |
| 145 | + assert result["valid"] == "This is valid UTF-8" |
| 146 | + assert result["invalid"] == invalid_utf8 |
| 147 | + assert result["nested"][0] == "valid" |
| 148 | + assert result["nested"][1] == invalid_utf8 |
0 commit comments