Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ info:
http:
method: POST
url: "{{base_url}}/patient/$gpc.getstructuredrecord"
headers:
- name: Content-Type
value: application/fhir+json
body:
type: json
data: |-
Expand Down
24 changes: 24 additions & 0 deletions gateway-api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,30 @@ paths:
diagnostics:
type: string
example: "Patient not found"
'415':
description: Unsupported Media Type - Content-Type header must be "application/fhir+json"
content:
application/fhir+json:
schema:
type: object
properties:
resourceType:
type: string
example: "OperationOutcome"
issue:
type: array
items:
type: object
properties:
severity:
type: string
example: "error"
code:
type: string
example: "invalid"
diagnostics:
type: string
example: 'Unsupported "Content-Type". Expected "application/fhir+json".'
'500':
description: Internal server error
content:
Expand Down
2 changes: 1 addition & 1 deletion gateway-api/src/gateway_api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ def get_structured_record() -> Response:
log_error(e)
response.add_error_response(e)
except Exception:
error = UnexpectedError(traceback=traceback.format_exc())
error = UnexpectedError()
log_error(error)
response.add_error_response(error)

Expand Down
17 changes: 15 additions & 2 deletions gateway-api/src/gateway_api/common/error.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
from dataclasses import dataclass
from http.client import BAD_GATEWAY, BAD_REQUEST, INTERNAL_SERVER_ERROR, NOT_FOUND
from http.client import (
BAD_GATEWAY,
BAD_REQUEST,
INTERNAL_SERVER_ERROR,
NOT_FOUND,
UNSUPPORTED_MEDIA_TYPE,
)

from fhir.stu3 import Issue, IssueCode, IssueSeverity, OperationOutcome

Expand Down Expand Up @@ -110,7 +116,14 @@ class JWTValidationError(AbstractCDGError):


class UnexpectedError(AbstractCDGError):
_message = "Internal Server Error: {traceback}"
_message = "Internal Server Error"
status_code = INTERNAL_SERVER_ERROR
severity = IssueSeverity.ERROR
error_code = IssueCode.EXCEPTION


class UnsupportedMediaTypeError(AbstractCDGError):
_message = "Unsupported Media Type"
status_code = UNSUPPORTED_MEDIA_TYPE
severity = IssueSeverity.ERROR
error_code = IssueCode.INVALID
3 changes: 2 additions & 1 deletion gateway-api/src/gateway_api/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,11 +75,12 @@ def text(self) -> str:

def create_mock_request(headers: dict[str, str], body: dict[str, Any]) -> Request:
"""Create a proper Flask Request object with headers and JSON body."""
content_type = headers.get("Content-Type", "application/fhir+json")
builder = EnvironBuilder(
method="POST",
path="/patient/$gpc.getstructuredrecord",
data=json.dumps(body),
content_type="application/fhir+json",
content_type=content_type,
headers=headers,
)
env = builder.get_environ()
Expand Down
15 changes: 14 additions & 1 deletion gateway-api/src/gateway_api/get_structured_record/request.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
from gateway_api.common.error import (
InvalidRequestJSONError,
MissingOrEmptyHeaderError,
UnsupportedMediaTypeError,
)

ACCEPTED_CONTENT_TYPE = "application/fhir+json"

# Access record structured interaction ID from
# https://developer.nhs.uk/apis/gpconnect/accessrecord_structured_development.html#spine-interactions
ACCESS_RECORD_STRUCTURED_INTERACTION_ID = (
Expand All @@ -32,15 +35,25 @@ class GetStructuredRecordRequest:
def __init__(self, request: Request) -> None:
self._http_request = request
self._headers = CaseInsensitiveDict(request.headers)
self._validate_content_type()
try:
self.parameters = Parameters.model_validate(request.get_json())
self.parameters = Parameters.model_validate(
request.get_json(silent=True, force=True)
)
except (BadRequest, ValidationError) as error:
raise InvalidRequestJSONError() from error

self._status_code: int | None = None

self._validate_headers()

def _validate_content_type(self) -> None:
content_type = self._headers.get("Content-Type")
if content_type is None:
return
if content_type.split(";")[0].strip().lower() != ACCEPTED_CONTENT_TYPE:
raise UnsupportedMediaTypeError()

@property
def trace_id(self) -> str:
trace_id: str = self._headers["Ssp-TraceID"]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from gateway_api.common.error import (
MissingOrEmptyHeaderError,
UnsupportedMediaTypeError,
)
from gateway_api.conftest import create_mock_request
from gateway_api.get_structured_record.request import GetStructuredRecordRequest
Expand Down Expand Up @@ -118,3 +119,33 @@ def test_raises_value_error_when_trace_id_header_is_whitespace(
match='Missing or empty required header "Ssp-TraceID"',
):
GetStructuredRecordRequest(request=mock_request)

def test_missing_content_type_header_is_accepted(
self, valid_simple_request_payload: dict[str, Any]
) -> None:
"""Test that a missing Content-Type header does not raise an error."""
headers = {
"Content-Type": "",
"Ssp-TraceID": "test-trace-id",
"ODS-from": "test-ods",
}
mock_request = create_mock_request(headers, valid_simple_request_payload)

GetStructuredRecordRequest(request=mock_request)

def test_raises_unsupported_media_type_when_content_type_is_invalid(
self, valid_simple_request_payload: dict[str, Any]
) -> None:
"""
Test that UnsupportedMediaTypeError is raised when Content-Type
is not "application/fhir+json".
"""
headers = {
"Content-Type": "application/json",
"Ssp-TraceID": "test-trace-id",
"ODS-from": "test-ods",
}
mock_request = create_mock_request(headers, valid_simple_request_payload)

with pytest.raises(UnsupportedMediaTypeError):
GetStructuredRecordRequest(request=mock_request)
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ def test_add_provider_response_adds_200_status(
assert actual == 200, f"Expected status code to be 200, but got {actual}"

def test_add_error_response_adds_error_response_body(self) -> None:
error = UnexpectedError(traceback="something broke")
error = UnexpectedError()

response = GetStructuredRecordResponse()
response.add_error_response(error)
Expand All @@ -73,7 +73,7 @@ def test_add_error_response_adds_error_response_body(self) -> None:
{
"severity": "error",
"code": "exception",
"diagnostics": "Internal Server Error: something broke",
"diagnostics": "Internal Server Error",
}
],
}
Expand Down
2 changes: 1 addition & 1 deletion gateway-api/tests/schema/test_openapi_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def test_api_schema_compliance(case: Case, base_url: str) -> None:
# GPCAPIM-421
schemathesis.checks.not_a_server_error,
# GPCAPIM-419
schemathesis.checks.missing_required_header,
# schemathesis.checks.missing_required_header,
# GPCAPIM-422
schemathesis.checks.unsupported_method,
],
Expand Down
6 changes: 6 additions & 0 deletions schemathesis.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
[generation]
mode = "all"

[checks.missing_required_header]
expected-statuses = [400]

[checks.negative_data_rejection]
expected-statuses = [400, 401, 403, 404, 406, 415, 422, 428, "5xx"]
Loading