Metadata-Version: 2.5
Name: fhir-sdc-testkit
Version: 3.0.0a2
Summary: Drive an SDC Questionnaire through $populate, answers and $extract in tests
Project-URL: Homepage, https://github.com/beda-software/fhir-sdc
Project-URL: Source, https://github.com/beda-software/fhir-sdc/tree/master/fhir-sdc-testkit
Author-email: "beda.software" <hi@beda.software>
License-Expression: MIT
License-File: LICENSE
Keywords: fhir,pytest,questionnaire,sdc,testing
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Requires-Dist: fhirpy-types-r4b>=0.1.1
Requires-Dist: fhirpy>=2.2.0
Description-Content-Type: text/markdown

# fhir-sdc-testkit

Test an SDC Questionnaire and its extraction the way a form is really filled: `$populate` runs, answers are placed at
each `linkId`'s nesting, then `QuestionnaireResponse/$extract` runs. With `submit=True` the QuestionnaireResponse is
saved before extraction and the bundle is submitted after it. Only what a user types (or the frontend calculates) is
faked; everything else runs on
a real server implementing the [SDC](https://build.fhir.org/ig/HL7/sdc/) operations, such as
[fhir-sdc](../README.md).

```sh
pip install fhir-sdc-testkit
```

It is released from the fhir-sdc repository, with the same version as the fhir-sdc release it is tested against.

## The client

Everything takes an `AsyncFHIRClient` rooted at the server's `/fhir` base, built with `dump_resource` — fhirpy cannot
serialize pydantic resources without it:

```python
fhir_client = AsyncFHIRClient(
    f"{base_url}/fhir",
    authorization=authorization,
    dump_resource=lambda resource: resource.model_dump(),
)
```

## Usage

```python
import fhirpy_types_r4b as r4b
from fhir_sdc_testkit import answer, launch_context, populate_and_extract


async def test_intake_records_the_address(fhir_client):
    patient = await fhir_client.create(r4b.Patient(name=[r4b.HumanName(family="Test")]))

    await populate_and_extract(
        fhir_client,
        "patient-intake",
        parameters=launch_context(
            {"Patient": patient}, subject=r4b.Reference(reference=f"Patient/{patient.id}")
        ),
        answers={"postal-code": [answer(valueString="75001")]},
        submit=True,
    )

    updated = await fhir_client.get(r4b.Patient, patient.id)
    assert updated.address[0].postalCode == "75001"
```

`populate_and_extract` returns the QuestionnaireResponse and the bundle `$extract` returned (`None` when there is
nothing to extract). Without `submit` neither is saved, so the bundle can be asserted on directly. With it, the
response is the saved one — saved first, since a Mapping may reference it by id — and the bundle has been submitted.

`populate` and `extract` run the two operations on their own and save nothing.

### The Questionnaire

`populate_and_extract` and `populate` take an `r4b.Questionnaire`, saved or not (an assembled one, for example), or a
string, found by canonical (`url|version` or `url`) and then by id. A string matching several versions or nothing
raises `LookupError`. `$populate` and `$extract` always receive the resource itself.

### Answers

Keys are `linkId`s, addressed flatly — groups in between are created for you:

```python
answers={
    "country": [answer(valueCoding=r4b.Coding(code="FR"))],
    "postal-code": [answer(valueString="75001")],
}
```

A repeating group takes one answer set per occurrence, filled positionally over the occurrences `$populate` produced,
appending any that are missing and leaving extra populated ones alone:

```python
answers={
    "contacts": [
        {"phone": [answer(valueString="+1 555 0100")], "label": [answer(valueString="home")]},
        {"phone": [answer(valueString="+1 555 0111")], "label": [answer(valueString="work")]},
    ],
}
```

Inside a set, `linkId`s are flat again. A `linkId` that does not belong to the group raises, naming it.

### Parameters

`parameters` is required: it is passed to `$populate` and merged into the `$extract` call, so anything the server
accepts can go in it. `launch_context()` builds the SDC `$populate` input — a resource or a `Reference` under each
context name, and the optional `subject`. Pass at least one of them: a `$populate` reply that is not `Parameters`
raises `ValueError`.

```python
parameters=launch_context({"Patient": patient, "Author": r4b.Reference(reference="Practitioner/1")})
```

A list under a name builds one `context` with a `content` part per value, as SDC defines it. fhir-sdc uses only the
first `content` for now.

## Failures

An error response from the server raises fhirpy's exception for its status — `OperationOutcome` for most 4xx and 5xx
responses. A violated `itemConstraint` the server checks during `$extract` and a rejected transaction both arrive this
way; they differ in `issue[].code`. fhir-sdc reports a constraint's `key`, and a server's own validation code, such as
`invalid`, for a transaction. An `error` or `fatal` issue in the `$extract` result and, with `submit`, a batch entry
the server rejects raise `ExtractionError`.

```python
with pytest.raises(OperationOutcome) as refused:
    await populate_and_extract(fhir_client, "patient-intake", parameters=parameters, answers=answers)
assert refused.value.resource["issue"][0]["code"] == "postal-code-required"
```

## Permissions

The client's credentials need these requests, relative to its base URL:

| Request                                         | When                                    |
| ----------------------------------------------- | --------------------------------------- |
| `GET Questionnaire?url=…`, `GET Questionnaire?_id=…` | the Questionnaire is passed as a string |
| `POST Questionnaire/$populate`                  | `populate`, `populate_and_extract`      |
| `POST QuestionnaireResponse/$extract`           | `extract`, `populate_and_extract`       |
| `POST QuestionnaireResponse`                    | `populate_and_extract` with `submit`    |
| `POST /` (the extracted transaction or batch)   | `populate_and_extract` with `submit`    |

With `submit`, the bundle's entries also need the access their own requests do, such as `POST Patient`. The
operations may need more: fhir-sdc runs them with the caller's credentials, reading the `subject` and `Reference`
context values, x-fhir-query expressions, source queries (as a `POST /` batch) and, in `$extract`, the Mappings.

## Development

```sh
uv sync
uv run pytest
uv run ruff format . && uv run ruff check . && uv run mypy
```

That suite runs without a server. `$populate`, constraints and extraction are covered against a running Aidbox and
fhir-sdc by `tests/testkit/` in the repository root — `./run_test.sh tests/testkit`.
