|
| 1 | +""" |
| 2 | +Regression tests pinning the exact byte output of ``JsonSerializer`` after the |
| 3 | +removal of the ``ujson`` dependency (CVE-2022-31116 / CVE-2022-31117 / |
| 4 | +CVE-2021-45958). |
| 5 | +
|
| 6 | +``JsonSerializer`` feeds signing/hashing, so its output must stay byte-stable. |
| 7 | +ujson used to be the active encoder; these tests lock the stdlib-``json`` |
| 8 | +``OrderedJsonEncoder`` output so any future drift (separators, key ordering, |
| 9 | +unicode escaping, float repr) is caught. |
| 10 | +
|
| 11 | +The encoder is configured with ``sort_keys=True``, ``separators=(',', ':')`` |
| 12 | +and ``ensure_ascii=False`` -- i.e. sorted keys, no whitespace, and raw UTF-8 |
| 13 | +for non-ASCII characters (not ``\\uXXXX`` escapes). |
| 14 | +""" |
| 15 | +import pytest |
| 16 | + |
| 17 | +from common.serializers.json_serializer import JsonSerializer |
| 18 | + |
| 19 | + |
| 20 | +@pytest.fixture |
| 21 | +def sz(): |
| 22 | + return JsonSerializer() |
| 23 | + |
| 24 | + |
| 25 | +def test_keys_sorted_and_compact(sz): |
| 26 | + # Insertion order must not affect output; keys are sorted, no whitespace. |
| 27 | + assert sz.serialize({'b': 2, 'a': 1, 'c': 3}, toBytes=False) == '{"a":1,"b":2,"c":3}' |
| 28 | + |
| 29 | + |
| 30 | +def test_non_ascii_kept_raw_utf8(sz): |
| 31 | + # ensure_ascii=False: characters are emitted as raw UTF-8, not \\uXXXX. |
| 32 | + assert sz.serialize({'name': 'héllo', 'kr': '한글', 'emoji': '🚀'}, |
| 33 | + toBytes=False) == '{"emoji":"🚀","kr":"한글","name":"héllo"}' |
| 34 | + # And the bytes form is the UTF-8 encoding of that string. |
| 35 | + assert sz.serialize({'name': 'héllo'}) == '{"name":"héllo"}'.encode('utf-8') |
| 36 | + |
| 37 | + |
| 38 | +def test_float_repr_pinned(sz): |
| 39 | + assert sz.serialize({'a': 14.8639, 'b': -97.466179, 'c': 1.0, 'd': 1e20}, |
| 40 | + toBytes=False) == '{"a":14.8639,"b":-97.466179,"c":1.0,"d":1e+20}' |
| 41 | + |
| 42 | + |
| 43 | +def test_int_keys_become_strings(sz): |
| 44 | + # json coerces non-string keys to strings and then sorts lexicographically. |
| 45 | + assert sz.serialize({3: 'c', 1: 'a', 2: 'b'}, toBytes=False) == '{"1":"a","2":"b","3":"c"}' |
| 46 | + |
| 47 | + |
| 48 | +def test_bool_and_none(sz): |
| 49 | + assert sz.serialize({'t': True, 'f': False, 'n': None}, |
| 50 | + toBytes=False) == '{"f":false,"n":null,"t":true}' |
| 51 | + |
| 52 | + |
| 53 | +def test_empty_dict(sz): |
| 54 | + assert sz.serialize({}, toBytes=False) == '{}' |
| 55 | + |
| 56 | + |
| 57 | +def test_top_level_bytes_base64(sz): |
| 58 | + # The OrderedJsonEncoder.encode override base64-encodes a top-level |
| 59 | + # bytes/bytearray value (b'raw' -> base64 'cmF3'). |
| 60 | + assert sz.serialize(b'raw', toBytes=False) == '"cmF3"' |
| 61 | + assert sz.serialize(bytearray(b'raw'), toBytes=False) == '"cmF3"' |
| 62 | + |
| 63 | + |
| 64 | +def test_round_trip(sz): |
| 65 | + data = {'name': 'Alice', 'n': 1, 'f': 1.5, 'b': True, 'list': [1, 'two', None]} |
| 66 | + assert sz.deserialize(sz.serialize(data)) == data |
| 67 | + assert sz.deserialize(sz.serialize(data, toBytes=False)) == data |
| 68 | + |
| 69 | + |
| 70 | +@pytest.mark.parametrize('value', [ |
| 71 | + {'z': b'raw'}, # bytes nested in a dict value |
| 72 | + [b'raw'], # bytes nested in a list |
| 73 | + {'z': bytearray(b'raw')}, |
| 74 | +]) |
| 75 | +def test_nested_bytes_raise_typeerror(sz, value): |
| 76 | + """ |
| 77 | + KNOWN BEHAVIOURAL DIFFERENCE vs ujson. |
| 78 | +
|
| 79 | + The bytes special-case lives in ``OrderedJsonEncoder.encode`` and therefore |
| 80 | + only fires for a *top-level* bytes value (see test above). With ujson |
| 81 | + installed, bytes nested inside a container were silently encoded as UTF-8 |
| 82 | + strings; stdlib ``json`` raises ``TypeError`` instead. |
| 83 | +
|
| 84 | + This characterises the current behaviour so the difference is explicit and |
| 85 | + tracked. If a future change needs nested raw bytes to round-trip, the fix |
| 86 | + belongs in ``OrderedJsonEncoder.default`` (not here). |
| 87 | + """ |
| 88 | + with pytest.raises(TypeError): |
| 89 | + sz.serialize(value) |
0 commit comments