Skip to content

Commit 95539cc

Browse files
authored
Merge pull request #897 from NHSDigital/feature/eema1-NRL-1418-returnOperationOutcomeInSearch
NRL-1418 return an operation outcome issue for invalid search pointers
2 parents 0d3b8ba + d9d2d05 commit 95539cc

File tree

13 files changed

+192
-72
lines changed

13 files changed

+192
-72
lines changed

api/consumer/searchDocumentReference/search_document_reference.py

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from pydantic import ValidationError
22

3-
from nrlf.consumer.fhir.r4.model import Bundle, DocumentReference
3+
from nrlf.consumer.fhir.r4.model import (
4+
Bundle,
5+
DocumentReference,
6+
OperationOutcome,
7+
OperationOutcomeIssue,
8+
)
49
from nrlf.core.codes import SpineErrorConcept
510
from nrlf.core.config import Config
611
from nrlf.core.decorators import request_handler
712
from nrlf.core.dynamodb.repository import DocumentPointerRepository
8-
from nrlf.core.errors import OperationOutcomeError
913
from nrlf.core.logger import LogReference, logger
1014
from nrlf.core.model import ConnectionMetadata, ConsumerRequestParams
1115
from nrlf.core.response import Response, SpineErrorResponse
@@ -120,13 +124,21 @@ def handler(
120124
logger.log(
121125
LogReference.CONSEARCH005, error=str(exc), document=result.document
122126
)
123-
raise OperationOutcomeError(
124-
status_code="500",
125-
severity="error",
126-
code="exception",
127-
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
128-
diagnostics="An error occurred whilst parsing the document reference search results",
129-
) from exc
127+
operation_outcome = OperationOutcome(
128+
resourceType="OperationOutcome",
129+
issue=[
130+
OperationOutcomeIssue(
131+
severity="error",
132+
code="exception",
133+
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
134+
diagnostics="An error occurred whilst parsing the document reference search results",
135+
)
136+
],
137+
)
138+
bundle["total"] += 1
139+
bundle["entry"].append(
140+
{"resource": operation_outcome.model_dump(exclude_none=True)}
141+
)
130142

131143
response = Response.from_resource(Bundle.model_validate(bundle))
132144
logger.log(LogReference.CONSEARCH999)

api/consumer/searchDocumentReference/tests/test_search_document_reference_consumer.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -561,10 +561,13 @@ def test_search_document_reference_invalid_category(
561561
def test_search_document_reference_invalid_json(repository: DocumentPointerRepository):
562562
doc_ref = load_document_reference("Y05868-736253002-Valid")
563563
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
564-
doc_pointer.document = "invalid json"
565-
566564
repository.create(doc_pointer)
567565

566+
doc_pointer_invalid = DocumentPointer.from_document_reference(doc_ref)
567+
doc_pointer_invalid.id = "Y05868-11111-99999-999992"
568+
doc_pointer_invalid.document = "invalid json"
569+
570+
repository.create(doc_pointer_invalid)
568571
event = create_test_api_gateway_event(
569572
headers=create_headers(),
570573
query_string_parameters={
@@ -576,13 +579,14 @@ def test_search_document_reference_invalid_json(repository: DocumentPointerRepos
576579
body = result.pop("body")
577580

578581
assert result == {
579-
"statusCode": "500",
582+
"statusCode": "200",
580583
"headers": default_response_headers(),
581584
"isBase64Encoded": False,
582585
}
583586

584587
parsed_body = json.loads(body)
585-
assert parsed_body == {
588+
589+
expected_operation_outcome = {
586590
"resourceType": "OperationOutcome",
587591
"issue": [
588592
{
@@ -601,3 +605,19 @@ def test_search_document_reference_invalid_json(repository: DocumentPointerRepos
601605
}
602606
],
603607
}
608+
609+
assert parsed_body == {
610+
"resourceType": "Bundle",
611+
"type": "searchset",
612+
"link": [
613+
{
614+
"relation": "self",
615+
"url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191",
616+
}
617+
],
618+
"total": 2,
619+
"entry": [
620+
{"resource": doc_ref.model_dump(exclude_none=True)},
621+
{"resource": expected_operation_outcome},
622+
],
623+
}

api/consumer/searchPostDocumentReference/search_post_document_reference.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@
55
from nrlf.core.config import Config
66
from nrlf.core.decorators import request_handler
77
from nrlf.core.dynamodb.repository import DocumentPointerRepository
8-
from nrlf.core.errors import OperationOutcomeError
98
from nrlf.core.logger import LogReference, logger
109
from nrlf.core.model import ConnectionMetadata, ConsumerRequestParams
1110
from nrlf.core.response import Response, SpineErrorResponse
1211
from nrlf.core.validators import validate_category, validate_type
12+
from nrlf.producer.fhir.r4.model import OperationOutcome, OperationOutcomeIssue
1313

1414

1515
@request_handler(body=ConsumerRequestParams)
@@ -124,13 +124,21 @@ def handler(
124124
logger.log(
125125
LogReference.CONPOSTSEARCH005, error=str(exc), document=result.document
126126
)
127-
raise OperationOutcomeError(
128-
status_code="500",
129-
severity="error",
130-
code="exception",
131-
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
132-
diagnostics="An error occurred whilst parsing the document reference search results",
133-
) from exc
127+
operation_outcome = OperationOutcome(
128+
resourceType="OperationOutcome",
129+
issue=[
130+
OperationOutcomeIssue(
131+
severity="error",
132+
code="exception",
133+
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
134+
diagnostics="An error occurred whilst parsing the document reference search results",
135+
)
136+
],
137+
)
138+
bundle["total"] += 1
139+
bundle["entry"].append(
140+
{"resource": operation_outcome.model_dump(exclude_none=True)}
141+
)
134142

135143
response = Response.from_resource(Bundle.model_validate(bundle))
136144
logger.log(LogReference.CONPOSTSEARCH999)

api/consumer/searchPostDocumentReference/tests/test_search_post_document_reference_consumer.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -480,33 +480,39 @@ def test_search_document_reference_invalid_category(
480480

481481
@mock_aws
482482
@mock_repository
483-
def test_search_post_document_reference_invalid_json(
483+
def test_search_post_document_reference_invalid_json_adds_operation_outcome(
484484
repository: DocumentPointerRepository,
485485
):
486486
doc_ref = load_document_reference("Y05868-736253002-Valid")
487487
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
488-
doc_pointer.document = "invalid json"
489-
490488
repository.create(doc_pointer)
491489

490+
doc_pointer_invalid = DocumentPointer.from_document_reference(doc_ref)
491+
doc_pointer_invalid.id = "Y05868-11111-99999-999992"
492+
doc_pointer_invalid.document = "invalid json"
493+
494+
repository.create(doc_pointer_invalid)
492495
event = create_test_api_gateway_event(
493496
headers=create_headers(),
494497
body=json.dumps(
495-
{"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191"}
498+
{
499+
"subject:identifier": "https://fhir.nhs.uk/Id/nhs-number|6700028191",
500+
}
496501
),
497502
)
498503

499504
result = handler(event, create_mock_context())
500505
body = result.pop("body")
501506

502507
assert result == {
503-
"statusCode": "500",
508+
"statusCode": "200",
504509
"headers": default_response_headers(),
505510
"isBase64Encoded": False,
506511
}
507512

508513
parsed_body = json.loads(body)
509-
assert parsed_body == {
514+
515+
expected_operation_outcome = {
510516
"resourceType": "OperationOutcome",
511517
"issue": [
512518
{
@@ -525,3 +531,19 @@ def test_search_post_document_reference_invalid_json(
525531
}
526532
],
527533
}
534+
535+
assert parsed_body == {
536+
"resourceType": "Bundle",
537+
"type": "searchset",
538+
"link": [
539+
{
540+
"relation": "self",
541+
"url": "https://pytest.api.service.nhs.uk/record-locator/consumer/FHIR/R4/DocumentReference?subject:identifier=https://fhir.nhs.uk/Id/nhs-number|6700028191",
542+
}
543+
],
544+
"total": 2,
545+
"entry": [
546+
{"resource": doc_ref.model_dump(exclude_none=True)},
547+
{"resource": expected_operation_outcome},
548+
],
549+
}

api/consumer/swagger.yaml

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -911,16 +911,18 @@ components:
911911
fullUrl:
912912
type: string
913913
pattern: \S*
914-
description: "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource – i.e. if the fullUrl is not a urn:uuid, the URL shall be version–independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified."
914+
description: "The Absolute URL for the resource. The fullUrl SHALL NOT disagree with the id in the resource – i.e. if the fullUrl is not a urn:uuid, the URL shall be version–independent URL consistent with the Resource.id. The fullUrl is a version independent reference to the resource. The fullUrl element SHALL have a value except that: \n* fullUrl can be empty on a POST (although it does not need to when specifying a temporary id for reference in the bundle)\n* Results from operations might involve resources that are not identified."
915915
resource:
916-
$ref: "#/components/schemas/DocumentReference"
916+
oneOf:
917+
- $ref: "#/components/schemas/DocumentReference"
918+
- $ref: "#/components/schemas/OperationOutcome"
917919
description: The Resource for the entry. The purpose/meaning of the resource is determined by the Bundle.type.
918920
search:
919921
$ref: "#/components/schemas/BundleEntrySearch"
920922
description: Information about the search process that lead to the creation of this entry.
921923
request:
922924
$ref: "#/components/schemas/BundleEntryRequest"
923-
description: Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry.
925+
description: Additional information about how this entry should be processed as part of a transaction or batch. For history, it shows how the entry was processed to create the version contained in the entry.
924926
response:
925927
$ref: "#/components/schemas/BundleEntryResponse"
926928
description: Indicates the results of processing the corresponding 'request' entry in the batch or transaction being responded to or what the results of an operation where when returning history.

api/producer/searchDocumentReference/search_document_reference.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
from nrlf.core.codes import SpineErrorConcept
44
from nrlf.core.decorators import DocumentPointerRepository, request_handler
5-
from nrlf.core.errors import OperationOutcomeError
65
from nrlf.core.logger import LogReference, logger
76
from nrlf.core.model import ConnectionMetadata, ProducerRequestParams
87
from nrlf.core.response import Response, SpineErrorResponse
98
from nrlf.core.validators import validate_category, validate_type
10-
from nrlf.producer.fhir.r4.model import Bundle, DocumentReference
9+
from nrlf.producer.fhir.r4.model import (
10+
Bundle,
11+
DocumentReference,
12+
OperationOutcome,
13+
OperationOutcomeIssue,
14+
)
1115

1216

1317
@request_handler(params=ProducerRequestParams)
@@ -105,12 +109,20 @@ def handler(
105109
logger.log(
106110
LogReference.PROSEARCH005, error=str(exc), document=result.document
107111
)
108-
raise OperationOutcomeError(
109-
status_code="500",
110-
severity="error",
111-
code="exception",
112-
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
113-
diagnostics="An error occurred whilst parsing the document reference search results",
112+
operation_outcome = OperationOutcome(
113+
resourceType="OperationOutcome",
114+
issue=[
115+
OperationOutcomeIssue(
116+
severity="error",
117+
code="exception",
118+
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
119+
diagnostics="An error occurred whilst parsing the document reference search results",
120+
)
121+
],
122+
)
123+
bundle["total"] += 1
124+
bundle["entry"].append(
125+
{"resource": operation_outcome.model_dump(exclude_none=True)}
114126
)
115127

116128
response = Response.from_resource(Bundle.model_validate(bundle))

api/producer/searchDocumentReference/tests/test_search_document_reference_producer.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -460,10 +460,13 @@ def test_search_document_reference_filters_by_pointer_types(
460460
def test_search_document_reference_invalid_json(repository: DocumentPointerRepository):
461461
doc_ref = load_document_reference("Y05868-736253002-Valid")
462462
doc_pointer = DocumentPointer.from_document_reference(doc_ref)
463-
doc_pointer.document = "invalid json"
464-
465463
repository.create(doc_pointer)
466464

465+
doc_pointer_invalid = DocumentPointer.from_document_reference(doc_ref)
466+
doc_pointer_invalid.id = "Y05868-11111-99999-999992"
467+
doc_pointer_invalid.document = "invalid json"
468+
469+
repository.create(doc_pointer_invalid)
467470
event = create_test_api_gateway_event(
468471
headers=create_headers(),
469472
query_string_parameters={
@@ -475,13 +478,14 @@ def test_search_document_reference_invalid_json(repository: DocumentPointerRepos
475478
body = result.pop("body")
476479

477480
assert result == {
478-
"statusCode": "500",
481+
"statusCode": "200",
479482
"headers": default_response_headers(),
480483
"isBase64Encoded": False,
481484
}
482485

483486
parsed_body = json.loads(body)
484-
assert parsed_body == {
487+
488+
expected_operation_outcome = {
485489
"resourceType": "OperationOutcome",
486490
"issue": [
487491
{
@@ -500,3 +504,13 @@ def test_search_document_reference_invalid_json(repository: DocumentPointerRepos
500504
}
501505
],
502506
}
507+
508+
assert parsed_body == {
509+
"resourceType": "Bundle",
510+
"type": "searchset",
511+
"total": 2,
512+
"entry": [
513+
{"resource": doc_ref.model_dump(exclude_none=True)},
514+
{"resource": expected_operation_outcome},
515+
],
516+
}

api/producer/searchPostDocumentReference/search_post_document_reference.py

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22

33
from nrlf.core.codes import SpineErrorConcept
44
from nrlf.core.decorators import DocumentPointerRepository, request_handler
5-
from nrlf.core.errors import OperationOutcomeError
65
from nrlf.core.logger import LogReference, logger
76
from nrlf.core.model import ConnectionMetadata, ProducerRequestParams
87
from nrlf.core.response import Response, SpineErrorResponse
98
from nrlf.core.validators import validate_category, validate_type
10-
from nrlf.producer.fhir.r4.model import Bundle, DocumentReference
9+
from nrlf.producer.fhir.r4.model import (
10+
Bundle,
11+
DocumentReference,
12+
OperationOutcome,
13+
OperationOutcomeIssue,
14+
)
1115

1216

1317
@request_handler(body=ProducerRequestParams)
@@ -99,12 +103,20 @@ def handler(
99103
logger.log(
100104
LogReference.PROPOSTSEARCH005, error=str(exc), document=result.document
101105
)
102-
raise OperationOutcomeError(
103-
status_code="500",
104-
severity="error",
105-
code="exception",
106-
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
107-
diagnostics="An error occurred whilst parsing the document reference search results",
106+
operation_outcome = OperationOutcome(
107+
resourceType="OperationOutcome",
108+
issue=[
109+
OperationOutcomeIssue(
110+
severity="error",
111+
code="exception",
112+
details=SpineErrorConcept.from_code("INTERNAL_SERVER_ERROR"),
113+
diagnostics="An error occurred whilst parsing the document reference search results",
114+
)
115+
],
116+
)
117+
bundle["total"] += 1
118+
bundle["entry"].append(
119+
{"resource": operation_outcome.model_dump(exclude_none=True)}
108120
)
109121

110122
logger.log(LogReference.PROPOSTSEARCH999)

0 commit comments

Comments
 (0)