"""Unit tests for ``_protocol.py`` framing primitives. These run without a database — they assert that the encoders produce exactly the bytes IBM's JDBC reference would produce, and that the decoders round-trip cleanly. The byte expectations come from the captured login PDU in ``docs/CAPTURES/01-connect-only.socat.log``. """ from __future__ import annotations from io import BytesIO import pytest from informix_db._protocol import IfxStreamReader, IfxStreamWriter, ProtocolError def make_writer() -> tuple[IfxStreamWriter, BytesIO]: buf = BytesIO() return IfxStreamWriter(buf), buf # --------------------------------------------------------------------------- # Endianness # --------------------------------------------------------------------------- class TestEndianness: """All multi-byte integers MUST be big-endian (network byte order).""" def test_short_is_big_endian(self) -> None: w, buf = make_writer() w.write_short(0x1234) assert buf.getvalue() == b"\x12\x34" def test_int_is_big_endian(self) -> None: w, buf = make_writer() w.write_int(0x12345678) assert buf.getvalue() == b"\x12\x34\x56\x78" def test_long_bigint_is_big_endian(self) -> None: w, buf = make_writer() w.write_long_bigint(0x123456789ABCDEF0) assert buf.getvalue() == b"\x12\x34\x56\x78\x9a\xbc\xde\xf0" def test_observed_capability_int_matches(self) -> None: """Cap_2 from the captured PDU is 0x3c000000 — verify our encoder hits it.""" w, buf = make_writer() w.write_int(0x3C000000) assert buf.getvalue() == b"\x3c\x00\x00\x00" # --------------------------------------------------------------------------- # Padding # --------------------------------------------------------------------------- class TestPadding: """Variable-length payloads must be padded to even byte alignment.""" def test_even_length_no_padding(self) -> None: w, buf = make_writer() w.write_padded(b"\x01\x02") assert buf.getvalue() == b"\x01\x02" def test_odd_length_pads_with_nul(self) -> None: w, buf = make_writer() w.write_padded(b"\x01\x02\x03") assert buf.getvalue() == b"\x01\x02\x03\x00" def test_empty_no_padding(self) -> None: w, buf = make_writer() w.write_padded(b"") assert buf.getvalue() == b"" # --------------------------------------------------------------------------- # Length-prefixed strings # --------------------------------------------------------------------------- class TestStringEncoding: """``write_string_with_nul`` must produce ``[short len+1][bytes][nul]``.""" def test_simple_ascii(self) -> None: """The username 'informix' from the captured login: 9 bytes (8+nul).""" w, buf = make_writer() w.write_string_with_nul("informix") assert buf.getvalue() == b"\x00\x09informix\x00" def test_password_in4mix_matches_capture(self) -> None: """The password 'in4mix' from the captured login: 7 bytes (6+nul).""" w, buf = make_writer() w.write_string_with_nul("in4mix") assert buf.getvalue() == b"\x00\x07in4mix\x00" def test_empty_string(self) -> None: w, buf = make_writer() w.write_string_with_nul("") # Empty string: length=1 (just the nul), content=just the nul. assert buf.getvalue() == b"\x00\x01\x00" # --------------------------------------------------------------------------- # Reader round-trips # --------------------------------------------------------------------------- class TestRoundTrip: """Every encoder must round-trip through its matching decoder.""" @pytest.mark.parametrize("value", [0, 1, -1, 32767, -32768, 0x1234]) def test_short_round_trip(self, value: int) -> None: w, buf = make_writer() w.write_short(value) r = IfxStreamReader(BytesIO(buf.getvalue())) assert r.read_short() == value @pytest.mark.parametrize("value", [0, 1, -1, 0x7FFFFFFF, -0x80000000, 0x3C000000]) def test_int_round_trip(self, value: int) -> None: w, buf = make_writer() w.write_int(value) r = IfxStreamReader(BytesIO(buf.getvalue())) assert r.read_int() == value @pytest.mark.parametrize("value", [0, 1, -1, 0x7FFFFFFFFFFFFFFF, -0x8000000000000000]) def test_long_bigint_round_trip(self, value: int) -> None: w, buf = make_writer() w.write_long_bigint(value) r = IfxStreamReader(BytesIO(buf.getvalue())) assert r.read_long_bigint() == value @pytest.mark.parametrize("text", ["", "informix", "in4mix", "hello world"]) def test_string_with_nul_round_trip(self, text: str) -> None: w, buf = make_writer() w.write_string_with_nul(text) r = IfxStreamReader(BytesIO(buf.getvalue())) assert r.read_string_with_nul() == text # --------------------------------------------------------------------------- # Failure modes # --------------------------------------------------------------------------- class TestFailureModes: """Truncated or malformed streams must raise ``ProtocolError``, not silently misbehave.""" def test_short_eof_raises(self) -> None: r = IfxStreamReader(BytesIO(b"\x12")) # 1 byte, want 2 with pytest.raises(ProtocolError, match="unexpected EOF"): r.read_short() def test_int_eof_raises(self) -> None: r = IfxStreamReader(BytesIO(b"\x00\x00\x00")) # 3 bytes, want 4 with pytest.raises(ProtocolError, match="unexpected EOF"): r.read_int() def test_negative_string_length_rejected(self) -> None: # A short with the high bit set parses as negative; should fail fast. r = IfxStreamReader(BytesIO(b"\xff\xff")) with pytest.raises(ProtocolError, match="negative string length"): r.read_string_with_nul()