diff --git a/src/utils/files.py b/src/utils/files.py index 4dff50f4..cdbac514 100644 --- a/src/utils/files.py +++ b/src/utils/files.py @@ -10,7 +10,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from src import schemas from src.config import settings -from src.exceptions import FileProcessingError, UnsupportedFileTypeError +from src.exceptions import ( + FileProcessingError, + UnsupportedFileTypeError, + ValidationException, +) from src.schemas import Message logger = logging.getLogger(__name__) @@ -58,7 +62,19 @@ class JSONProcessor: async def extract_text(self, content: bytes) -> str: import json - data = json.loads(content.decode("utf-8")) + try: + decoded_content = content.decode("utf-8") + except UnicodeDecodeError as exc: + raise ValidationException("JSON uploads must be UTF-8 encoded") from exc + + if not decoded_content.strip(): + return "" + + try: + data = json.loads(decoded_content) + except json.JSONDecodeError as exc: + raise ValidationException("Uploaded JSON is invalid") from exc + # Convert JSON to readable text format return json.dumps(data, ensure_ascii=False) diff --git a/tests/routes/test_files.py b/tests/routes/test_files.py index e31e2367..3af9f60b 100644 --- a/tests/routes/test_files.py +++ b/tests/routes/test_files.py @@ -134,6 +134,33 @@ async def test_create_messages_with_json_file( assert message["session_id"] == session_name +@pytest.mark.asyncio +async def test_create_messages_with_empty_json_file( + client: TestClient, + db_session: AsyncSession, + sample_data: tuple[Workspace, Peer], +): + """Test that empty JSON uploads do not crash and create empty content.""" + test_workspace, test_peer = sample_data + + test_session = await _create_test_session(db_session, test_workspace) + session_name = test_session.name + + file_data = io.BytesIO(b"") + files = {"file": ("empty.json", file_data, "application/json")} + form_data = {"peer_id": test_peer.name} + + url = _get_upload_url(test_workspace.name, session_name) + response = client.post(url, files=files, data=form_data) + + assert response.status_code == 201 + data = response.json() + assert len(data) == 1 + assert data[0]["content"] == "" + assert data[0]["peer_id"] == test_peer.name + assert data[0]["session_id"] == session_name + + @pytest.mark.asyncio async def test_create_messages_with_unsupported_file_type( client: TestClient, diff --git a/tests/utils/test_files.py b/tests/utils/test_files.py new file mode 100644 index 00000000..0b577be7 --- /dev/null +++ b/tests/utils/test_files.py @@ -0,0 +1,39 @@ +import json + +import pytest + +from src.exceptions import ValidationException +from src.utils.files import JSONProcessor + + +@pytest.mark.asyncio +async def test_json_processor_returns_empty_string_for_blank_content(): + processor = JSONProcessor() + + assert await processor.extract_text(b"") == "" + assert await processor.extract_text(b" \n\t") == "" + + +@pytest.mark.asyncio +async def test_json_processor_preserves_valid_json_behavior(): + processor = JSONProcessor() + + result = await processor.extract_text(b'{"name": "test", "count": 1}') + + assert json.loads(result) == {"name": "test", "count": 1} + + +@pytest.mark.asyncio +async def test_json_processor_rejects_non_utf8_content(): + processor = JSONProcessor() + + with pytest.raises(ValidationException, match="UTF-8"): + await processor.extract_text(b"\xff\xfe\x00{") + + +@pytest.mark.asyncio +async def test_json_processor_rejects_invalid_json_content(): + processor = JSONProcessor() + + with pytest.raises(ValidationException, match="invalid"): + await processor.extract_text(b'{"name": }')