generated from NHSDigital/repository-template
-
Notifications
You must be signed in to change notification settings - Fork 1
Feature/gpcapim 251 #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
DWolfsNHS
wants to merge
20
commits into
main
Choose a base branch
from
feature/GPCAPIM-251
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+505
−0
Open
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
d6b5fc9
[GPCAPIM-251] initial commit with basic file structure and descriptions
DWolfsNHS e124406
[GPCAPIM-251]: Implement GPProviderClient for FHIR API interaction
DWolfsNHS 296403f
[GPCAPIM-251]: Refactor GPProviderClient and enhance test coverage
DWolfsNHS 02fa73e
[GPCAPIM-251]: Update GPProviderClient methods for FHIR API interaction
DWolfsNHS 7908028
[GPCAPIM-251]: Update GPProviderClient and stub for FHIR API interaction
DWolfsNHS f6680ee
[GPCAPIM-251]: Refactor GPProvider class names for consistency and sw…
DWolfsNHS deae9e6
[GPCAPIM-251]: Enhance GPProviderClient to include headers for FHIR A…
DWolfsNHS 2ea0376
[GPCAPIM-251]: Update GpProviderClient to handle request body
DWolfsNHS 74f0db7
[GPCAPIM-251]: Add tests for GPProviderClient response handling
DWolfsNHS e0e80be
[GPCAPIM-251]: Refactor response handling in GPProviderStub and tests
DWolfsNHS 5ecc05c
[GPCAPIM-251]: Refactor response handling in stub provider and tests
DWolfsNHS ecb046d
[GPCAPIM-251]: Refactor GPProviderClient to improve request handling
DWolfsNHS b5448c5
[GPCAPIM-251]: cleanup and update documentation
DWolfsNHS 7b94e69
[GPCAPIM-251]: Add error handling test for access_structured_record
DWolfsNHS 2026888
[GPCAPIM-251]: Address peer feedback
DWolfsNHS a25d463
[GPCAPIM-251]: PR feedback
DWolfsNHS fc4aa34
[GPCAPIM-251]: Update remaining provider endpoint in tests
DWolfsNHS a105270
[GPCAPIM-251]: Fix constant naming for interaction ID
DWolfsNHS e790853
[GPCAPIM-251]: Implement stub provider for development
DWolfsNHS fddb69e
[GPCAPIM-251]: Refactor provider request handling and update tests
DWolfsNHS File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,146 @@ | ||
| """ | ||
| Module: gateway_api.provider_request | ||
|
|
||
| This module contains the GpProviderClient class, which provides a | ||
| simple client for interacting with the GPProvider FHIR GP System. | ||
|
|
||
| The GpProviderClient class includes methods to fetch structured patient | ||
| records from a GPProvider FHIR API endpoint. | ||
|
|
||
| Usage: | ||
| Instantiate a GpProviderClient with: | ||
| - provider_endpoint: The FHIR API endpoint for the provider. | ||
| - provider_asid: The ASID for the provider. | ||
| - consumer_asid: The ASID for the consumer. | ||
|
|
||
| Use the `access_structured_record` method to fetch a structured patient record: | ||
| Parameters: | ||
| - trace_id (str): A unique identifier for the request. | ||
| - body (str): The request body in FHIR format. | ||
|
|
||
| Returns: | ||
| The response from the provider FHIR API. | ||
| """ | ||
|
|
||
| from collections.abc import Callable | ||
| from urllib.parse import urljoin | ||
|
|
||
| from requests import HTTPError, Response | ||
| from stubs.stub_provider import GpProviderStub | ||
|
|
||
| ARS_INTERACTION_ID = ( | ||
| "urn:nhs:names:services:gpconnect:structured" | ||
| ":fhir:operation:gpc.getstructuredrecord-1" | ||
| ) | ||
| ARS_FHIR_BASE = "FHIR/STU3" | ||
| FHIR_RESOURCE = "patient" | ||
| ARS_FHIR_OPERATION = "$gpc.getstructuredrecord" | ||
| TIMEOUT: int | None = None # None used for quicker dev, adjust as needed | ||
|
|
||
| # Direct all requests to the stub provider for steel threading in dev. | ||
| # Replace with `from requests import post` for real requests. | ||
| PostCallable = Callable[..., Response] | ||
| _provider_stub = GpProviderStub() | ||
|
|
||
|
|
||
| def _stubbed_post(trace_id: str, body: str) -> Response: | ||
| """A stubbed requests.post function that routes to the GPProviderStub.""" | ||
| return _provider_stub.access_record_structured(trace_id, body) | ||
|
|
||
|
|
||
| post: PostCallable = _stubbed_post | ||
|
|
||
|
|
||
| class ExternalServiceError(Exception): | ||
| """ | ||
| Exception raised when the downstream GPProvider FHIR API request fails. | ||
| """ | ||
|
|
||
|
|
||
| class GpProviderClient: | ||
| """ | ||
| A client for interacting with the GPProvider FHIR GP System. | ||
|
|
||
| This class provides methods to interact with the GPProvider FHIR API, | ||
| including fetching structured patient records. | ||
|
|
||
| Attributes: | ||
| provider_endpoint (str): The FHIR API endpoint for the provider. | ||
| provider_asid (str): The ASID for the provider. | ||
| consumer_asid (str): The ASID for the consumer. | ||
|
|
||
| Methods: | ||
| access_structured_record(trace_id: str, body: str) -> Response: | ||
| Fetch a structured patient record from the GPProvider FHIR API. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| provider_endpoint: str, | ||
| provider_asid: str, | ||
| consumer_asid: str, | ||
| ) -> None: | ||
| self.provider_endpoint = provider_endpoint | ||
| self.provider_asid = provider_asid | ||
| self.consumer_asid = consumer_asid | ||
|
|
||
| def _build_headers(self, trace_id: str) -> dict[str, str]: | ||
| """ | ||
| Build the headers required for the GPProvider FHIR API request. | ||
|
|
||
| Args: | ||
| trace_id (str): A unique identifier for the request. | ||
|
|
||
| Returns: | ||
| dict[str, str]: A dictionary containing the headers for the request, | ||
| including content type, interaction ID, and ASIDs for the provider | ||
| and consumer. | ||
| """ | ||
| return { | ||
| "Content-Type": "application/fhir+json", | ||
| "Accept": "application/fhir+json", | ||
| "Ssp-InteractionID": ARS_INTERACTION_ID, | ||
| "Ssp-To": self.provider_asid, | ||
| "Ssp-From": self.consumer_asid, | ||
| "Ssp-TraceID": trace_id, | ||
| } | ||
|
|
||
| def access_structured_record( | ||
| self, | ||
| trace_id: str, | ||
| body: str, | ||
| ) -> Response: | ||
| """ | ||
| Fetch a structured patient record from the GPProvider FHIR API. | ||
|
|
||
| Args: | ||
| trace_id (str): A unique identifier for the request, passed in the headers. | ||
| body (str): The request body in FHIR format. | ||
|
|
||
| Returns: | ||
| Response: The response from the GPProvider FHIR API. | ||
|
|
||
| Raises: | ||
| ExternalServiceError: If the API request fails with an HTTP error. | ||
| """ | ||
|
|
||
| headers = self._build_headers(trace_id) | ||
|
|
||
| endpoint_path = "/".join([ARS_FHIR_BASE, FHIR_RESOURCE, ARS_FHIR_OPERATION]) | ||
| url = urljoin(self.provider_endpoint, endpoint_path) | ||
|
|
||
| response = post( | ||
| url, | ||
| headers=headers, | ||
| data=body, | ||
| timeout=TIMEOUT, | ||
| ) | ||
|
|
||
| try: | ||
| response.raise_for_status() | ||
| except HTTPError as err: | ||
| raise ExternalServiceError( | ||
| f"GPProvider FHIR API request failed:{err.response.reason}" | ||
| ) from err | ||
|
|
||
| return response | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,225 @@ | ||
| """ | ||
| Unit tests for :mod:`gateway_api.provider_request`. | ||
|
|
||
| This module contains unit tests for the `GpProviderClient` class, which is responsible | ||
| for interacting with the GPProvider FHIR API. | ||
|
|
||
| """ | ||
|
|
||
| from typing import Any | ||
|
|
||
| import pytest | ||
| from requests import Response | ||
| from requests.structures import CaseInsensitiveDict | ||
| from stubs.stub_provider import GpProviderStub | ||
|
|
||
| from gateway_api import provider_request | ||
| from gateway_api.provider_request import ExternalServiceError, GpProviderClient | ||
|
|
||
| ars_interactionId = ( | ||
| "urn:nhs:names:services:gpconnect:structured" | ||
| ":fhir:operation:gpc.getstructuredrecord-1" | ||
| ) | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def stub() -> GpProviderStub: | ||
| return GpProviderStub() | ||
|
|
||
|
|
||
| @pytest.fixture | ||
| def mock_request_post( | ||
| monkeypatch: pytest.MonkeyPatch, stub: GpProviderStub | ||
| ) -> dict[str, Any]: | ||
| """ | ||
| Fixture to patch the `requests.post` method for testing. | ||
|
|
||
| This fixture intercepts calls to `requests.post` and routes them to the | ||
| stub provider. It also captures the most recent request details, such as | ||
| headers, body, and URL, for verification in tests. | ||
|
|
||
| Returns: | ||
| dict[str, Any]: A dictionary containing the captured request details. | ||
| """ | ||
| capture: dict[str, Any] = {} | ||
|
|
||
| def _fake_post( | ||
| url: str, | ||
| headers: CaseInsensitiveDict[str], | ||
| data: str, | ||
| timeout: int, | ||
| ) -> Response: | ||
| """A fake requests.post implementation.""" | ||
|
|
||
| capture["headers"] = dict(headers) | ||
| capture["data"] = data | ||
| capture["url"] = url | ||
|
|
||
| # Provide dummy or captured arguments as required by the stub signature | ||
| return stub.access_record_structured( | ||
| trace_id=headers.get("Ssp-TraceID", "dummy-trace-id"), body=data | ||
| ) | ||
|
|
||
| monkeypatch.setattr(provider_request, "post", _fake_post) | ||
| return capture | ||
|
|
||
|
|
||
| def test_valid_gpprovider_access_structured_record_makes_request_correct_url_post_200( | ||
| mock_request_post: dict[str, Any], | ||
| stub: GpProviderStub, | ||
| ) -> None: | ||
| """ | ||
| Test that the `access_structured_record` method constructs the correct URL | ||
| for the GPProvider FHIR API request and receives a 200 OK response. | ||
|
|
||
| This test verifies that the URL includes the correct FHIR base path and | ||
| operation for accessing a structured patient record. | ||
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://test.com" | ||
| trace_id = "some_uuid_value" | ||
|
|
||
| client = GpProviderClient( | ||
| provider_endpoint=provider_endpoint, | ||
| provider_asid=provider_asid, | ||
| consumer_asid=consumer_asid, | ||
| ) | ||
|
|
||
| result = client.access_structured_record(trace_id, "body") | ||
|
|
||
| captured_url = mock_request_post.get("url", provider_endpoint) | ||
|
|
||
| assert ( | ||
| captured_url | ||
| == provider_endpoint + "/FHIR/STU3/patient/$gpc.getstructuredrecord" | ||
| ) | ||
| assert result.status_code == 200 | ||
|
|
||
|
|
||
| def test_valid_gpprovider_access_structured_record_with_correct_headers_post_200( | ||
| mock_request_post: dict[str, Any], | ||
| stub: GpProviderStub, | ||
| ) -> None: | ||
| """ | ||
| Test that the `access_structured_record` method includes the correct headers | ||
| in the GPProvider FHIR API request and receives a 200 OK response. | ||
|
|
||
| This test verifies that the headers include: | ||
| - Content-Type and Accept headers for FHIR+JSON. | ||
| - Ssp-TraceID, Ssp-From, Ssp-To, and Ssp-InteractionID for GPConnect. | ||
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://test.com" | ||
| trace_id = "some_uuid_value" | ||
|
|
||
| client = GpProviderClient( | ||
| provider_endpoint=provider_endpoint, | ||
| provider_asid=provider_asid, | ||
| consumer_asid=consumer_asid, | ||
| ) | ||
| expected_headers = { | ||
| "Content-Type": "application/fhir+json", | ||
| "Accept": "application/fhir+json", | ||
| "Ssp-TraceID": str(trace_id), | ||
| "Ssp-From": consumer_asid, | ||
| "Ssp-To": provider_asid, | ||
| "Ssp-InteractionID": ars_interactionId, | ||
| } | ||
|
|
||
| result = client.access_structured_record(trace_id, "body") | ||
|
|
||
| captured_headers = mock_request_post["headers"] | ||
|
|
||
| assert expected_headers == captured_headers | ||
| assert result.status_code == 200 | ||
|
|
||
|
|
||
| def test_valid_gpprovider_access_structured_record_with_correct_body_200( | ||
| mock_request_post: dict[str, Any], | ||
| stub: GpProviderStub, | ||
| ) -> None: | ||
| """ | ||
| Test that the `access_structured_record` method includes the correct body | ||
| in the GPProvider FHIR API request and receives a 200 OK response. | ||
|
|
||
| This test verifies that the request body matches the expected FHIR parameters | ||
| resource sent to the GPProvider API. | ||
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://test.com" | ||
| trace_id = "some_uuid_value" | ||
|
|
||
| request_body = "some_FHIR_request_params" | ||
|
|
||
| client = GpProviderClient( | ||
| provider_endpoint=provider_endpoint, | ||
| provider_asid=provider_asid, | ||
| consumer_asid=consumer_asid, | ||
| ) | ||
|
|
||
| result = client.access_structured_record(trace_id, request_body) | ||
|
|
||
| captured_body = mock_request_post["data"] | ||
|
|
||
| assert result.status_code == 200 | ||
| assert captured_body == request_body | ||
|
|
||
|
|
||
| def test_valid_gpprovider_access_structured_record_returns_stub_response_200( | ||
| mock_request_post: dict[str, Any], | ||
| stub: GpProviderStub, | ||
| ) -> None: | ||
| """ | ||
| Test that the `access_structured_record` method returns the same response | ||
| as provided by the stub provider. | ||
|
|
||
| This test verifies that the response from the GPProvider FHIR API matches | ||
| the expected response, including the status code and content. | ||
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://test.com" | ||
| trace_id = "some_uuid_value" | ||
|
|
||
| client = GpProviderClient( | ||
| provider_endpoint=provider_endpoint, | ||
| provider_asid=provider_asid, | ||
| consumer_asid=consumer_asid, | ||
| ) | ||
|
|
||
| expected_response = stub.access_record_structured(trace_id, "body") | ||
|
|
||
| result = client.access_structured_record(trace_id, "body") | ||
|
|
||
| assert result.status_code == 200 | ||
| assert result.content == expected_response.content | ||
|
|
||
|
|
||
| def test_access_structured_record_raises_external_service_error( | ||
| mock_request_post: dict[str, Any], | ||
| stub: GpProviderStub, | ||
| monkeypatch: pytest.MonkeyPatch, | ||
| ) -> None: | ||
| """ | ||
| Test that the `access_structured_record` method raises an `ExternalServiceError` | ||
| when the GPProvider FHIR API request fails with an HTTP error. | ||
| """ | ||
| provider_asid = "200000001154" | ||
| consumer_asid = "200000001152" | ||
| provider_endpoint = "https://test.com" | ||
| trace_id = "invalid for test" | ||
|
|
||
| client = GpProviderClient( | ||
| provider_endpoint=provider_endpoint, | ||
| provider_asid=provider_asid, | ||
| consumer_asid=consumer_asid, | ||
| ) | ||
|
|
||
| with pytest.raises( | ||
| ExternalServiceError, | ||
| match="GPProvider FHIR API request failed:Bad Request", | ||
| ): | ||
| client.access_structured_record(trace_id, "body") |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we could move this into a common library. I've started one in my current ticket anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed - happy to pattern here, or integrate this as we merge/rebase