"""Tests for NTCP2 payload block codec. Covers encode/decode roundtrips, helper constructors, options encoding, edge cases, and block ordering validation. """ import struct import pytest from i2p_transport.ntcp2_blocks import ( BLOCK_DATETIME, BLOCK_OPTIONS, BLOCK_ROUTERINFO, BLOCK_I2NP, BLOCK_TERMINATION, BLOCK_PADDING, NTCP2Block, encode_blocks, decode_blocks, encode_msg1_options, decode_msg1_options, encode_msg2_options, decode_msg2_options, datetime_block, i2np_block, padding_block, termination_block, router_info_block, ) # --- Block type constants --- class TestConstants: def test_block_type_values(self): assert BLOCK_DATETIME == 0 assert BLOCK_OPTIONS == 1 assert BLOCK_ROUTERINFO == 2 assert BLOCK_I2NP == 3 assert BLOCK_TERMINATION == 4 assert BLOCK_PADDING == 254 # --- NTCP2Block dataclass --- class TestNTCP2Block: def test_create_block(self): block = NTCP2Block(block_type=3, data=b"\x01\x02\x03") assert block.block_type == 3 assert block.data == b"\x01\x02\x03" def test_create_empty_data_block(self): block = NTCP2Block(block_type=0, data=b"") assert block.block_type == 0 assert block.data == b"" # --- encode_blocks / decode_blocks roundtrip --- class TestEncodeDecodeBlocks: def test_single_block_roundtrip(self): original = [NTCP2Block(block_type=BLOCK_I2NP, data=b"\xaa\xbb\xcc")] encoded = encode_blocks(original) # 1 byte type + 2 bytes length + 3 bytes data = 6 bytes assert len(encoded) == 6 decoded = decode_blocks(encoded) assert len(decoded) == 1 assert decoded[0].block_type == BLOCK_I2NP assert decoded[0].data == b"\xaa\xbb\xcc" def test_multiple_blocks_roundtrip(self): blocks = [ NTCP2Block(block_type=BLOCK_DATETIME, data=b"\x00\x00\x00\x01"), NTCP2Block(block_type=BLOCK_I2NP, data=b"\xde\xad"), NTCP2Block(block_type=BLOCK_PADDING, data=b"\x00" * 10), ] encoded = encode_blocks(blocks) decoded = decode_blocks(encoded) assert len(decoded) == 3 for orig, dec in zip(blocks, decoded): assert orig.block_type == dec.block_type assert orig.data == dec.data def test_empty_data_block_roundtrip(self): blocks = [NTCP2Block(block_type=BLOCK_I2NP, data=b"")] encoded = encode_blocks(blocks) # 1 type + 2 length + 0 data = 3 bytes assert len(encoded) == 3 decoded = decode_blocks(encoded) assert len(decoded) == 1 assert decoded[0].data == b"" def test_encode_wire_format(self): """Verify the exact binary layout: type(1) + length(2 BE) + data.""" block = NTCP2Block(block_type=0x03, data=b"\x01\x02") encoded = encode_blocks([block]) assert encoded[0:1] == b"\x03" # type byte assert encoded[1:3] == b"\x00\x02" # length = 2, big-endian assert encoded[3:5] == b"\x01\x02" # data def test_empty_input_returns_empty_list(self): assert decode_blocks(b"") == [] def test_no_blocks_encodes_to_empty(self): assert encode_blocks([]) == b"" # --- Helper constructors --- class TestDatetimeBlock: def test_datetime_block_format(self): ts = 1700000000 block = datetime_block(ts) assert block.block_type == BLOCK_DATETIME assert len(block.data) == 4 assert struct.unpack("!I", block.data)[0] == ts def test_datetime_block_roundtrip(self): ts = 0 block = datetime_block(ts) encoded = encode_blocks([block]) decoded = decode_blocks(encoded) assert struct.unpack("!I", decoded[0].data)[0] == ts class TestI2NPBlock: def test_i2np_block(self): msg = b"\x11\x22\x33\x44\x55" block = i2np_block(msg) assert block.block_type == BLOCK_I2NP assert block.data == msg def test_zero_length_i2np_block(self): block = i2np_block(b"") assert block.block_type == BLOCK_I2NP assert block.data == b"" encoded = encode_blocks([block]) decoded = decode_blocks(encoded) assert decoded[0].block_type == BLOCK_I2NP assert decoded[0].data == b"" class TestPaddingBlock: def test_padding_block_size(self): block = padding_block(32) assert block.block_type == BLOCK_PADDING assert len(block.data) == 32 def test_padding_block_zero_size(self): block = padding_block(0) assert block.block_type == BLOCK_PADDING assert len(block.data) == 0 def test_padding_block_random_bytes(self): """Two padding blocks of the same size should (almost certainly) differ.""" b1 = padding_block(64) b2 = padding_block(64) # Extremely unlikely to be equal for 64 random bytes assert b1.data != b2.data class TestTerminationBlock: def test_termination_block_format(self): block = termination_block(frames_received=12345, reason=1) assert block.block_type == BLOCK_TERMINATION assert len(block.data) == 9 # 8 bytes LE frames_received + 1 byte reason frames = struct.unpack("