# SPDX-License-Identifier: AGPL-3.0-only # Copyright (c) 2026 sol pbc """Framing encode/decode round-trip + flag validation (forked from spl/home).""" from __future__ import annotations import pytest from solstone.convey.secure_listener.framing import ( CONTROL_NONCE_LEN, FLAG_CLOSE, FLAG_DATA, FLAG_OPEN, FLAG_PING, FLAG_PONG, FLAG_RESERVED_MASK, FLAG_RESET, FLAG_WINDOW, HEADER_LEN, MAX_SEND_CREDIT, RESET_PROTOCOL_ERROR, Frame, FrameDecoder, ProtocolError, build_close, build_data, build_open, build_ping, build_pong, build_reset, build_window, parse_control_nonce, parse_reset_reason, parse_window_credit, validate_flags, ) def test_header_is_8_bytes() -> None: assert HEADER_LEN == 8 def test_encode_decode_roundtrip() -> None: original = Frame(stream_id=7, flags=FLAG_DATA, payload=b"hello world") encoded = original.encode() decoder = FrameDecoder() decoder.feed(encoded) got = decoder.next() assert got == original def test_decoder_handles_fragmented_feeds() -> None: frame = Frame(stream_id=5, flags=FLAG_DATA, payload=b"fragmented") encoded = frame.encode() decoder = FrameDecoder() for byte in encoded: decoder.feed(bytes([byte])) assert decoder.next() == frame def test_decoder_returns_none_when_incomplete() -> None: decoder = FrameDecoder() decoder.feed(b"\x00\x00\x00\x01") # partial header assert decoder.next() is None def test_multiple_frames_in_one_buffer() -> None: frames = [build_data(1, b"a"), build_data(3, b"bb"), build_data(5, b"ccc")] decoder = FrameDecoder() for f in frames: decoder.feed(f.encode()) assert decoder.drain() == frames def test_reserved_bits_rejected_on_encode() -> None: with pytest.raises(ProtocolError): Frame(stream_id=1, flags=FLAG_RESERVED_MASK, payload=b"").encode() def test_reserved_bits_rejected_on_decode() -> None: bad = Frame(stream_id=1, flags=FLAG_DATA, payload=b"") encoded = bytearray(bad.encode()) encoded[4] |= 0x80 decoder = FrameDecoder() decoder.feed(bytes(encoded)) with pytest.raises(ProtocolError): decoder.next() def test_payload_length_bound() -> None: build_data(1, b"").encode() ok = Frame(stream_id=1, flags=FLAG_DATA, payload=b"x" * ((1 << 24) - 1)) ok.encode() def test_open_with_initial_bytes_carries_both_flags() -> None: f = build_open(1, b"seed") assert f.flags & FLAG_OPEN and f.flags & FLAG_DATA assert f.payload == b"seed" def test_open_without_bytes_is_pure_open() -> None: f = build_open(1) assert f.flags & FLAG_OPEN assert not (f.flags & FLAG_DATA) def test_data_with_close_carries_both() -> None: f = build_data(1, b"last", close=True) assert f.flags & FLAG_DATA and f.flags & FLAG_CLOSE def test_close_carries_only_close_flag() -> None: f = build_close(1) assert f.flags & FLAG_CLOSE assert not (f.flags & FLAG_OPEN) def test_window_credit_parse_roundtrip() -> None: f = build_window(1, 65536) assert parse_window_credit(f) == 65536 def test_reset_reason_parse_roundtrip() -> None: f = build_reset(1, RESET_PROTOCOL_ERROR) assert parse_reset_reason(f) == RESET_PROTOCOL_ERROR def test_validate_flags_allows_only_legal_combos() -> None: for flag in ( FLAG_OPEN, FLAG_DATA, FLAG_CLOSE, FLAG_RESET, FLAG_WINDOW, FLAG_OPEN | FLAG_DATA, FLAG_OPEN | FLAG_CLOSE, FLAG_DATA | FLAG_CLOSE, FLAG_OPEN | FLAG_DATA | FLAG_CLOSE, ): validate_flags(flag) with pytest.raises(ProtocolError): validate_flags(FLAG_DATA | FLAG_WINDOW) def test_window_frame_requires_4_byte_payload() -> None: f = Frame(stream_id=1, flags=FLAG_WINDOW, payload=b"abc") with pytest.raises(ProtocolError): parse_window_credit(f) def test_reset_frame_requires_1_byte_payload() -> None: f = Frame(stream_id=1, flags=FLAG_RESET, payload=b"") with pytest.raises(ProtocolError): parse_reset_reason(f) # ---- streamID==0 PING/PONG control frames ----------------------------------- def test_ping_round_trip_wire_format() -> None: nonce = bytes(range(1, 9)) f = build_ping(nonce) encoded = f.encode() # 4-byte big-endian stream_id (0) + 1 flag byte + 3-byte big-endian length (8) + 8-byte nonce. assert encoded == bytes([0, 0, 0, 0, FLAG_PING, 0, 0, 8]) + nonce decoder = FrameDecoder() decoder.feed(encoded) got = decoder.next() assert got == f assert got.stream_id == 0 assert got.flags == FLAG_PING def test_pong_round_trip_wire_format() -> None: nonce = bytes(range(1, 9)) f = build_pong(nonce) assert f.encode() == bytes([0, 0, 0, 0, FLAG_PONG, 0, 0, 8]) + nonce def test_ping_requires_8_byte_nonce() -> None: with pytest.raises(ProtocolError): build_ping(b"short") with pytest.raises(ProtocolError): build_ping(b"") with pytest.raises(ProtocolError): build_ping(b"x" * 9) def test_pong_requires_8_byte_nonce() -> None: with pytest.raises(ProtocolError): build_pong(b"short") def test_parse_control_nonce_round_trip() -> None: nonce = b"\xde\xad\xbe\xef\xfe\xed\xfa\xce" assert parse_control_nonce(build_ping(nonce)) == nonce assert parse_control_nonce(build_pong(nonce)) == nonce def test_parse_control_nonce_rejects_non_control_frame() -> None: f = build_data(1, b"x" * 8) with pytest.raises(ProtocolError): parse_control_nonce(f) def test_parse_control_nonce_rejects_wrong_payload_length() -> None: f = Frame(stream_id=0, flags=FLAG_PING, payload=b"short") with pytest.raises(ProtocolError): parse_control_nonce(f) def test_validate_flags_allows_solo_ping_and_pong() -> None: validate_flags(FLAG_PING) validate_flags(FLAG_PONG) def test_validate_flags_rejects_ping_combined_with_other_flags() -> None: for other in (FLAG_OPEN, FLAG_DATA, FLAG_CLOSE, FLAG_RESET, FLAG_WINDOW, FLAG_PONG): with pytest.raises(ProtocolError): validate_flags(FLAG_PING | other) def test_validate_flags_rejects_pong_combined_with_other_flags() -> None: for other in (FLAG_OPEN, FLAG_DATA, FLAG_CLOSE, FLAG_RESET, FLAG_WINDOW): with pytest.raises(ProtocolError): validate_flags(FLAG_PONG | other) def test_control_nonce_len_is_eight() -> None: assert CONTROL_NONCE_LEN == 8 def test_max_send_credit_is_signed_31_bit_cap() -> None: assert MAX_SEND_CREDIT == (1 << 31) - 1 def test_reserved_mask_collapsed_to_bit_seven() -> None: # bits 5 and 6 are PING/PONG; only bit 7 (0x80) remains reserved. assert FLAG_RESERVED_MASK == 0x80