|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -from unittest.mock import AsyncMock, patch |
| 5 | +from unittest.mock import AsyncMock, MagicMock, patch |
6 | 6 |
|
7 | 7 | import pytest |
8 | 8 |
|
9 | 9 | from infrastructure.safe_fetch import ( |
10 | 10 | FetchHardError, |
11 | 11 | _is_public, |
| 12 | + _read_body, |
12 | 13 | fetch_public_image, |
13 | 14 | resolve_public_ip, |
14 | 15 | ) |
@@ -129,3 +130,71 @@ async def _fake_send(self, request, **kwargs): |
129 | 130 | ): |
130 | 131 | await fetch_public_image("https://example.com/a.png") |
131 | 132 | assert captured["accept_encoding"] == "identity" |
| 133 | + |
| 134 | + |
| 135 | +class TestReadBodyStopMarker: |
| 136 | + """``stop_after`` ends the read at </head> so an HTML caller pays for the |
| 137 | + head, not the cap. Heads are not reliably small: youtube puts ~700KB of |
| 138 | + inline JSON before its meta tags, which a 512KB cap silently cut off.""" |
| 139 | + |
| 140 | + @staticmethod |
| 141 | + def _resp(chunks): |
| 142 | + resp = MagicMock() |
| 143 | + |
| 144 | + async def aiter_bytes(): |
| 145 | + for chunk in chunks: |
| 146 | + yield chunk |
| 147 | + |
| 148 | + resp.aiter_bytes = aiter_bytes |
| 149 | + return resp |
| 150 | + |
| 151 | + @pytest.mark.asyncio |
| 152 | + async def test_read_stops_at_the_marker(self): |
| 153 | + body = await _read_body( |
| 154 | + self._resp([b"<head><title>x</title></head>", b"<body>" + b"z" * 5000]), |
| 155 | + max_bytes=1_000_000, |
| 156 | + truncate_over_cap=True, |
| 157 | + stop_after=b"</head>", |
| 158 | + ) |
| 159 | + assert bytes(body) == b"<head><title>x</title></head>" |
| 160 | + |
| 161 | + @pytest.mark.asyncio |
| 162 | + async def test_marker_split_across_chunks_is_still_found(self): |
| 163 | + body = await _read_body( |
| 164 | + self._resp([b"<head>a</he", b"ad><body>ignored"]), |
| 165 | + max_bytes=1_000_000, |
| 166 | + truncate_over_cap=True, |
| 167 | + stop_after=b"</head>", |
| 168 | + ) |
| 169 | + assert bytes(body) == b"<head>a</head>" |
| 170 | + |
| 171 | + @pytest.mark.asyncio |
| 172 | + async def test_tags_past_the_old_512kb_cap_survive(self): |
| 173 | + """The youtube shape: a huge head, then the tags.""" |
| 174 | + head = b"<head>" + b"j" * 700_000 + b'<meta property="og:title"></head>' |
| 175 | + body = await _read_body( |
| 176 | + self._resp([head, b"<body>" + b"z" * 600_000]), |
| 177 | + max_bytes=1_048_576, |
| 178 | + truncate_over_cap=True, |
| 179 | + stop_after=b"</head>", |
| 180 | + ) |
| 181 | + assert b"og:title" in bytes(body) |
| 182 | + |
| 183 | + @pytest.mark.asyncio |
| 184 | + async def test_a_missing_marker_still_honours_the_cap(self): |
| 185 | + body = await _read_body( |
| 186 | + self._resp([b"x" * 900, b"y" * 900]), |
| 187 | + max_bytes=1_000, |
| 188 | + truncate_over_cap=True, |
| 189 | + stop_after=b"</head>", |
| 190 | + ) |
| 191 | + assert len(body) == 1_000 |
| 192 | + |
| 193 | + @pytest.mark.asyncio |
| 194 | + async def test_without_a_marker_the_whole_body_is_read(self): |
| 195 | + body = await _read_body( |
| 196 | + self._resp([b"<head></head>", b"<body>tail"]), |
| 197 | + max_bytes=1_000, |
| 198 | + truncate_over_cap=True, |
| 199 | + ) |
| 200 | + assert bytes(body) == b"<head></head><body>tail" |
0 commit comments