File size: 6,447 Bytes
9636a02
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
"""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


@pytest.fixture
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