Spaces:
Running
Running
| """Tests for FHIR async client, tool wrappers, and deterministic query aggregation.""" | |
| from __future__ import annotations | |
| import asyncio | |
| from typing import Any | |
| import httpx | |
| import pytest | |
| from backend.errors import FHIRClientError | |
| from backend.fhir import mock_api | |
| from backend.fhir.client import FHIRClient | |
| from backend.fhir import tools, queries | |
| def asgi_client_patch(monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Patch httpx.AsyncClient to target mock FHIR FastAPI app via ASGI transport. | |
| Args: | |
| monkeypatch (pytest.MonkeyPatch): Pytest monkeypatch fixture. | |
| Returns: | |
| None: Applies monkeypatch side effects for test duration. | |
| """ | |
| transport = httpx.ASGITransport(app=mock_api.app) | |
| original_async_client = httpx.AsyncClient | |
| def build_client(*args: Any, **kwargs: Any) -> httpx.AsyncClient: | |
| kwargs["transport"] = transport | |
| return original_async_client(*args, **kwargs) | |
| monkeypatch.setattr("backend.fhir.client.httpx.AsyncClient", build_client) | |
| def test_fhir_client_reads_core_endpoints(asgi_client_patch: None) -> None: | |
| """Verify FHIRClient returns JSON payloads for patient-scoped endpoint queries. | |
| Args: | |
| asgi_client_patch (None): Applied fixture to route requests to ASGI app. | |
| Returns: | |
| None: Assertions verify successful responses. | |
| """ | |
| client = FHIRClient(fhir_server_url="http://testserver/fhir", timeout_s=5) | |
| patient = asyncio.run(client.get_patient("pt-001")) | |
| observations = asyncio.run(client.get_observations("pt-001")) | |
| assert patient["resourceType"] == "Patient" | |
| assert observations["resourceType"] == "Bundle" | |
| assert observations["total"] >= 1 | |
| def test_fhir_client_404_returns_empty_dict(asgi_client_patch: None) -> None: | |
| """Verify unknown patient requests return empty dictionaries for 404s. | |
| Args: | |
| asgi_client_patch (None): Applied fixture to route requests to ASGI app. | |
| Returns: | |
| None: Assertions verify 404-empty behaviour. | |
| """ | |
| client = FHIRClient(fhir_server_url="http://testserver/fhir", timeout_s=5) | |
| missing = asyncio.run(client.get_patient("pt-999")) | |
| assert missing == {} | |
| def test_fhir_client_retries_once_on_5xx(monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Verify a single retry occurs after initial server-side error responses. | |
| Args: | |
| monkeypatch (pytest.MonkeyPatch): Pytest monkeypatch fixture. | |
| Returns: | |
| None: Assertions validate retry semantics. | |
| """ | |
| attempts = {"count": 0} | |
| def handler(request: httpx.Request) -> httpx.Response: | |
| attempts["count"] += 1 | |
| if attempts["count"] == 1: | |
| return httpx.Response(status_code=500, json={"detail": "temporary"}) | |
| return httpx.Response(status_code=200, json={"resourceType": "Bundle", "entry": [], "total": 0}) | |
| transport = httpx.MockTransport(handler) | |
| original_async_client = httpx.AsyncClient | |
| def build_client(*args: Any, **kwargs: Any) -> httpx.AsyncClient: | |
| kwargs["transport"] = transport | |
| return original_async_client(*args, **kwargs) | |
| monkeypatch.setattr("backend.fhir.client.httpx.AsyncClient", build_client) | |
| client = FHIRClient(fhir_server_url="http://retry/fhir", timeout_s=5) | |
| response = asyncio.run(client.get_conditions("pt-001")) | |
| assert response["resourceType"] == "Bundle" | |
| assert attempts["count"] == 2 | |
| def test_fhir_client_timeout_raises_contextual_error(monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Verify timeout exceptions are raised with contextual FHIR endpoint details. | |
| Args: | |
| monkeypatch (pytest.MonkeyPatch): Pytest monkeypatch fixture. | |
| Returns: | |
| None: Assertion verifies timeout failure semantics. | |
| """ | |
| class TimeoutClient: | |
| """Minimal async context manager that always raises timeout on GET. | |
| Args: | |
| None: Created by monkeypatch in tests. | |
| Returns: | |
| None: Helper class for timeout simulation. | |
| """ | |
| async def __aenter__(self) -> "TimeoutClient": | |
| """Enter async context manager. | |
| Args: | |
| None: No parameters. | |
| Returns: | |
| TimeoutClient: Context-managed instance. | |
| """ | |
| return self | |
| async def __aexit__(self, exc_type: Any, exc: Any, tb: Any) -> None: | |
| """Exit async context manager. | |
| Args: | |
| exc_type (Any): Exception type when raised. | |
| exc (Any): Exception instance when raised. | |
| tb (Any): Traceback when raised. | |
| Returns: | |
| None: No exception suppression. | |
| """ | |
| return None | |
| async def get(self, url: str, params: dict[str, Any] | None = None) -> httpx.Response: | |
| """Raise timeout for every request. | |
| Args: | |
| url (str): Requested URL string. | |
| params (dict[str, Any] | None): Request query parameters. | |
| Returns: | |
| httpx.Response: Never returns because timeout is raised. | |
| """ | |
| raise httpx.TimeoutException("simulated timeout") | |
| monkeypatch.setattr("backend.fhir.client.httpx.AsyncClient", lambda *a, **k: TimeoutClient()) | |
| client = FHIRClient(fhir_server_url="http://timeout/fhir", timeout_s=1) | |
| with pytest.raises(FHIRClientError, match="timeout"): | |
| asyncio.run(client.get_observations("pt-001")) | |
| def test_tools_and_queries_aggregate_expected_lists(asgi_client_patch: None, monkeypatch: pytest.MonkeyPatch) -> None: | |
| """Verify tool wrappers and query aggregation return resource lists for pt-001. | |
| Args: | |
| asgi_client_patch (None): Applied fixture to route requests to ASGI app. | |
| monkeypatch (pytest.MonkeyPatch): Fixture for replacing default tool client. | |
| Returns: | |
| None: Assertions validate list extraction and aggregation keys. | |
| """ | |
| patched_client = FHIRClient(fhir_server_url="http://testserver/fhir", timeout_s=5) | |
| monkeypatch.setattr(tools, "DEFAULT_FHIR_CLIENT", patched_client) | |
| patients = asyncio.run(tools.search_patients("Thompson")) | |
| conditions = asyncio.run(tools.get_conditions("pt-001")) | |
| context = asyncio.run(queries.get_full_patient_context("pt-001")) | |
| assert patients | |
| assert conditions | |
| assert context["medications"] | |
| assert context["observations"] | |
| assert context["allergies"] | |
| assert "encounters" in context | |