|
| 1 | +"""Pin the oversize-message rejection boundary (ISSUE-105). |
| 2 | +
|
| 3 | +``max_message_size`` caps the total envelope size of a single wire |
| 4 | +frame. Realistic triggers in production are dynamically-generated SQL: |
| 5 | +many-row ``INSERT VALUES (...), (...), ...`` or ``WHERE col IN (?, ?, ...)`` |
| 6 | +with thousands of placeholders. The encoder must reject at construction |
| 7 | +time with a clear error rather than silently truncate or produce bytes |
| 8 | +the server will refuse. |
| 9 | +
|
| 10 | +Pre-ISSUE-105, no test pinned this boundary. Changes to the envelope |
| 11 | +cap behavior would go undetected. |
| 12 | +""" |
| 13 | + |
| 14 | +from __future__ import annotations |
| 15 | + |
| 16 | +import pytest |
| 17 | + |
| 18 | +from dqlitewire.buffer import ReadBuffer |
| 19 | +from dqlitewire.codec import decode_message, encode_message |
| 20 | +from dqlitewire.exceptions import DecodeError |
| 21 | +from dqlitewire.messages.requests import QuerySqlRequest |
| 22 | + |
| 23 | + |
| 24 | +class TestOversizeSqlEncode: |
| 25 | + """An encoder-constructed frame whose total size exceeds the |
| 26 | + buffer's max cap must be rejected at decode — the wire format |
| 27 | + itself does not refuse oversize frames at encode time, but the |
| 28 | + decoder's envelope check is the first thing any real peer does |
| 29 | + to our bytes.""" |
| 30 | + |
| 31 | + def test_huge_sql_frame_exceeds_buffer_cap(self) -> None: |
| 32 | + """A frame encoded from a multi-MB SQL string must be rejected |
| 33 | + by the decoder's envelope cap.""" |
| 34 | + # Construct a SQL string whose encoded frame exceeds a small |
| 35 | + # buffer cap. Use a cap small enough that the test is cheap; |
| 36 | + # the real cap is 64 MiB. |
| 37 | + small_cap = 1024 * 1024 # 1 MiB |
| 38 | + # Each "?," is 2 bytes; aim for ~1.5 MB of SQL. |
| 39 | + big_sql = "SELECT " + ",".join(["?"] * 750_000) |
| 40 | + msg = QuerySqlRequest(db_id=0, sql=big_sql) |
| 41 | + encoded = encode_message(msg) |
| 42 | + assert len(encoded) > small_cap, ( |
| 43 | + f"test setup: encoded frame must exceed cap ({len(encoded)} vs {small_cap})" |
| 44 | + ) |
| 45 | + |
| 46 | + buf = ReadBuffer(max_message_size=small_cap) |
| 47 | + with pytest.raises(DecodeError, match="exceeds maximum"): |
| 48 | + buf.feed(encoded) |
| 49 | + |
| 50 | + def test_huge_sql_within_cap_decodes(self) -> None: |
| 51 | + """A frame that fits within the cap must decode cleanly — |
| 52 | + guards against a regression that over-tightens the check.""" |
| 53 | + big_sql = "SELECT " + ",".join(["?"] * 1_000) # ~6 KB |
| 54 | + msg = QuerySqlRequest(db_id=0, sql=big_sql) |
| 55 | + encoded = encode_message(msg) |
| 56 | + assert len(encoded) < ReadBuffer.DEFAULT_MAX_MESSAGE_SIZE |
| 57 | + decoded = decode_message(encoded, is_request=True) |
| 58 | + assert isinstance(decoded, QuerySqlRequest) |
| 59 | + assert decoded.sql == big_sql |
| 60 | + |
| 61 | + def test_just_over_default_cap_is_rejected(self) -> None: |
| 62 | + """Boundary test against the real 64 MiB default. Construct a |
| 63 | + SQL string whose frame size is slightly over the default cap. |
| 64 | + The test uses a custom buffer at the default size so the |
| 65 | + assertion is explicit.""" |
| 66 | + # Each '?' + comma = 2 bytes; padding overhead is small. Aim |
| 67 | + # for ~64 MiB + a margin. |
| 68 | + target_bytes = ReadBuffer.DEFAULT_MAX_MESSAGE_SIZE + 1024 * 1024 # 65 MiB |
| 69 | + n_placeholders = target_bytes // 2 |
| 70 | + # Build the string in one go to avoid quadratic concatenation. |
| 71 | + big_sql = "SELECT " + ",".join(["?"] * n_placeholders) |
| 72 | + msg = QuerySqlRequest(db_id=0, sql=big_sql) |
| 73 | + encoded = encode_message(msg) |
| 74 | + assert len(encoded) > ReadBuffer.DEFAULT_MAX_MESSAGE_SIZE |
| 75 | + |
| 76 | + buf = ReadBuffer() # default cap |
| 77 | + with pytest.raises(DecodeError, match="exceeds maximum"): |
| 78 | + buf.feed(encoded) |
0 commit comments