"""`S3Storage.get_bytes` на заглушке Minio (без сети): чтение, закрытие response, `NoSuchKey` → `ObjectNotFoundError`.""" from collections.abc import AsyncIterator from typing import Any from unittest.mock import MagicMock import pytest from miniopy_async.error import S3Error from src.core.clients import S3Storage from src.core.storage import ObjectNotFoundError class StubContent: def __init__(self, data: bytes) -> None: self._data = data async def iter_chunked(self, size: int) -> AsyncIterator[bytes]: for start in range(0, len(self._data), size): yield self._data[start : start + size] class StubResponse: """Ответ `aiohttp` с флагами закрытия.""" def __init__(self, data: bytes, *, fail_read: bool = False) -> None: self._data = data self.content = StubContent(data) self._fail_read = fail_read self.closed = False self.released = False async def read(self) -> bytes: if self._fail_read: raise ConnectionError("обрыв чтения") return self._data def close(self) -> None: self.closed = True async def release(self) -> None: self.released = True class StubMinio: def __init__(self, response: StubResponse | None = None, error: Exception | None = None) -> None: self._response = response self._error = error self.calls: list[tuple[str, str]] = [] async def get_object(self, bucket: str, obj_name: str) -> StubResponse: self.calls.append((bucket, obj_name)) if self._error is not None: raise self._error assert self._response is not None # noqa: S101 return self._response def _s3_error(code: str) -> S3Error: return S3Error(code, "сообщение", None, None, None, MagicMock()) def _storage(minio: StubMinio) -> S3Storage: client: Any = minio return S3Storage(client, "arm-ka") async def test_get_bytes_returns_content_and_closes_response() -> None: response = StubResponse(b"payload") minio = StubMinio(response) assert await _storage(minio).get_bytes("uploads/a/b.xls") == b"payload" assert minio.calls == [("arm-ka", "uploads/a/b.xls")] assert response.closed is True assert response.released is True async def test_get_bytes_closes_response_when_read_fails() -> None: response = StubResponse(b"", fail_read=True) with pytest.raises(ConnectionError): await _storage(StubMinio(response)).get_bytes("k") assert response.closed is True assert response.released is True async def test_get_bytes_no_such_key_is_object_not_found() -> None: with pytest.raises(ObjectNotFoundError): await _storage(StubMinio(error=_s3_error("NoSuchKey"))).get_bytes("missing") async def test_get_bytes_other_s3_error_is_not_swallowed() -> None: with pytest.raises(S3Error) as caught: await _storage(StubMinio(error=_s3_error("AccessDenied"))).get_bytes("k") assert caught.value.code == "AccessDenied" async def test_stream_yields_chunks_and_releases_response() -> None: response = StubResponse(b"abcdefghij") chunks = [chunk async for chunk in _storage(StubMinio(response)).stream("k", chunk_size=4)] assert chunks == [b"abcd", b"efgh", b"ij"] assert response.closed is True assert response.released is True async def test_stream_no_such_key_is_object_not_found_before_first_chunk() -> None: with pytest.raises(ObjectNotFoundError): await _storage(StubMinio(error=_s3_error("NoSuchKey"))).stream("missing").__anext__() async def test_stream_other_s3_error_is_not_swallowed() -> None: with pytest.raises(S3Error) as caught: await _storage(StubMinio(error=_s3_error("AccessDenied"))).stream("k").__anext__() assert caught.value.code == "AccessDenied"