mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-08-08 20:30:59 +00:00
feat(cloud): harden multi-tenant runtime resources
This commit is contained in:
@@ -6,8 +6,13 @@ Tests session management, reuse, and cleanup.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
import aiohttp
|
||||
import httpx
|
||||
from aiohttp import web
|
||||
|
||||
from langbot.pkg.utils import httpclient
|
||||
@@ -88,6 +93,89 @@ class TestCloseAll:
|
||||
|
||||
assert len(httpclient._sessions) == 0
|
||||
|
||||
|
||||
class TestReadLimited:
|
||||
async def test_rejects_oversized_content_length_before_reading(self):
|
||||
content = SimpleNamespace(iter_chunked=None)
|
||||
response = SimpleNamespace(headers={'Content-Length': '11'}, content=content)
|
||||
|
||||
with pytest.raises(httpclient.RemoteResponseTooLargeError):
|
||||
await httpclient.read_limited(response, max_bytes=10)
|
||||
|
||||
async def test_rejects_chunked_body_that_crosses_limit(self):
|
||||
class Content:
|
||||
async def iter_chunked(self, _chunk_size):
|
||||
yield b'12345'
|
||||
yield b'678901'
|
||||
|
||||
response = SimpleNamespace(headers={}, content=Content())
|
||||
|
||||
with pytest.raises(httpclient.RemoteResponseTooLargeError):
|
||||
await httpclient.read_limited(response, max_bytes=10)
|
||||
|
||||
async def test_returns_body_within_limit(self):
|
||||
class Content:
|
||||
async def iter_chunked(self, _chunk_size):
|
||||
yield b'12345'
|
||||
yield b'67890'
|
||||
|
||||
response = SimpleNamespace(headers={}, content=Content())
|
||||
|
||||
assert await httpclient.read_limited(response, max_bytes=10) == b'1234567890'
|
||||
|
||||
async def test_json_reader_uses_same_limit(self):
|
||||
class Content:
|
||||
async def iter_chunked(self, _chunk_size):
|
||||
yield b'{"ok":true}'
|
||||
|
||||
response = SimpleNamespace(
|
||||
headers={},
|
||||
content=Content(),
|
||||
)
|
||||
|
||||
assert await httpclient.read_json_limited(response, max_bytes=16) == {'ok': True}
|
||||
|
||||
async def test_response_json_parse_runs_off_event_loop(self):
|
||||
event_loop_thread = threading.get_ident()
|
||||
response = SimpleNamespace(json=lambda: threading.get_ident())
|
||||
|
||||
assert await httpclient.parse_json_response(response) != event_loop_thread
|
||||
|
||||
async def test_response_json_parse_supports_async_test_doubles(self):
|
||||
response = SimpleNamespace(json=AsyncMock(return_value={'ok': True}))
|
||||
|
||||
assert await httpclient.parse_json_response(response) == {'ok': True}
|
||||
|
||||
async def test_response_text_runs_off_loop_and_caps_diagnostics(self):
|
||||
event_loop_thread = threading.get_ident()
|
||||
|
||||
class Response:
|
||||
@property
|
||||
def text(self):
|
||||
return f'{threading.get_ident()}:abcdef'
|
||||
|
||||
value = await httpclient.response_text(Response(), max_chars=4)
|
||||
|
||||
assert not value.startswith(str(event_loop_thread))
|
||||
assert value.endswith('[truncated]')
|
||||
|
||||
async def test_httpx_hook_rejects_before_automatic_buffer_grows(self):
|
||||
class Source(httpx.AsyncByteStream):
|
||||
async def __aiter__(self):
|
||||
yield b'123'
|
||||
yield b'45'
|
||||
|
||||
async def aclose(self):
|
||||
return None
|
||||
|
||||
transport = httpx.MockTransport(lambda _request: httpx.Response(200, stream=Source()))
|
||||
async with httpx.AsyncClient(
|
||||
transport=transport,
|
||||
event_hooks=httpclient.httpx_response_limit_hooks(max_bytes=4),
|
||||
) as client:
|
||||
with pytest.raises(httpclient.RemoteResponseTooLargeError, match='4-byte'):
|
||||
await client.get('https://example.invalid')
|
||||
|
||||
async def test_close_all_handles_already_closed(self):
|
||||
"""close_all handles already closed sessions gracefully."""
|
||||
session = httpclient.get_session()
|
||||
|
||||
Reference in New Issue
Block a user