From 184d852eb51108f34f73eeeb51607b61491a14c3 Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Sun, 26 Apr 2026 23:53:46 +0200 Subject: [PATCH 1/4] Wrap persistent-session POST responses in async with --- maxapi/connection/base.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index 7324f58..a7db198 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -234,8 +234,8 @@ async def upload_file(self, url: str, path: str, type: UploadType) -> str: session = bot.session if session is not None and not session.closed: - response = await session.post(url=url, data=form) - return await response.text() + async with session.post(url=url, data=form) as response: + return await response.text() else: async with ClientSession( timeout=bot.default_connection.timeout @@ -285,8 +285,8 @@ async def upload_file_buffer( session = bot.session if session is not None and not session.closed: - response = await session.post(url=url, data=form) - return await response.text() + async with session.post(url=url, data=form) as response: + return await response.text() else: async with ClientSession( timeout=bot.default_connection.timeout From b0c70a3936536bc80d81f243ca8e7ae1b0830f6c Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Mon, 27 Apr 2026 14:36:33 +0200 Subject: [PATCH 2/4] Wrap temp_session.post in async with to guarantee response cleanup --- maxapi/connection/base.py | 8 ++++---- tests/test_coverage_gaps.py | 10 ++++++++-- tests/test_upload_file.py | 33 ++++++++++++++++++++++++--------- 3 files changed, 36 insertions(+), 15 deletions(-) diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index a7db198..21a3b2f 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -240,8 +240,8 @@ async def upload_file(self, url: str, path: str, type: UploadType) -> str: async with ClientSession( timeout=bot.default_connection.timeout ) as temp_session: - response = await temp_session.post(url=url, data=form) - return await response.text() + async with temp_session.post(url=url, data=form) as response: + return await response.text() async def upload_file_buffer( self, filename: str, url: str, buffer: bytes, type: UploadType @@ -291,8 +291,8 @@ async def upload_file_buffer( async with ClientSession( timeout=bot.default_connection.timeout ) as temp_session: - response = await temp_session.post(url=url, data=form) - return await response.text() + async with temp_session.post(url=url, data=form) as response: + return await response.text() async def download_file( self, diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 5b77e89..85ce256 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -502,8 +502,11 @@ async def test_upload_file_uses_temp_session_when_session_is_none( mock_response = AsyncMock() mock_response.text = AsyncMock(return_value='{"token":"abc"}') + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_session_instance = AsyncMock() - mock_session_instance.post = AsyncMock(return_value=mock_response) + mock_session_instance.post = Mock(return_value=mock_cm) mock_session_instance.__aenter__ = AsyncMock( return_value=mock_session_instance ) @@ -538,9 +541,12 @@ async def test_upload_file_buffer_mimetypes_guess_extension( mock_response = AsyncMock() mock_response.text = AsyncMock(return_value='{"token":"xyz"}') + mock_cm_buf = AsyncMock() + mock_cm_buf.__aenter__.return_value = mock_response + bot.session = MagicMock() bot.session.closed = False - bot.session.post = AsyncMock(return_value=mock_response) + bot.session.post = Mock(return_value=mock_cm_buf) # Подменяем puremagic, чтобы вернуть распознаваемый MIME-матч, # и mimetypes.guess_extension — чтобы вернуть реальное расширение. diff --git a/tests/test_upload_file.py b/tests/test_upload_file.py index 8312300..5851b4c 100644 --- a/tests/test_upload_file.py +++ b/tests/test_upload_file.py @@ -31,9 +31,12 @@ async def test_known_extension_uses_guessed_mime(self, tmp_path): mock_response = AsyncMock() mock_response.text = AsyncMock(return_value='{"token":"t"}') + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False - mock_session.post = AsyncMock(return_value=mock_response) + mock_session.post = Mock(return_value=mock_cm) conn, _bot = _make_connection_with_bot(session=mock_session) @@ -43,7 +46,7 @@ async def test_known_extension_uses_guessed_mime(self, tmp_path): type=UploadType.IMAGE, ) - mock_session.post.assert_awaited_once() + mock_session.post.assert_called_once() @pytest.mark.asyncio async def test_unknown_extension_falls_back_to_type_wildcard( @@ -56,9 +59,12 @@ async def test_unknown_extension_falls_back_to_type_wildcard( mock_response = AsyncMock() mock_response.text = AsyncMock(return_value='{"token":"t"}') + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False - mock_session.post = AsyncMock(return_value=mock_response) + mock_session.post = Mock(return_value=mock_cm) conn, _bot = _make_connection_with_bot(session=mock_session) @@ -72,7 +78,7 @@ async def test_unknown_extension_falls_back_to_type_wildcard( type=UploadType.FILE, ) - mock_session.post.assert_awaited_once() + mock_session.post.assert_called_once() class TestUploadFileTempSession: @@ -90,8 +96,11 @@ async def test_temp_session_with_timeout_when_no_session(self, tmp_path): conn, bot = _make_connection_with_bot(session=None) expected_timeout = bot.default_connection.timeout + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_temp_session = AsyncMock() - mock_temp_session.post = AsyncMock(return_value=mock_response) + mock_temp_session.post = Mock(return_value=mock_cm) with patch( "maxapi.connection.base.ClientSession", @@ -107,7 +116,7 @@ async def test_temp_session_with_timeout_when_no_session(self, tmp_path): ) mock_cs_cls.assert_called_once_with(timeout=expected_timeout) - mock_temp_session.post.assert_awaited_once() + mock_temp_session.post.assert_called_once() @pytest.mark.asyncio async def test_temp_session_with_timeout_when_session_closed( @@ -126,8 +135,11 @@ async def test_temp_session_with_timeout_when_session_closed( conn, bot = _make_connection_with_bot(session=closed_session) expected_timeout = bot.default_connection.timeout + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_temp_session = AsyncMock() - mock_temp_session.post = AsyncMock(return_value=mock_response) + mock_temp_session.post = Mock(return_value=mock_cm) with patch( "maxapi.connection.base.ClientSession", @@ -153,9 +165,12 @@ async def test_uses_existing_session_when_open(self, tmp_path): mock_response = AsyncMock() mock_response.text = AsyncMock(return_value='{"token":"t"}') + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False - mock_session.post = AsyncMock(return_value=mock_response) + mock_session.post = Mock(return_value=mock_cm) conn, _bot = _make_connection_with_bot(session=mock_session) @@ -169,4 +184,4 @@ async def test_uses_existing_session_when_open(self, tmp_path): ) mock_cs_cls.assert_not_called() - mock_session.post.assert_awaited_once() + mock_session.post.assert_called_once() From 1a1baca45ccf41c48bf4594b050662af2c2834b6 Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Tue, 28 Apr 2026 16:43:50 +0200 Subject: [PATCH 3/4] Fix coverage and CI tests --- maxapi/connection/base.py | 24 +++++++++++--------- tests/test_coverage_gaps.py | 44 +++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/maxapi/connection/base.py b/maxapi/connection/base.py index 21a3b2f..cf496a5 100644 --- a/maxapi/connection/base.py +++ b/maxapi/connection/base.py @@ -237,11 +237,13 @@ async def upload_file(self, url: str, path: str, type: UploadType) -> str: async with session.post(url=url, data=form) as response: return await response.text() else: - async with ClientSession( - timeout=bot.default_connection.timeout - ) as temp_session: - async with temp_session.post(url=url, data=form) as response: - return await response.text() + async with ( + ClientSession( + timeout=bot.default_connection.timeout + ) as temp_session, + temp_session.post(url=url, data=form) as response, + ): + return await response.text() async def upload_file_buffer( self, filename: str, url: str, buffer: bytes, type: UploadType @@ -288,11 +290,13 @@ async def upload_file_buffer( async with session.post(url=url, data=form) as response: return await response.text() else: - async with ClientSession( - timeout=bot.default_connection.timeout - ) as temp_session: - async with temp_session.post(url=url, data=form) as response: - return await response.text() + async with ( + ClientSession( + timeout=bot.default_connection.timeout + ) as temp_session, + temp_session.post(url=url, data=form) as response, + ): + return await response.text() async def download_file( self, diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 85ce256..5df90d1 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -543,6 +543,7 @@ async def test_upload_file_buffer_mimetypes_guess_extension( mock_response.text = AsyncMock(return_value='{"token":"xyz"}') mock_cm_buf = AsyncMock() mock_cm_buf.__aenter__.return_value = mock_response + mock_cm_buf.__aexit__.return_value = False bot.session = MagicMock() bot.session.closed = False @@ -576,3 +577,46 @@ def _fake_getitem(self_m, idx): # guess_extension was called (the covered line) mock_ge.assert_called_once_with("image/png") assert result == '{"token":"xyz"}' + + async def test_upload_file_buffer_uses_temp_session_when_session_is_none( + self, bot + ): + """upload_file_buffer falls back to a new ClientSession + when bot.session=None.""" + from maxapi.connection.base import BaseConnection + from maxapi.enums.upload_type import UploadType + + conn = BaseConnection() + conn.bot = bot + bot.session = None # force the else-branch + + some_buffer = b"\x00" * 32 + + mock_response = AsyncMock() + mock_response.text = AsyncMock(return_value='{"token":"buf"}') + + mock_cm = AsyncMock() + mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False + + mock_session_instance = AsyncMock() + mock_session_instance.post = Mock(return_value=mock_cm) + mock_session_instance.__aenter__ = AsyncMock( + return_value=mock_session_instance + ) + mock_session_instance.__aexit__ = AsyncMock(return_value=False) + + with patch( + "maxapi.connection.base.ClientSession", + return_value=mock_session_instance, + ): + result = await conn.upload_file_buffer( + filename="clip", + url="https://upload.example.com", + buffer=some_buffer, + type=UploadType.VIDEO, + ) + + assert result == '{"token":"buf"}' + mock_session_instance.post.assert_called_once() + mock_cm.__aenter__.assert_called_once() From ad9e070d3ed26b67ddbf2b6776da13ea17e27a4d Mon Sep 17 00:00:00 2001 From: Aleksandr Kovalko Date: Wed, 29 Apr 2026 01:57:06 +0200 Subject: [PATCH 4/4] Set __aexit__.return_value = False on post() context manager mocks --- tests/test_coverage_gaps.py | 1 + tests/test_upload_file.py | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/tests/test_coverage_gaps.py b/tests/test_coverage_gaps.py index 5df90d1..65f928c 100644 --- a/tests/test_coverage_gaps.py +++ b/tests/test_coverage_gaps.py @@ -504,6 +504,7 @@ async def test_upload_file_uses_temp_session_when_session_is_none( mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_session_instance = AsyncMock() mock_session_instance.post = Mock(return_value=mock_cm) diff --git a/tests/test_upload_file.py b/tests/test_upload_file.py index 5851b4c..b013c90 100644 --- a/tests/test_upload_file.py +++ b/tests/test_upload_file.py @@ -33,6 +33,7 @@ async def test_known_extension_uses_guessed_mime(self, tmp_path): mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False @@ -61,6 +62,7 @@ async def test_unknown_extension_falls_back_to_type_wildcard( mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False @@ -98,6 +100,7 @@ async def test_temp_session_with_timeout_when_no_session(self, tmp_path): mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_temp_session = AsyncMock() mock_temp_session.post = Mock(return_value=mock_cm) @@ -137,6 +140,7 @@ async def test_temp_session_with_timeout_when_session_closed( mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_temp_session = AsyncMock() mock_temp_session.post = Mock(return_value=mock_cm) @@ -167,6 +171,7 @@ async def test_uses_existing_session_when_open(self, tmp_path): mock_cm = AsyncMock() mock_cm.__aenter__.return_value = mock_response + mock_cm.__aexit__.return_value = False mock_session = AsyncMock(spec=ClientSession) mock_session.closed = False