@@ -2525,3 +2525,151 @@ def test_refresh_failure_preserves_existing_config(self, tmp_path):
25252525 # The pre-existing config was NOT destroyed before the failure
25262526 # (install handles cleanup atomically; refresh no longer pre-strips).
25272527 assert config_path .read_text () == original
2528+
2529+
2530+ # -- Bounded stdin reader ---------------------------------------------------
2531+
2532+ class TestReadStdinBounded :
2533+ """Test _read_stdin_bounded byte-accurate limiting."""
2534+
2535+ def test_small_payload_passes_through (self ):
2536+ from specify_cli .events import _read_stdin_bounded
2537+ from io import BytesIO
2538+
2539+ buf = BytesIO (b'{"key": "value"}' )
2540+ stdin_mock = MagicMock ()
2541+ stdin_mock .isatty .return_value = False
2542+ stdin_mock .buffer = buf
2543+ with patch ("specify_cli.events.sys" ) as mock_sys :
2544+ mock_sys .stdin = stdin_mock
2545+ result = _read_stdin_bounded (max_bytes = 1024 )
2546+ assert result == '{"key": "value"}'
2547+
2548+ def test_multibyte_utf8_counted_as_bytes (self ):
2549+ """A 4-byte UTF-8 character counts as 4 bytes, not 1 character."""
2550+ from specify_cli .events import _read_stdin_bounded
2551+ from io import BytesIO
2552+
2553+ # U+1F600 (😀) is 4 bytes in UTF-8
2554+ payload = "hello \U0001F600 world" .encode ("utf-8" )
2555+ assert len (payload ) == 16 # "hello " (6) + 😀 (4) + " world" (6)
2556+
2557+ buf = BytesIO (payload )
2558+ stdin_mock = MagicMock ()
2559+ stdin_mock .isatty .return_value = False
2560+ stdin_mock .buffer = buf
2561+ with patch ("specify_cli.events.sys" ) as mock_sys :
2562+ mock_sys .stdin = stdin_mock
2563+ result = _read_stdin_bounded (max_bytes = 16 )
2564+ assert len (result .encode ("utf-8" )) == 16
2565+
2566+ def test_oversized_payload_truncated (self ):
2567+ from specify_cli .events import _read_stdin_bounded
2568+ from io import BytesIO
2569+
2570+ buf = BytesIO (b"x" * 200 )
2571+ stdin_mock = MagicMock ()
2572+ stdin_mock .isatty .return_value = False
2573+ stdin_mock .buffer = buf
2574+ with patch ("specify_cli.events.sys" ) as mock_sys :
2575+ mock_sys .stdin = stdin_mock
2576+ result = _read_stdin_bounded (max_bytes = 100 )
2577+ assert len (result .encode ("utf-8" )) == 100
2578+ assert result == "x" * 100
2579+
2580+ def test_exact_limit_passes (self ):
2581+ from specify_cli .events import _read_stdin_bounded
2582+ from io import BytesIO
2583+
2584+ buf = BytesIO (b"a" * 65536 )
2585+ stdin_mock = MagicMock ()
2586+ stdin_mock .isatty .return_value = False
2587+ stdin_mock .buffer = buf
2588+ with patch ("specify_cli.events.sys" ) as mock_sys :
2589+ mock_sys .stdin = stdin_mock
2590+ result = _read_stdin_bounded (max_bytes = 65536 )
2591+ assert result == "a" * 65536
2592+
2593+ def test_tty_returns_empty_json (self ):
2594+ from specify_cli .events import _read_stdin_bounded
2595+
2596+ stdin_mock = MagicMock ()
2597+ stdin_mock .isatty .return_value = True
2598+ with patch ("specify_cli.events.sys" ) as mock_sys :
2599+ mock_sys .stdin = stdin_mock
2600+ result = _read_stdin_bounded ()
2601+ assert result == "{}"
2602+
2603+ def test_empty_stdin_returns_empty_string (self ):
2604+ from specify_cli .events import _read_stdin_bounded
2605+ from io import BytesIO
2606+
2607+ buf = BytesIO (b"" )
2608+ stdin_mock = MagicMock ()
2609+ stdin_mock .isatty .return_value = False
2610+ stdin_mock .buffer = buf
2611+ with patch ("specify_cli.events.sys" ) as mock_sys :
2612+ mock_sys .stdin = stdin_mock
2613+ result = _read_stdin_bounded ()
2614+ assert result == ""
2615+
2616+ def test_invalid_utf8_replaced (self ):
2617+ from specify_cli .events import _read_stdin_bounded
2618+ from io import BytesIO
2619+
2620+ buf = BytesIO (b"hello\xff \xfe world" )
2621+ stdin_mock = MagicMock ()
2622+ stdin_mock .isatty .return_value = False
2623+ stdin_mock .buffer = buf
2624+ with patch ("specify_cli.events.sys" ) as mock_sys :
2625+ mock_sys .stdin = stdin_mock
2626+ result = _read_stdin_bounded ()
2627+ assert "hello" in result
2628+ assert "world" in result
2629+
2630+
2631+ class TestReadStdinBoundedCLI :
2632+ """Test the CLI event runner's bounded stdin reader."""
2633+
2634+ def test_cli_reader_small_payload (self ):
2635+ from specify_cli .commands .event import _read_stdin_bounded
2636+ from io import BytesIO
2637+
2638+ buf = BytesIO (b'{"event": "test"}' )
2639+ stdin_mock = MagicMock ()
2640+ stdin_mock .isatty .return_value = False
2641+ stdin_mock .buffer = buf
2642+ with patch ("specify_cli.commands.event.sys" ) as mock_sys :
2643+ mock_sys .stdin = stdin_mock
2644+ result = _read_stdin_bounded (max_bytes = 1024 )
2645+ assert result == '{"event": "test"}'
2646+
2647+ def test_cli_reader_oversized (self ):
2648+ from specify_cli .commands .event import _read_stdin_bounded
2649+ from io import BytesIO
2650+
2651+ buf = BytesIO (b"y" * 500 )
2652+ stdin_mock = MagicMock ()
2653+ stdin_mock .isatty .return_value = False
2654+ stdin_mock .buffer = buf
2655+ with patch ("specify_cli.commands.event.sys" ) as mock_sys :
2656+ mock_sys .stdin = stdin_mock
2657+ result = _read_stdin_bounded (max_bytes = 100 )
2658+ assert len (result ) == 100
2659+
2660+ def test_cli_reader_multibyte (self ):
2661+ from specify_cli .commands .event import _read_stdin_bounded
2662+ from io import BytesIO
2663+
2664+ # U+00E9 (é) is 2 bytes in UTF-8
2665+ payload = "caf\u00e9 " .encode ("utf-8" )
2666+ assert len (payload ) == 5 # c + a + f + 2
2667+
2668+ buf = BytesIO (payload )
2669+ stdin_mock = MagicMock ()
2670+ stdin_mock .isatty .return_value = False
2671+ stdin_mock .buffer = buf
2672+ with patch ("specify_cli.commands.event.sys" ) as mock_sys :
2673+ mock_sys .stdin = stdin_mock
2674+ result = _read_stdin_bounded (max_bytes = 5 )
2675+ assert result == "caf\u00e9 "
0 commit comments