"""Tests for ai_service.py error handling (gitea#36 point 7). A malformed response from the AI model (missing fields, invalid JSON) must raise the dedicated AIResponseError, not a bare KeyError/JSONDecodeError — JSONDecodeError in particular is itself a ValueError subclass, so without this it was caught by the router's `except ValueError` (meant for our own config errors) and misreported as a 400 instead of a 502. """ import json from unittest.mock import AsyncMock, patch import pytest from src.backend.services import ai_service class _FakeResponse: def __init__(self, json_data): self._json_data = json_data def raise_for_status(self): pass def json(self): return self._json_data def _patch_openrouter(monkeypatch, response_json: dict): monkeypatch.setattr(ai_service, "extract_text_from_pdf", lambda _b: "some extracted text") monkeypatch.setattr(ai_service.settings, "openrouter_api_key", "test-key") monkeypatch.setattr(ai_service.settings, "openrouter_model", "test-model") fake_client = AsyncMock() fake_client.post = AsyncMock(return_value=_FakeResponse(response_json)) fake_client.__aenter__ = AsyncMock(return_value=fake_client) fake_client.__aexit__ = AsyncMock(return_value=False) return patch("src.backend.services.ai_service.httpx.AsyncClient", return_value=fake_client) async def test_missing_choices_key_raises_ai_response_error(monkeypatch): with _patch_openrouter(monkeypatch, {"no_choices_here": True}): with pytest.raises(ai_service.AIResponseError): await ai_service.parse_technical_sheet(b"%PDF-fake") async def test_invalid_json_content_raises_ai_response_error(monkeypatch): bad_content = "not valid json {{{" response_json = {"choices": [{"message": {"content": bad_content}}]} with _patch_openrouter(monkeypatch, response_json): with pytest.raises(ai_service.AIResponseError): await ai_service.parse_technical_sheet(b"%PDF-fake") async def test_valid_json_content_returns_tasks(monkeypatch): tasks = [{"title": "MATERIALI", "directive": "Verificare", "description": "..."}] response_json = {"choices": [{"message": {"content": json.dumps(tasks)}}]} with _patch_openrouter(monkeypatch, response_json): result = await ai_service.parse_technical_sheet(b"%PDF-fake") assert result == tasks async def test_empty_extracted_text_returns_empty_list(monkeypatch): """No AI call at all when the PDF has no extractable text.""" monkeypatch.setattr(ai_service, "extract_text_from_pdf", lambda _b: " ") result = await ai_service.parse_technical_sheet(b"%PDF-fake") assert result == []