From 4ed5144be5d27edbd3bbc9f0d8d48d8ab48fe6ec Mon Sep 17 00:00:00 2001 From: eesa456 Date: Thu, 7 Aug 2025 12:44:16 +0100 Subject: [PATCH 01/20] NRL-1554 Add new content retrieval mechanism extension --- api/consumer/swagger.yaml | 53 +++++++++++- api/producer/swagger.yaml | 53 +++++++++++- layer/nrlf/consumer/fhir/r4/model.py | 24 +++++- layer/nrlf/core/constants.py | 8 ++ layer/nrlf/core/errors.py | 14 ++- layer/nrlf/core/tests/test_pydantic_errors.py | 15 ++-- layer/nrlf/core/tests/test_validators.py | 86 +++++++++++++++++++ layer/nrlf/core/validators.py | 52 ++++++++++- layer/nrlf/producer/fhir/r4/model.py | 24 +++++- layer/nrlf/producer/fhir/r4/strict_model.py | 24 +++++- 10 files changed, 329 insertions(+), 24 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 82a711a4a..95f4a9702 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -810,10 +810,12 @@ components: extension: type: array items: - $ref: "#/components/schemas/ContentStabilityExtension" + oneOf: + - $ref: "#/components/schemas/ContentStabilityExtension" + - $ref: "#/components/schemas/RetrievalMechanismExtension" description: Additional extension for content stability. minItems: 1 - maxItems: 1 + maxItems: 2 required: - attachment - format @@ -975,6 +977,53 @@ components: - system - code - display + RetrievalMechanismExtension: + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + valueCodeableConcept: + $ref: "#/components/schemas/RetrievalMechanismExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + RetrievalMechanismExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object + properties: + coding: + type: array + items: + $ref: "#/components/schemas/RetrievalMechanismExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + RetrievalMechanismExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" + code: + type: string + enum: ["SSP", "Direct", "NDR"] + display: + type: string + enum: + ["Spine Secure Proxy", "National Document Repository", "Direct"] + required: + - system + - code + - display NRLFormatCode: allOf: - $ref: "#/components/schemas/Coding" diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index bb2289946..7bd2e8b2a 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1415,10 +1415,12 @@ components: extension: type: array items: - $ref: "#/components/schemas/ContentStabilityExtension" + oneOf: + - $ref: "#/components/schemas/ContentStabilityExtension" + - $ref: "#/components/schemas/RetrievalMechanismExtension" description: Additional extension for content stability. minItems: 1 - maxItems: 1 + maxItems: 2 required: - attachment - format @@ -1631,6 +1633,53 @@ components: - system - code - display + RetrievalMechanismExtension: + allOf: + - $ref: "#/components/schemas/Extension" + - type: object + properties: + url: + type: string + enum: + - "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + valueCodeableConcept: + $ref: "#/components/schemas/RetrievalMechanismExtensionValueCodeableConcept" + required: + - url + - valueCodeableConcept + RetrievalMechanismExtensionValueCodeableConcept: + allOf: + - $ref: "#/components/schemas/CodeableConcept" + - type: object + properties: + coding: + type: array + items: + $ref: "#/components/schemas/RetrievalMechanismExtensionCoding" + minItems: 1 + maxItems: 1 + required: + - coding + RetrievalMechanismExtensionCoding: + allOf: + - $ref: "#/components/schemas/Coding" + - type: object + properties: + system: + type: string + enum: + - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" + code: + type: string + enum: ["SSP", "Direct", "NDR"] + display: + type: string + enum: + ["Spine Secure Proxy", "National Document Repository", "Direct"] + required: + - system + - code + - display NRLFormatCode: allOf: - $ref: "#/components/schemas/Coding" diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 898ddfe53..70f794a05 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-06-13T14:43:32+00:00 +# timestamp: 2025-08-07T11:40:24+00:00 from __future__ import annotations @@ -240,6 +240,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "NDR"] + display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -501,6 +507,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -574,6 +586,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -633,7 +652,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], + Field(max_length=2, min_length=1), ] diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 17236ac77..6346242e1 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -680,6 +680,14 @@ def coding_value(self): CONTENT_STABILITY_SYSTEM_URL = ( "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ) +CONTENT_RETRIEVAL_SYSTEM_URL = ( + "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" +) +CONTENT_RETRIEVAL_CODE_MAP = { + "Direct": "Direct", + "SSP": "Spine Secure Proxy", + "NDR": "National Document Repository", +} CONTENT_FORMAT_CODE_URL = "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" CONTENT_FORMAT_CODE_MAP = { "urn:nhs-ic:record-contact": "Contact details (HTTP Unsecured)", diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 4280801b0..2d4c34805 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -3,7 +3,11 @@ from pydantic import ValidationError from pydantic_core import ErrorDetails -from nrlf.core.constants import CONTENT_FORMAT_CODE_URL, CONTENT_STABILITY_SYSTEM_URL +from nrlf.core.constants import ( + CONTENT_FORMAT_CODE_URL, + CONTENT_RETRIEVAL_SYSTEM_URL, + CONTENT_STABILITY_SYSTEM_URL, +) from nrlf.core.response import Response from nrlf.core.types import CodeableConcept from nrlf.producer.fhir.r4 import model as producer_model @@ -11,8 +15,12 @@ def format_error_location(loc: List) -> str: + # List of extension class names to exclude from error paths + exclude_classes = {"ContentStabilityExtension", "RetrievalMechanismExtension"} + filtered_loc = [each for each in loc if each not in exclude_classes] + formatted_loc = "" - for each in loc: + for each in filtered_loc: if isinstance(each, int): formatted_loc = f"{formatted_loc}[{each}]" else: @@ -26,7 +34,7 @@ def append_value_set_url(loc_string: str) -> str: if "content" in loc_string: if "extension" in loc_string: - return f". See ValueSet: {CONTENT_STABILITY_SYSTEM_URL}" + return f". See ValueSets: {CONTENT_STABILITY_SYSTEM_URL} & {CONTENT_RETRIEVAL_SYSTEM_URL}" if "format" in loc_string: return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}" diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 9a06a116e..63f600cfd 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -99,6 +99,11 @@ def test_validate_content_multiple_content_stability_extensions(): document_ref_data["content"][0]["extension"][0] ) + # Add a third duplicate contentStability extension + document_ref_data["content"][0]["extension"].append( + document_ref_data["content"][0]["extension"][0] + ) + with pytest.raises(ParseError) as error: validator.validate(document_ref_data) @@ -116,7 +121,7 @@ def test_validate_content_multiple_content_stability_extensions(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 1 item after validation, not 2. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 2 items after validation, not 3. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", "expression": ["content[0].extension"], } @@ -133,7 +138,6 @@ def test_validate_content_invalid_content_stability_code(): validator.validate(document_ref_data) exc = error.value - assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -146,7 +150,7 @@ def test_validate_content_invalid_content_stability_code(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], } @@ -163,7 +167,6 @@ def test_validate_content_invalid_content_stability_display(): validator.validate(document_ref_data) exc = error.value - assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -176,7 +179,7 @@ def test_validate_content_invalid_content_stability_display(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", "expression": [ "content[0].extension[0].valueCodeableConcept.coding[0].display" ], @@ -195,7 +198,6 @@ def test_validate_content_invalid_content_stability_system(): validator.validate(document_ref_data) exc = error.value - assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -224,7 +226,6 @@ def test_validate_content_invalid_content_stability_url(): validator.validate(document_ref_data) exc = error.value - assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 9f0f54f0e..1b02d34f7 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1365,6 +1365,92 @@ def test_validate_content_extension_invalid_code_and_display_mismatch(): } +def test_validate_content_extension_missing_content_stability(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Remove all ContentStability extensions + document_ref_data["content"][0]["extension"] = [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + } + ] + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert result.resource.id == "Y05868-99999-99999-999999" + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + "expression": ["content[0].extension"], + } + + +def test_validate_content_extension_mismatch_between_retrieval_mechanism_display_and_code(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add a retrieval mechanism extension with a valid code but wrong display + document_ref_data["content"][0]["extension"].append( + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Spine Secure Proxy", + } + ] + }, + } + ) + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert result.resource.id == "Y05868-99999-99999-999999" + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension display: Spine Secure Proxy Expected display is 'Direct'", + "expression": [ + "content[0].extension[1].valueCodeableConcept.coding[0].display" + ], + } + + def test_validate_content_invalid_content_type(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 266e93ab3..9569337f4 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -10,6 +10,7 @@ ATTACHMENT_CONTENT_TYPES, CATEGORY_ATTRIBUTES, CONTENT_FORMAT_CODE_MAP, + CONTENT_RETRIEVAL_CODE_MAP, ODS_SYSTEM, PRACTICE_SETTING_VALUE_SET_URL, REQUIRED_CREATE_FIELDS, @@ -509,13 +510,56 @@ def _validate_content_extension(self, model: DocumentReference): logger.debug("Validating extension") for i, content in enumerate(model.content): - coding = content.extension[0].valueCodeableConcept.coding[0] - if coding.code != coding.display.lower(): + if len(content.extension) == 0: self.result.add_error( issue_code="business-rule", error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", - field=f"content[{i}].extension[0].valueCodeableConcept.coding[0].display", + diagnostics=f"Invalid content extension: Extension must have at least one value", + field=f"content[{i}].extension", + ) + return + + has_content_stability = False + for j, extension in enumerate(content.extension): + if ( + extension.url + == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ): + coding = extension.valueCodeableConcept.coding[0] + if coding.code != coding.display.lower() or coding.display not in [ + "Static", + "Dynamic", + ]: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return + has_content_stability = True + + elif ( + extension.url + == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ): + coding = extension.valueCodeableConcept.coding[0] + retrievalDisplay = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) + if coding.display != retrievalDisplay: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Expected display is '{retrievalDisplay}'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return + + if not has_content_stability: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + field=f"content[{i}].extension", ) return diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 514792e24..499de5aff 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-05-22T18:53:21+00:00 +# timestamp: 2025-08-07T11:40:19+00:00 from __future__ import annotations @@ -284,6 +284,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "NDR"] + display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -549,6 +555,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -610,6 +622,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -669,7 +688,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], + Field(max_length=2, min_length=1), ] diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index e7081e66b..fc619d957 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-05-22T18:53:21+00:00 +# timestamp: 2025-08-07T11:40:22+00:00 from __future__ import annotations @@ -255,6 +255,12 @@ class ContentStabilityExtensionCoding(Coding): display: Literal["Static", "Dynamic"] +class RetrievalMechanismExtensionCoding(Coding): + system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] + code: Literal["SSP", "Direct", "NDR"] + display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + + class NRLFormatCode(Coding): system: Annotated[ Literal["https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode"], @@ -486,6 +492,12 @@ class ContentStabilityExtensionValueCodeableConcept(CodeableConcept): ] +class RetrievalMechanismExtensionValueCodeableConcept(CodeableConcept): + coding: Annotated[ + List[RetrievalMechanismExtensionCoding], Field(max_length=1, min_length=1) + ] + + class RequestHeader(Parent): odsCode: RequestHeaderOdsCode @@ -541,6 +553,13 @@ class ContentStabilityExtension(Extension): valueCodeableConcept: ContentStabilityExtensionValueCodeableConcept +class RetrievalMechanismExtension(Extension): + url: Literal[ + "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ] + valueCodeableConcept: RetrievalMechanismExtensionValueCodeableConcept + + class OperationOutcome(Parent): resourceType: Literal["OperationOutcome"] id: Annotated[ @@ -594,7 +613,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[ContentStabilityExtension], Field(max_length=1, min_length=1) + List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], + Field(max_length=2, min_length=1), ] From 9fd7818a06a74a8b6de7f2faaf4f965891b8396e Mon Sep 17 00:00:00 2001 From: eesa456 Date: Mon, 11 Aug 2025 11:26:15 +0100 Subject: [PATCH 02/20] NRL-1554 sonar qube fixes --- layer/nrlf/core/constants.py | 1 + layer/nrlf/core/validators.py | 61 +++++++++++++++++++++-------------- tests/features/utils/data.py | 19 ++++++++++- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 6346242e1..6ceeaba05 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -677,6 +677,7 @@ def coding_value(self): CONTENT_STABILITY_EXTENSION_URL = ( "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" ) +CONTENT_RETRIEVAL_EXTENSION_URL = "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" CONTENT_STABILITY_SYSTEM_URL = ( "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability" ) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 9569337f4..49f858cf7 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -507,14 +507,14 @@ def _validate_content_extension(self, model: DocumentReference): Validate the content.extension field contains an appropriate coding. """ logger.log(LogReference.VALIDATOR001, step="content_extension") - logger.debug("Validating extension") + for i, content in enumerate(model.content): - if len(content.extension) == 0: + if not content.extension: self.result.add_error( issue_code="business-rule", error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension: Extension must have at least one value", + diagnostics="Invalid content extension: Extension must have at least one value", field=f"content[{i}].extension", ) return @@ -525,44 +525,55 @@ def _validate_content_extension(self, model: DocumentReference): extension.url == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" ): - coding = extension.valueCodeableConcept.coding[0] - if coding.code != coding.display.lower() or coding.display not in [ - "Static", - "Dynamic", - ]: - self.result.add_error( - issue_code="business-rule", - error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", - field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", - ) + if not self._validate_content_stability_extension(extension, i, j): return has_content_stability = True - elif ( extension.url == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" ): - coding = extension.valueCodeableConcept.coding[0] - retrievalDisplay = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) - if coding.display != retrievalDisplay: - self.result.add_error( - issue_code="business-rule", - error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension display: {coding.display} Expected display is '{retrievalDisplay}'", - field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", - ) + if not self._validate_retrieval_mechanism_extension( + extension, i, j + ): return if not has_content_stability: self.result.add_error( issue_code="business-rule", error_code="UNPROCESSABLE_ENTITY", - diagnostics=f"Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + diagnostics="Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", field=f"content[{i}].extension", ) return + def _validate_content_stability_extension(self, extension, i, j): + coding = extension.valueCodeableConcept.coding[0] + if coding.code != coding.display.lower() or coding.display not in [ + "Static", + "Dynamic", + ]: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Extension display must be the same as code either 'Static' or 'Dynamic'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return False + return True + + def _validate_retrieval_mechanism_extension(self, extension, i, j): + coding = extension.valueCodeableConcept.coding[0] + expected_retrieval_display = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) + if coding.display != expected_retrieval_display: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics=f"Invalid content extension display: {coding.display} Expected display is '{expected_retrieval_display}'", + field=f"content[{i}].extension[{j}].valueCodeableConcept.coding[0].display", + ) + return False + return True + def _validate_author(self, model: DocumentReference): """ Validate the author field contains an appropriate coding system and code. diff --git a/tests/features/utils/data.py b/tests/features/utils/data.py index cd6a7262d..dab741c80 100644 --- a/tests/features/utils/data.py +++ b/tests/features/utils/data.py @@ -1,6 +1,8 @@ from layer.nrlf.core.constants import ( CATEGORY_ATTRIBUTES, CONTENT_FORMAT_CODE_URL, + CONTENT_RETRIEVAL_EXTENSION_URL, + CONTENT_RETRIEVAL_SYSTEM_URL, CONTENT_STABILITY_EXTENSION_URL, CONTENT_STABILITY_SYSTEM_URL, SNOMED_PRACTICE_SETTINGS, @@ -22,6 +24,9 @@ NRLCoding, NRLFormatCode, Reference, + RetrievalMechanismExtension, + RetrievalMechanismExtensionCoding, + RetrievalMechanismExtensionValueCodeableConcept, ) from tests.features.utils.constants import ( DEFAULT_TEST_AUTHOR, @@ -75,7 +80,19 @@ def create_test_document_reference(items: dict) -> DocumentReference: ) ] ), - ) + ), + RetrievalMechanismExtension( + url=CONTENT_RETRIEVAL_EXTENSION_URL, + valueCodeableConcept=RetrievalMechanismExtensionValueCodeableConcept( + coding=[ + RetrievalMechanismExtensionCoding( + system=CONTENT_RETRIEVAL_SYSTEM_URL, + code="Direct", + display="Direct", + ) + ] + ), + ), ], ) ], From a523b94a2598715238b03ca0c42a2e37c711dd52 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Mon, 11 Aug 2025 11:34:24 +0100 Subject: [PATCH 03/20] NRL-1554 reduce complexity --- layer/nrlf/core/validators.py | 52 ++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 49f858cf7..79b152966 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -519,33 +519,35 @@ def _validate_content_extension(self, model: DocumentReference): ) return - has_content_stability = False - for j, extension in enumerate(content.extension): - if ( - extension.url - == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ): - if not self._validate_content_stability_extension(extension, i, j): - return - has_content_stability = True - elif ( - extension.url - == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" - ): - if not self._validate_retrieval_mechanism_extension( - extension, i, j - ): - return - - if not has_content_stability: - self.result.add_error( - issue_code="business-rule", - error_code="UNPROCESSABLE_ENTITY", - diagnostics="Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", - field=f"content[{i}].extension", - ) + if not self._has_valid_extensions(content.extension, i): return + def _has_valid_extensions(self, extensions, i): + has_content_stability = False + for j, extension in enumerate(extensions): + if ( + extension.url + == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" + ): + if not self._validate_content_stability_extension(extension, i, j): + return False + has_content_stability = True + elif ( + extension.url + == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + ): + if not self._validate_retrieval_mechanism_extension(extension, i, j): + return False + if not has_content_stability: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics="Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + field=f"content[{i}].extension", + ) + return False + return True + def _validate_content_stability_extension(self, extension, i, j): coding = extension.valueCodeableConcept.coding[0] if coding.code != coding.display.lower() or coding.display not in [ From 2790ae232beb889934c4a35970cfc577f392ccc0 Mon Sep 17 00:00:00 2001 From: eesa456 Date: Mon, 11 Aug 2025 11:45:26 +0100 Subject: [PATCH 04/20] NRL-1554 add retrieval mechanism extension to int tests --- .../readDocumentReference-success.feature | 24 +++++++++++++++++++ .../readDocumentReference-success.feature | 12 ++++++++++ 2 files changed, 36 insertions(+) diff --git a/tests/features/consumer/readDocumentReference-success.feature b/tests/features/consumer/readDocumentReference-success.feature index aa91cd59f..4fd465fde 100644 --- a/tests/features/consumer/readDocumentReference-success.feature +++ b/tests/features/consumer/readDocumentReference-success.feature @@ -87,6 +87,18 @@ Feature: Consumer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } @@ -192,6 +204,18 @@ Feature: Consumer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } diff --git a/tests/features/producer/readDocumentReference-success.feature b/tests/features/producer/readDocumentReference-success.feature index 9e4c2f8d7..c8d5215d4 100644 --- a/tests/features/producer/readDocumentReference-success.feature +++ b/tests/features/producer/readDocumentReference-success.feature @@ -89,6 +89,18 @@ Feature: Producer - readDocumentReference - Success Scenarios } ] } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } } ] } From aac33c02503702b60f824bf1127364e9a0a89e4e Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 18 Aug 2025 09:04:11 +0100 Subject: [PATCH 05/20] NRL-1554 Fix missing swagger changes from NRL-1472 --- api/consumer/swagger.yaml | 2 ++ api/producer/swagger.yaml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 95f4a9702..68ad5af0c 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1039,12 +1039,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 7bd2e8b2a..9ffd59f4b 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1695,12 +1695,14 @@ components: enum: - "urn:nhs-ic:record-contact" - "urn:nhs-ic:unstructured" + - "urn:nhs-ic:structured" description: The code representing the format of the document. display: type: string enum: - "Contact details (HTTP Unsecured)" - "Unstructured Document" + - "Structured Document" description: The display text for the code. required: - system From 938119800bdf35809da1b8c287df90fe371ea793 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 18 Aug 2025 09:21:04 +0100 Subject: [PATCH 06/20] NRL-1554 Allow arbitrary extensions in content, change diagnostic messages --- api/consumer/swagger.yaml | 4 +- api/producer/swagger.yaml | 4 +- layer/nrlf/consumer/fhir/r4/model.py | 6 +- layer/nrlf/core/errors.py | 53 +++++------- layer/nrlf/core/tests/test_pydantic_errors.py | 46 ++-------- layer/nrlf/core/tests/test_validators.py | 73 +++++++++++++++- layer/nrlf/core/validators.py | 84 ++++++++++++++----- layer/nrlf/producer/fhir/r4/model.py | 6 +- layer/nrlf/producer/fhir/r4/strict_model.py | 6 +- 9 files changed, 176 insertions(+), 106 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index 68ad5af0c..e60902835 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -813,9 +813,9 @@ components: oneOf: - $ref: "#/components/schemas/ContentStabilityExtension" - $ref: "#/components/schemas/RetrievalMechanismExtension" - description: Additional extension for content stability. + - $ref: "#/components/schemas/Extension" + description: Additional extensions which include Content Stability and Retrieval Mechanism. minItems: 1 - maxItems: 2 required: - attachment - format diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 9ffd59f4b..195bde39a 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1418,9 +1418,9 @@ components: oneOf: - $ref: "#/components/schemas/ContentStabilityExtension" - $ref: "#/components/schemas/RetrievalMechanismExtension" - description: Additional extension for content stability. + - $ref: "#/components/schemas/Extension" + description: Additional extensions which include Content Stability and Retrieval Mechanism. minItems: 1 - maxItems: 2 required: - attachment - format diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 70f794a05..3d78df041 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-07T11:40:24+00:00 +# timestamp: 2025-08-14T08:25:07+00:00 from __future__ import annotations @@ -652,8 +652,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], - Field(max_length=2, min_length=1), + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] diff --git a/layer/nrlf/core/errors.py b/layer/nrlf/core/errors.py index 2d4c34805..d230d0400 100644 --- a/layer/nrlf/core/errors.py +++ b/layer/nrlf/core/errors.py @@ -3,11 +3,6 @@ from pydantic import ValidationError from pydantic_core import ErrorDetails -from nrlf.core.constants import ( - CONTENT_FORMAT_CODE_URL, - CONTENT_RETRIEVAL_SYSTEM_URL, - CONTENT_STABILITY_SYSTEM_URL, -) from nrlf.core.response import Response from nrlf.core.types import CodeableConcept from nrlf.producer.fhir.r4 import model as producer_model @@ -15,12 +10,8 @@ def format_error_location(loc: List) -> str: - # List of extension class names to exclude from error paths - exclude_classes = {"ContentStabilityExtension", "RetrievalMechanismExtension"} - filtered_loc = [each for each in loc if each not in exclude_classes] - formatted_loc = "" - for each in filtered_loc: + for each in loc: if isinstance(each, int): formatted_loc = f"{formatted_loc}[{each}]" else: @@ -28,28 +19,25 @@ def format_error_location(loc: List) -> str: return formatted_loc -def append_value_set_url(loc_string: str) -> str: - if loc_string.endswith(("url", "system")): - return "" - - if "content" in loc_string: - if "extension" in loc_string: - return f". See ValueSets: {CONTENT_STABILITY_SYSTEM_URL} & {CONTENT_RETRIEVAL_SYSTEM_URL}" - if "format" in loc_string: - return f". See ValueSet: {CONTENT_FORMAT_CODE_URL}" - - return "" - - -def diag_for_error(error: ErrorDetails) -> str: +def diag_for_error(error: ErrorDetails, value_set: str, root_location: tuple) -> str: loc_string = format_error_location(error["loc"]) + if root_location: + loc_string = format_error_location(root_location) + "." + loc_string + msg = f"{loc_string or 'DocumentReference'}: {error['msg']}" - msg += append_value_set_url(loc_string) + msg += f", see: {value_set}" if value_set else "" return msg -def expression_for_error(error: ErrorDetails) -> Optional[str]: - return format_error_location(error["loc"]) or "DocumentReference" +def expression_for_error(error: ErrorDetails, root_location: tuple) -> Optional[str]: + loc_string = format_error_location(error["loc"]) or "DocumentReference" + if root_location and error["loc"]: + loc_string = ( + format_error_location(root_location) + + "." + + format_error_location(error["loc"]) + ) + return loc_string class OperationOutcomeError(Exception): @@ -99,15 +87,20 @@ def __init__(self, issues: List[OperationOutcomeIssue]): @classmethod def from_validation_error( - cls, exc: ValidationError, details: CodeableConcept, msg: str = "" + cls, + exc: ValidationError, + details: CodeableConcept, + msg: str = "", + value_set: str = "", + root_location: tuple = None, ): issues = [ producer_model.OperationOutcomeIssue( severity="error", code="invalid", details=details, # type: ignore - diagnostics=f"{msg} ({diag_for_error(error)})", - expression=[expression_for_error(error)], # type: ignore + diagnostics=f"{msg} ({diag_for_error(error, value_set, root_location)})", + expression=[expression_for_error(error, root_location)], # type: ignore ) for error in exc.errors() ] diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 63f600cfd..9b7af3d99 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -85,47 +85,11 @@ def test_validate_content_missing_format(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required. See ValueSet: https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode)", + "diagnostics": "Failed to parse DocumentReference resource (content[0].format: Field required)", "expression": ["content[0].format"], } -def test_validate_content_multiple_content_stability_extensions(): - validator = DocumentReferenceValidator() - document_ref_data = load_document_reference_json("Y05868-736253002-Valid") - - # Add a second duplicate contentStability extension - document_ref_data["content"][0]["extension"].append( - document_ref_data["content"][0]["extension"][0] - ) - - # Add a third duplicate contentStability extension - document_ref_data["content"][0]["extension"].append( - document_ref_data["content"][0]["extension"][0] - ) - - with pytest.raises(ParseError) as error: - validator.validate(document_ref_data) - - exc = error.value - assert len(exc.issues) == 1 - assert exc.issues[0].model_dump(exclude_none=True) == { - "severity": "error", - "code": "invalid", - "details": { - "coding": [ - { - "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", - "code": "BAD_REQUEST", - "display": "Bad request", - } - ] - }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: List should have at most 2 items after validation, not 3. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", - "expression": ["content[0].extension"], - } - - def test_validate_content_invalid_content_stability_code(): validator = DocumentReferenceValidator() document_ref_data = load_document_reference_json("Y05868-736253002-Valid") @@ -150,7 +114,7 @@ def test_validate_content_invalid_content_stability_code(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'static' or 'dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"], } @@ -179,7 +143,7 @@ def test_validate_content_invalid_content_stability_display(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic'. See ValueSets: https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability & https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism)", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Static' or 'Dynamic', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": [ "content[0].extension[0].valueCodeableConcept.coding[0].display" ], @@ -210,7 +174,7 @@ def test_validate_content_invalid_content_stability_system(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + "diagnostics": "Invalid content stability extension (content[0].extension[0].valueCodeableConcept.coding[0].system: Input should be 'https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].system"], } @@ -238,7 +202,7 @@ def test_validate_content_invalid_content_stability_url(): } ] }, - "diagnostics": "Failed to parse DocumentReference resource (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability')", + "diagnostics": "Invalid content stability extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability', see: https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability)", "expression": ["content[0].extension[0].url"], } diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 1b02d34f7..0d7bbebfe 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1402,7 +1402,7 @@ def test_validate_content_extension_missing_content_stability(): } ] }, - "diagnostics": "Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + "diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", "expression": ["content[0].extension"], } @@ -1559,3 +1559,74 @@ def test_validate_nrl_format_code_display_mismatch( "diagnostics": f"Invalid display for format code '{format_code}'. Expected '{expected_display}'", "expression": ["content[0].format.display"], } + + +def test_validate_content_multiple_content_stability_extensions(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add a second duplicate contentStability extension + document_ref_data["content"][0]["extension"].append( + document_ref_data["content"][0]["extension"][0] + ) + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", + "expression": ["content[0].extension"], + } + + +def test_validate_content_multiple_content_retrieval_extensions(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + # Add 2 content retrieval extensions + content_retrieval_extension = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + } + document_ref_data["content"][0]["extension"].append(content_retrieval_extension) + document_ref_data["content"][0]["extension"].append(content_retrieval_extension) + + result = validator.validate(document_ref_data) + + assert result.is_valid is False + assert len(result.issues) == 1 + assert result.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity", + } + ] + }, + "diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + "expression": ["content[0].extension"], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 79b152966..577de218d 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -25,6 +25,10 @@ from nrlf.core.logger import LogReference, logger from nrlf.core.types import DocumentReference, OperationOutcomeIssue, RequestQueryType from nrlf.producer.fhir.r4 import model as producer_model +from nrlf.producer.fhir.r4.model import ( + ContentStabilityExtension, + RetrievalMechanismExtension, +) def validate_type(type_: Optional[RequestQueryType], pointer_types: List[str]) -> bool: @@ -523,37 +527,64 @@ def _validate_content_extension(self, model: DocumentReference): return def _has_valid_extensions(self, extensions, i): - has_content_stability = False - for j, extension in enumerate(extensions): - if ( - extension.url - == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability" - ): - if not self._validate_content_stability_extension(extension, i, j): - return False - has_content_stability = True - elif ( - extension.url - == "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" - ): - if not self._validate_retrieval_mechanism_extension(extension, i, j): - return False - if not has_content_stability: + content_stability_count = 0 + content_retrieval_count = 0 + + for extension in extensions: + # if extension.url == CONTENT_STABILITY_EXTENSION_URL: + # content_stability_count += 1 + # elif extension.url == CONTENT_RETRIEVAL_EXTENSION_URL: + # content_retrieval_count += 1 + if "ContentStability" in str(extension): + content_stability_count += 1 + elif "RetrievalMechanism" in str(extension): + content_retrieval_count += 1 + + if content_stability_count != 1: self.result.add_error( issue_code="business-rule", error_code="UNPROCESSABLE_ENTITY", - diagnostics="Invalid content extension: Extension must have one content stability extension see value set ('https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability')", + diagnostics="Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')", field=f"content[{i}].extension", ) return False + + if content_retrieval_count > 1: + self.result.add_error( + issue_code="business-rule", + error_code="UNPROCESSABLE_ENTITY", + diagnostics="Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + field=f"content[{i}].extension", + ) + return False + + for j, extension in enumerate(extensions): + # if extension.url == CONTENT_STABILITY_EXTENSION_URL: + if "ContentStability" in str(extension): + if not self._validate_content_stability_extension(extension, i, j): + return False + # elif extension.url == CONTENT_RETRIEVAL_EXTENSION_URL: + elif "RetrievalMechanism" in str(extension): + if not self._validate_retrieval_mechanism_extension(extension, i, j): + return False + return True def _validate_content_stability_extension(self, extension, i, j): + try: + ContentStabilityExtension.model_validate(extension.model_dump()) + except ValidationError as exc: + # for error in exc.errors(): + # error["loc"] = ("content", i, "extension", j) + error["loc"] + raise ParseError.from_validation_error( + exc, + details=SpineErrorConcept.from_code("BAD_REQUEST"), + msg="Invalid content stability extension", + value_set="https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability", + root_location=("content", i, "extension", j), + ) from None coding = extension.valueCodeableConcept.coding[0] - if coding.code != coding.display.lower() or coding.display not in [ - "Static", - "Dynamic", - ]: + if coding.code != coding.display.lower(): self.result.add_error( issue_code="business-rule", error_code="UNPROCESSABLE_ENTITY", @@ -564,6 +595,17 @@ def _validate_content_stability_extension(self, extension, i, j): return True def _validate_retrieval_mechanism_extension(self, extension, i, j): + try: + RetrievalMechanismExtension.model_validate(extension.model_dump()) + except ValidationError as exc: + for error in exc.errors(): + error["loc"] = ("content", i, "extension", j) + error["loc"] + raise ParseError.from_validation_error( + exc, + details=SpineErrorConcept.from_code("BAD_REQUEST"), + msg="Invalid content retrieval extension", + value_set="https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism", + ) from None coding = extension.valueCodeableConcept.coding[0] expected_retrieval_display = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) if coding.display != expected_retrieval_display: diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 499de5aff..dcb06a738 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-07T11:40:19+00:00 +# timestamp: 2025-08-14T08:25:03+00:00 from __future__ import annotations @@ -688,8 +688,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], - Field(max_length=2, min_length=1), + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index fc619d957..b9434a086 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-07T11:40:22+00:00 +# timestamp: 2025-08-14T08:25:05+00:00 from __future__ import annotations @@ -613,8 +613,8 @@ class DocumentReferenceContent(Parent): ), ] extension: Annotated[ - List[Union[ContentStabilityExtension, RetrievalMechanismExtension]], - Field(max_length=2, min_length=1), + List[Union[ContentStabilityExtension, RetrievalMechanismExtension, Extension]], + Field(min_length=1), ] From 8a3b82fbb53ccf49da28402a1cd4177fa4f34e13 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Mon, 18 Aug 2025 09:26:38 +0100 Subject: [PATCH 07/20] NRL-1554 Remove previous modifications to unit tests --- layer/nrlf/core/tests/test_pydantic_errors.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 9b7af3d99..47f983cc2 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -102,6 +102,7 @@ def test_validate_content_invalid_content_stability_code(): validator.validate(document_ref_data) exc = error.value + assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -131,6 +132,7 @@ def test_validate_content_invalid_content_stability_display(): validator.validate(document_ref_data) exc = error.value + assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -162,6 +164,7 @@ def test_validate_content_invalid_content_stability_system(): validator.validate(document_ref_data) exc = error.value + assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", @@ -190,6 +193,7 @@ def test_validate_content_invalid_content_stability_url(): validator.validate(document_ref_data) exc = error.value + assert len(exc.issues) == 1 assert exc.issues[0].model_dump(exclude_none=True) == { "severity": "error", "code": "invalid", From a25467b92367ca65e2136fb136397cdc5ef8320e Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 19 Aug 2025 08:51:08 +0100 Subject: [PATCH 08/20] NRL-1554 Remove commented code --- layer/nrlf/core/validators.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 577de218d..b76604d64 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -531,10 +531,6 @@ def _has_valid_extensions(self, extensions, i): content_retrieval_count = 0 for extension in extensions: - # if extension.url == CONTENT_STABILITY_EXTENSION_URL: - # content_stability_count += 1 - # elif extension.url == CONTENT_RETRIEVAL_EXTENSION_URL: - # content_retrieval_count += 1 if "ContentStability" in str(extension): content_stability_count += 1 elif "RetrievalMechanism" in str(extension): @@ -559,11 +555,9 @@ def _has_valid_extensions(self, extensions, i): return False for j, extension in enumerate(extensions): - # if extension.url == CONTENT_STABILITY_EXTENSION_URL: if "ContentStability" in str(extension): if not self._validate_content_stability_extension(extension, i, j): return False - # elif extension.url == CONTENT_RETRIEVAL_EXTENSION_URL: elif "RetrievalMechanism" in str(extension): if not self._validate_retrieval_mechanism_extension(extension, i, j): return False @@ -574,8 +568,6 @@ def _validate_content_stability_extension(self, extension, i, j): try: ContentStabilityExtension.model_validate(extension.model_dump()) except ValidationError as exc: - # for error in exc.errors(): - # error["loc"] = ("content", i, "extension", j) + error["loc"] raise ParseError.from_validation_error( exc, details=SpineErrorConcept.from_code("BAD_REQUEST"), From c7f89c8f3c005b06b4d1cc6775cf1a4183888c34 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 19 Aug 2025 09:25:22 +0100 Subject: [PATCH 09/20] NRL-1554 Add test for two retrieval mechanisms --- layer/nrlf/core/tests/test_validators.py | 97 ++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 0d7bbebfe..2f83bc452 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1630,3 +1630,100 @@ def test_validate_content_multiple_content_retrieval_extensions(): "diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", "expression": ["content[0].extension"], } + + +def test_validate_two_content_with_different_retrieval_mechanisms(): + """Test that two content items with different retrieval mechanisms are valid.""" + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + unstructured_format = { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document", + } + + static_content_stability = { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static", + } + ] + }, + } + + # Add retrieval mechanism extension to the first content item, ssp + first_content = { + "attachment": { + "contentType": "application/pdf", + "url": "ssp://example.com/document1.pdf", + }, + "format": unstructured_format, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "SSP", + "display": "Spine Secure Proxy", + } + ] + }, + }, + static_content_stability, + ], + } + + document_ref_data["content"] = [first_content] + + # Add valid ASID identifier in context.related + document_ref_data["context"]["related"] = [ + { + "identifier": { + "system": "https://fhir.nhs.uk/Id/nhsSpineASID", + "value": "123456789012", + } + } + ] + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + assert len(result.issues) == 0 + + # Add a second content item with a different retrieval mechanism + second_content = { + "attachment": { + "contentType": "application/pdf", + "url": "http://example.com/document2.pdf", + }, + "format": unstructured_format, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + }, + static_content_stability, + ], + } + + document_ref_data["content"].append(second_content) + + result = validator.validate(document_ref_data) + + assert result.is_valid is True + assert len(result.issues) == 0 From d945e98700e7e8aebcf4221aefc231eae6b3b9a4 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 26 Aug 2025 17:51:57 +0100 Subject: [PATCH 10/20] NRL-1554 Lowercase matching for extensions, remove unused code, add root_location to retrieval mechanism error --- layer/nrlf/core/tests/test_validators.py | 53 ++++++++++++++++++++++++ layer/nrlf/core/validators.py | 11 +++-- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 2f83bc452..ce8e7e5d7 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1727,3 +1727,56 @@ def test_validate_two_content_with_different_retrieval_mechanisms(): assert result.is_valid is True assert len(result.issues) == 0 + + +def test_validate_content_retrieval_lowercase_urls(): + """Test that the extension is recognised when 'RetrievalMechanism' is in lowercase and throws an error for mismatching the URL case.""" + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0]["extension"] = [ + { + "url": "https://fhir.nhs.uk/england/structuredefinition/extension-england-retrievalmechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct", + } + ] + }, + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static", + } + ] + }, + }, + ] + + with pytest.raises(ParseError) as exc_info: + validator.validate(document_ref_data) + + assert len(exc_info.value.issues) == 1 + assert exc_info.value.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request", + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].url"], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index b76604d64..bac4053d2 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -531,9 +531,9 @@ def _has_valid_extensions(self, extensions, i): content_retrieval_count = 0 for extension in extensions: - if "ContentStability" in str(extension): + if "contentstability" in str(extension).lower(): content_stability_count += 1 - elif "RetrievalMechanism" in str(extension): + elif "retrievalmechanism" in str(extension).lower(): content_retrieval_count += 1 if content_stability_count != 1: @@ -555,10 +555,10 @@ def _has_valid_extensions(self, extensions, i): return False for j, extension in enumerate(extensions): - if "ContentStability" in str(extension): + if "contentstability" in str(extension).lower(): if not self._validate_content_stability_extension(extension, i, j): return False - elif "RetrievalMechanism" in str(extension): + elif "retrievalmechanism" in str(extension).lower(): if not self._validate_retrieval_mechanism_extension(extension, i, j): return False @@ -590,13 +590,12 @@ def _validate_retrieval_mechanism_extension(self, extension, i, j): try: RetrievalMechanismExtension.model_validate(extension.model_dump()) except ValidationError as exc: - for error in exc.errors(): - error["loc"] = ("content", i, "extension", j) + error["loc"] raise ParseError.from_validation_error( exc, details=SpineErrorConcept.from_code("BAD_REQUEST"), msg="Invalid content retrieval extension", value_set="https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism", + root_location=("content", i, "extension", j), ) from None coding = extension.valueCodeableConcept.coding[0] expected_retrieval_display = CONTENT_RETRIEVAL_CODE_MAP.get(coding.code) From e4ad92d958aa7ed09089d715482ff12f30d3a373 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Aug 2025 08:20:18 +0100 Subject: [PATCH 11/20] NRL-1554 Replace NDR for LDR (Large Document Repository) --- api/consumer/swagger.yaml | 4 ++-- api/producer/swagger.yaml | 4 ++-- layer/nrlf/consumer/fhir/r4/model.py | 6 +++--- layer/nrlf/core/constants.py | 2 +- layer/nrlf/producer/fhir/r4/model.py | 6 +++--- layer/nrlf/producer/fhir/r4/strict_model.py | 6 +++--- resources/fhir/NRLF-Retrieval-CodeSystem.json | 6 +++--- resources/fhir/NRLF-RetrievalMechanism-ValueSet.json | 4 ++-- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index e60902835..d60cc2f33 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1015,11 +1015,11 @@ components: - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" code: type: string - enum: ["SSP", "Direct", "NDR"] + enum: ["SSP", "Direct", "LDR"] display: type: string enum: - ["Spine Secure Proxy", "National Document Repository", "Direct"] + ["Spine Secure Proxy", "Large Document Repository", "Direct"] required: - system - code diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 195bde39a..27b0a843c 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1671,11 +1671,11 @@ components: - "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism" code: type: string - enum: ["SSP", "Direct", "NDR"] + enum: ["SSP", "Direct", "LDR"] display: type: string enum: - ["Spine Secure Proxy", "National Document Repository", "Direct"] + ["Spine Secure Proxy", "Large Document Repository", "Direct"] required: - system - code diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 3d78df041..9e5948deb 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-14T08:25:07+00:00 +# timestamp: 2025-08-26T08:20:24+00:00 from __future__ import annotations @@ -242,8 +242,8 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] - code: Literal["SSP", "Direct", "NDR"] - display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 6ceeaba05..326f93e7b 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -687,7 +687,7 @@ def coding_value(self): CONTENT_RETRIEVAL_CODE_MAP = { "Direct": "Direct", "SSP": "Spine Secure Proxy", - "NDR": "National Document Repository", + "LDR": "Large Document Repository", } CONTENT_FORMAT_CODE_URL = "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" CONTENT_FORMAT_CODE_MAP = { diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index dcb06a738..8831105aa 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-14T08:25:03+00:00 +# timestamp: 2025-08-26T08:20:20+00:00 from __future__ import annotations @@ -286,8 +286,8 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] - code: Literal["SSP", "Direct", "NDR"] - display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index b9434a086..129de1e6d 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-14T08:25:05+00:00 +# timestamp: 2025-08-26T08:20:22+00:00 from __future__ import annotations @@ -257,8 +257,8 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] - code: Literal["SSP", "Direct", "NDR"] - display: Literal["Spine Secure Proxy", "National Document Repository", "Direct"] + code: Literal["SSP", "Direct", "LDR"] + display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] class NRLFormatCode(Coding): diff --git a/resources/fhir/NRLF-Retrieval-CodeSystem.json b/resources/fhir/NRLF-Retrieval-CodeSystem.json index 7f58167a9..5f5be74d2 100644 --- a/resources/fhir/NRLF-Retrieval-CodeSystem.json +++ b/resources/fhir/NRLF-Retrieval-CodeSystem.json @@ -53,9 +53,9 @@ "definition": "This document can be retrieved via Spine Secure Proxy by authorised organisations. The custodian's ASID will be needed and can be found in the context.related field." }, { - "code": "NDR", - "display": "National Document Repository", - "definition": "This document can be retrieved via the National Document Repository proxy service." + "code": "LDR", + "display": "Large Document Repository", + "definition": "This document can be retrieved via the Large Document Repository proxy service." } ] } diff --git a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json index 4956351c1..150f781a8 100644 --- a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json +++ b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json @@ -31,8 +31,8 @@ "display": "Spine Secure Proxy" }, { - "code": "NDR", - "display": "National Document Repository" + "code": "LDR", + "display": "Large Document Repository" } ] } From 0be0f57c48e339d00e59ed03a597c5850a1a1cb7 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Aug 2025 08:54:51 +0100 Subject: [PATCH 12/20] NRL-1554 Remove check for empty content extension as it is checked in pydantic, add relevant tests --- layer/nrlf/core/tests/test_pydantic_errors.py | 28 ++++++++++++ layer/nrlf/core/validators.py | 9 ---- .../createDocumentReference-failure.feature | 45 +++++++++++++++++++ 3 files changed, 73 insertions(+), 9 deletions(-) diff --git a/layer/nrlf/core/tests/test_pydantic_errors.py b/layer/nrlf/core/tests/test_pydantic_errors.py index 47f983cc2..4a53838ba 100644 --- a/layer/nrlf/core/tests/test_pydantic_errors.py +++ b/layer/nrlf/core/tests/test_pydantic_errors.py @@ -439,3 +439,31 @@ def test_validate_missing_display_from_coding_where_mandatory(): "diagnostics": "Failed to parse DocumentReference resource (context.practiceSetting.coding[0].display: Field required)", "expression": ["context.practiceSetting.coding[0].display"], } + + +def test_validate_content_no_content_extension(): + validator = DocumentReferenceValidator() + document_ref_data = load_document_reference_json("Y05868-736253002-Valid") + + document_ref_data["content"][0].pop("extension") + + with pytest.raises(ParseError) as error: + validator.validate(document_ref_data) + + exc = error.value + assert len(exc.issues) == 1 + assert exc.issues[0].model_dump(exclude_none=True) == { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request", + } + ] + }, + "diagnostics": "Failed to parse DocumentReference resource (content[0].extension: Field required)", + "expression": ["content[0].extension"], + } diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index bac4053d2..3e7ceaeec 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -514,15 +514,6 @@ def _validate_content_extension(self, model: DocumentReference): logger.debug("Validating extension") for i, content in enumerate(model.content): - if not content.extension: - self.result.add_error( - issue_code="business-rule", - error_code="UNPROCESSABLE_ENTITY", - diagnostics="Invalid content extension: Extension must have at least one value", - field=f"content[{i}].extension", - ) - return - if not self._has_valid_extensions(content.extension, i): return diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 9aa0bf9dc..b3b9f57f5 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -1044,3 +1044,48 @@ Feature: Producer - createDocumentReference - Failure Scenarios "expression": ["DocumentReference"] } """ + + Scenario: RetrievalMechanism extension is empty + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "MESSAGE_NOT_WELL_FORMED", + "display": "Message not well formed" + } + ] + }, + "diagnostics": "Request body could not be parsed (DocumentReference: Value error, The following fields are empty: content[0].extension)", + "expression": [ + "DocumentReference" + ] + } + """ From daf09f08a88d0958071662ebeeab3207f90ec064 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Aug 2025 09:21:04 +0100 Subject: [PATCH 13/20] NRL-1554 Add more negative test scenarios --- .../createDocumentReference-failure.feature | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index b3b9f57f5..5dd97989c 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -1089,3 +1089,413 @@ Feature: Producer - createDocumentReference - Failure Scenarios ] } """ + + Scenario: Multiple RetrievalMechanism extensions in content + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 422 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity" + } + ] + }, + "diagnostics": "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')", + "expression": ["content[0].extension"] + } + """ + + Scenario: RetrievalMechanism extension with mismatched code and display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Spine Secure Proxy" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 422 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "business-rule", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "UNPROCESSABLE_ENTITY", + "display": "Unprocessable Entity" + } + ] + }, + "diagnostics": "Invalid content extension display: Spine Secure Proxy Expected display is 'Direct'", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] + } + """ + + Scenario: RetrievalMechanism extension with invalid URL case + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/england/structuredefinition/extension-england-retrievalmechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].url"] + } + """ + + Scenario: RetrievalMechanism extension with invalid code + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "INVALID_CODE", + "display": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].code: Input should be 'SSP', 'Direct' or 'LDR', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].code"] + } + """ + + Scenario: RetrievalMechanism extension with missing display + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Large Document Repository' or 'Direct', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] + } + """ + + Scenario: RetrievalMechanism extension with missing valueCodeableConcept + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 736253002 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/my-doc.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism" + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 400 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "error", + "code": "invalid", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/CodeSystem/Spine-ErrorOrWarningCode", + "code": "BAD_REQUEST", + "display": "Bad request" + } + ] + }, + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept: Input should be a valid dictionary or instance of RetrievalMechanismExtensionValueCodeableConcept, see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "expression": ["content[0].extension[0].valueCodeableConcept"] + } + """ From f40b44fd04dcb4a32211d7869df08d05dd599d0f Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Aug 2025 13:35:39 +0100 Subject: [PATCH 14/20] NRL-1554 Bump fhir resources versions and fix sonarcloud warning --- layer/nrlf/core/tests/test_validators.py | 2 +- resources/fhir/NRLF-Retrieval-CodeSystem.json | 2 +- resources/fhir/NRLF-RetrievalMechanism-ValueSet.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index ce8e7e5d7..362228afb 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1701,7 +1701,7 @@ def test_validate_two_content_with_different_retrieval_mechanisms(): second_content = { "attachment": { "contentType": "application/pdf", - "url": "http://example.com/document2.pdf", + "url": "https://example.com/document2.pdf", }, "format": unstructured_format, "extension": [ diff --git a/resources/fhir/NRLF-Retrieval-CodeSystem.json b/resources/fhir/NRLF-Retrieval-CodeSystem.json index 5f5be74d2..776011d5e 100644 --- a/resources/fhir/NRLF-Retrieval-CodeSystem.json +++ b/resources/fhir/NRLF-Retrieval-CodeSystem.json @@ -2,7 +2,7 @@ "resourceType": "CodeSystem", "id": "England-RetrievalMechanismNRL", "url": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanismNRL", - "version": "1.0.0", + "version": "1.0.1", "name": "EnglandRetrievalMechanismNRL", "title": "England Retrieval MechanismNRL", "status": "draft", diff --git a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json index 150f781a8..72b3bc649 100644 --- a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json +++ b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json @@ -2,7 +2,7 @@ "resourceType": "ValueSet", "id": "England-RetrievalMechanism", "url": "https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism", - "version": "1.0.0", + "version": "1.0.1", "name": "EnglandRetrievalMechanism", "status": "draft", "date": "2025-02-28", From 0b32adc574393b3b8162fd6bdd31c66962a0ba87 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Wed, 27 Aug 2025 14:04:54 +0100 Subject: [PATCH 15/20] NRL-1554 Add test for two contents with different retrieval mechanisms --- .../createDocumentReference-success.feature | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) diff --git a/tests/features/producer/createDocumentReference-success.feature b/tests/features/producer/createDocumentReference-success.feature index e1094739d..cc7186b12 100644 --- a/tests/features/producer/createDocumentReference-success.feature +++ b/tests/features/producer/createDocumentReference-success.feature @@ -377,3 +377,117 @@ Feature: Producer - createDocumentReference - Success Scenarios | contentType | application/json+fhir | | formatCode | urn:nhs-ic:structured | | formatDisplay | Structured Document | + + Scenario: Successfully create a DocumentReference with two contents and different retrieval mechanisms + Given the application 'DataShare' (ID 'z00z-y11y-x22x') is registered to access the API + And the organisation 'TSTCUS' is authorised to access pointer types: + | system | value | + | http://snomed.info/sct | 1363501000000100 | + When producer 'TSTCUS' requests creation of a DocumentReference with default test values except 'content' is: + """ + "content": [ + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/doc1.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "Direct", + "display": "Direct" + } + ] + } + } + ] + }, + { + "attachment": { + "contentType": "application/pdf", + "url": "https://example.org/doc2.pdf" + }, + "format": { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode", + "code": "urn:nhs-ic:unstructured", + "display": "Unstructured Document" + }, + "extension": [ + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + "code": "static", + "display": "Static" + } + ] + } + }, + { + "url": "https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + "valueCodeableConcept": { + "coding": [ + { + "system": "https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + "code": "SSP", + "display": "Spine Secure Proxy" + } + ] + } + } + ] + } + ] + """ + Then the response status code is 201 + And the response is an OperationOutcome with 1 issue + And the OperationOutcome contains the issue: + """ + { + "severity": "information", + "code": "informational", + "details": { + "coding": [ + { + "system": "https://fhir.nhs.uk/ValueSet/NRL-ResponseCode", + "code": "RESOURCE_CREATED", + "display": "Resource created" + } + ] + }, + "diagnostics": "The document has been created" + } + """ + And the response has a Location header + And the Location header starts with '/producer/FHIR/R4/DocumentReference/TSTCUS-' + And the resource in the Location header exists with values: + | property | value | + | content[0].attachment.url | https://example.org/doc1.pdf | + | content[0].extension[1].valueCodeableConcept.coding[0].code | Direct | + | content[0].extension[1].valueCodeableConcept.coding[0].display | Direct | + | content[1].attachment.url | https://example.org/doc2.pdf | + | content[1].extension[1].valueCodeableConcept.coding[0].code | SSP | + | content[1].extension[1].valueCodeableConcept.coding[0].display | Spine Secure Proxy | From 7357fdff3f2fcfa5cf720470ef6a8eed83697594 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 28 Aug 2025 17:34:00 +0100 Subject: [PATCH 16/20] NRL-1554 Change order of display to match the code --- api/consumer/swagger.yaml | 2 +- api/producer/swagger.yaml | 2 +- layer/nrlf/consumer/fhir/r4/model.py | 4 ++-- layer/nrlf/producer/fhir/r4/model.py | 4 ++-- layer/nrlf/producer/fhir/r4/strict_model.py | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index d60cc2f33..a686a85f3 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1019,7 +1019,7 @@ components: display: type: string enum: - ["Spine Secure Proxy", "Large Document Repository", "Direct"] + ["Spine Secure Proxy", "Direct", "Large Document Repository"] required: - system - code diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 27b0a843c..64ac9f5e0 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1675,7 +1675,7 @@ components: display: type: string enum: - ["Spine Secure Proxy", "Large Document Repository", "Direct"] + ["Spine Secure Proxy", "Direct", "Large Document Repository"] required: - system - code diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 9e5948deb..762b62721 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-26T08:20:24+00:00 +# timestamp: 2025-08-28T16:30:30+00:00 from __future__ import annotations @@ -243,7 +243,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index 8831105aa..f4bcf7eaa 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-26T08:20:20+00:00 +# timestamp: 2025-08-28T16:30:27+00:00 from __future__ import annotations @@ -287,7 +287,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index 129de1e6d..edac6b8a7 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-26T08:20:22+00:00 +# timestamp: 2025-08-28T16:30:28+00:00 from __future__ import annotations @@ -258,7 +258,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Large Document Repository", "Direct"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] class NRLFormatCode(Coding): From 6fa21a6ccb2b43bf9c24774b4974a9064ef25919 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Thu, 28 Aug 2025 17:56:33 +0100 Subject: [PATCH 17/20] NRL-1554 Reduce cognitive complexity --- layer/nrlf/core/validators.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/layer/nrlf/core/validators.py b/layer/nrlf/core/validators.py index 3e7ceaeec..6c906cf50 100644 --- a/layer/nrlf/core/validators.py +++ b/layer/nrlf/core/validators.py @@ -517,14 +517,20 @@ def _validate_content_extension(self, model: DocumentReference): if not self._has_valid_extensions(content.extension, i): return + def _is_content_stability_extension(self, extension): + return "contentstability" in str(extension).lower() + + def _is_retrieval_mechanism_extension(self, extension): + return "retrievalmechanism" in str(extension).lower() + def _has_valid_extensions(self, extensions, i): content_stability_count = 0 content_retrieval_count = 0 for extension in extensions: - if "contentstability" in str(extension).lower(): + if self._is_content_stability_extension(extension): content_stability_count += 1 - elif "retrievalmechanism" in str(extension).lower(): + elif self._is_retrieval_mechanism_extension(extension): content_retrieval_count += 1 if content_stability_count != 1: @@ -545,14 +551,16 @@ def _has_valid_extensions(self, extensions, i): ) return False + return self._validate_content_extension_items(extensions, i) + + def _validate_content_extension_items(self, extensions, i): for j, extension in enumerate(extensions): - if "contentstability" in str(extension).lower(): + if self._is_content_stability_extension(extension): if not self._validate_content_stability_extension(extension, i, j): return False - elif "retrievalmechanism" in str(extension).lower(): + elif self._is_retrieval_mechanism_extension(extension): if not self._validate_retrieval_mechanism_extension(extension, i, j): return False - return True def _validate_content_stability_extension(self, extension, i, j): From e444ccc4fe016e2162d6e2b583ece06ab12a2f2a Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Tue, 2 Sep 2025 09:45:19 +0100 Subject: [PATCH 18/20] NRL-1554 Fix integration test --- tests/features/producer/createDocumentReference-failure.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index 5dd97989c..ab233e982 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -1436,7 +1436,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Large Document Repository' or 'Direct', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Direct' or 'Large Document Repository', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] } """ From 0e81e6ac852e03d1c695104ce9d4829b3ddf24b4 Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 5 Sep 2025 09:26:38 +0100 Subject: [PATCH 19/20] NRL-1554 Retrieval instead of Repository, add unit tests --- api/consumer/swagger.yaml | 3 +- api/producer/swagger.yaml | 3 +- layer/nrlf/consumer/fhir/r4/model.py | 4 +- layer/nrlf/core/constants.py | 2 +- layer/nrlf/core/tests/test_validators.py | 131 ++++++++++++++++++ layer/nrlf/producer/fhir/r4/model.py | 4 +- layer/nrlf/producer/fhir/r4/strict_model.py | 4 +- resources/fhir/NRLF-Retrieval-CodeSystem.json | 4 +- .../NRLF-RetrievalMechanism-ValueSet.json | 2 +- .../createDocumentReference-failure.feature | 2 +- 10 files changed, 144 insertions(+), 15 deletions(-) diff --git a/api/consumer/swagger.yaml b/api/consumer/swagger.yaml index a686a85f3..a6fb5141e 100644 --- a/api/consumer/swagger.yaml +++ b/api/consumer/swagger.yaml @@ -1018,8 +1018,7 @@ components: enum: ["SSP", "Direct", "LDR"] display: type: string - enum: - ["Spine Secure Proxy", "Direct", "Large Document Repository"] + enum: ["Spine Secure Proxy", "Direct", "Large Document Retrieval"] required: - system - code diff --git a/api/producer/swagger.yaml b/api/producer/swagger.yaml index 64ac9f5e0..d513cec19 100644 --- a/api/producer/swagger.yaml +++ b/api/producer/swagger.yaml @@ -1674,8 +1674,7 @@ components: enum: ["SSP", "Direct", "LDR"] display: type: string - enum: - ["Spine Secure Proxy", "Direct", "Large Document Repository"] + enum: ["Spine Secure Proxy", "Direct", "Large Document Retrieval"] required: - system - code diff --git a/layer/nrlf/consumer/fhir/r4/model.py b/layer/nrlf/consumer/fhir/r4/model.py index 762b62721..36cbefce2 100644 --- a/layer/nrlf/consumer/fhir/r4/model.py +++ b/layer/nrlf/consumer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-28T16:30:30+00:00 +# timestamp: 2025-09-05T08:22:10+00:00 from __future__ import annotations @@ -243,7 +243,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/core/constants.py b/layer/nrlf/core/constants.py index 326f93e7b..d72162829 100644 --- a/layer/nrlf/core/constants.py +++ b/layer/nrlf/core/constants.py @@ -687,7 +687,7 @@ def coding_value(self): CONTENT_RETRIEVAL_CODE_MAP = { "Direct": "Direct", "SSP": "Spine Secure Proxy", - "LDR": "Large Document Repository", + "LDR": "Large Document Retrieval", } CONTENT_FORMAT_CODE_URL = "https://fhir.nhs.uk/England/CodeSystem/England-NRLFormatCode" CONTENT_FORMAT_CODE_MAP = { diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 362228afb..577a69819 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -4,6 +4,7 @@ from nrlf.core.constants import ( CATEGORY_ATTRIBUTES, + CONTENT_RETRIEVAL_CODE_MAP, ODS_SYSTEM, TYPE_ATTRIBUTES, TYPE_CATEGORIES, @@ -16,9 +17,15 @@ validate_type, ) from nrlf.producer.fhir.r4.model import ( + ContentStabilityExtension, + ContentStabilityExtensionCoding, + ContentStabilityExtensionValueCodeableConcept, DocumentReference, OperationOutcomeIssue, RequestQueryType, + RetrievalMechanismExtension, + RetrievalMechanismExtensionCoding, + RetrievalMechanismExtensionValueCodeableConcept, ) from nrlf.tests.data import load_document_reference_json @@ -1780,3 +1787,127 @@ def test_validate_content_retrieval_lowercase_urls(): "diagnostics": "Invalid content retrieval extension (content[0].extension[0].url: Input should be 'https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", "expression": ["content[0].extension[0].url"], } + + +def make_content_stability_extension(code, display): + return ContentStabilityExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-ContentStability", + valueCodeableConcept=ContentStabilityExtensionValueCodeableConcept( + coding=[ + ContentStabilityExtensionCoding( + system="https://fhir.nhs.uk/England/CodeSystem/England-NRLContentStability", + code=code, + display=display, + ) + ] + ), + ) + + +def make_retrieval_mechanism_extension(code, display): + return RetrievalMechanismExtension( + url="https://fhir.nhs.uk/England/StructureDefinition/Extension-England-RetrievalMechanism", + valueCodeableConcept=RetrievalMechanismExtensionValueCodeableConcept( + coding=[ + RetrievalMechanismExtensionCoding( + system="https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism", + code=code, + display=display, + ) + ] + ), + ) + + +def test_has_valid_extensions(): + validator = DocumentReferenceValidator() + extensions = [ + make_content_stability_extension("static", "Static"), + make_retrieval_mechanism_extension("Direct", "Direct"), + ] + assert validator._has_valid_extensions(extensions, 0) is True + + +def test_has_valid_extensions_multiple_retrieval_mechanism(): + validator = DocumentReferenceValidator() + extensions = [ + make_content_stability_extension("static", "Static"), + make_retrieval_mechanism_extension("Direct", "Direct"), + make_retrieval_mechanism_extension("SSP", "Spine Secure Proxy"), + ] + assert validator._has_valid_extensions(extensions, 0) is False + assert any( + "Invalid content retrieval extension: Extension must have one content retrieval extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism')" + in issue.diagnostics + for issue in validator.result.issues + ) + + +def test_no_content_extensions(): + validator = DocumentReferenceValidator() + extensions = [] + assert validator._has_valid_extensions(extensions, 0) is False + assert any( + "Invalid content extension: Extension must have one content stability extension, see: ('https://fhir.nhs.uk/England/ValueSet/England-NRLContentStability')" + in issue.diagnostics + for issue in validator.result.issues + ) + + +@pytest.mark.parametrize( + "code, display", [("static", "Static"), ("dynamic", "Dynamic")] +) +def test_validate_content_stability_extension_valid(code, display): + validator = DocumentReferenceValidator() + ext = make_content_stability_extension("static", "Static") + assert validator._validate_content_stability_extension(ext, 0, 0) is True + assert validator.result.issues == [] + + +@pytest.mark.parametrize( + "code, display", [("dynamic", "Static"), ("static", "Dynamic")] +) +def test_validate_content_stability_extension_display_mismatch(code, display): + validator = DocumentReferenceValidator() + ext = make_content_stability_extension(code, display) + assert validator._validate_content_stability_extension(ext, 0, 0) is False + assert any( + f"Invalid content extension display: {display} Extension display must be the same as code either 'Static' or 'Dynamic'" + in issue.diagnostics + for issue in validator.result.issues + ) + + +@pytest.mark.parametrize( + "code, display", + [ + ("SSP", "Spine Secure Proxy"), + ("Direct", "Direct"), + ("LDR", "Large Document Retrieval"), + ], +) +def test_validate_retrieval_mechanism_extension_valid(code, display): + validator = DocumentReferenceValidator() + ext = make_retrieval_mechanism_extension(code, display) + assert validator._validate_retrieval_mechanism_extension(ext, 0, 0) is True + assert validator.result.issues == [] + + +@pytest.mark.parametrize( + "code, display", + [ + (code, wrong_display) + for code, correct_display in CONTENT_RETRIEVAL_CODE_MAP.items() + for wrong_display in CONTENT_RETRIEVAL_CODE_MAP.values() + if wrong_display != correct_display + ], +) +def test_validate_retrieval_mechanism_extension_display_mismatch(code, display): + validator = DocumentReferenceValidator() + ext = make_retrieval_mechanism_extension(code, display) + assert validator._validate_retrieval_mechanism_extension(ext, 0, 0) is False + assert any( + f"Invalid content extension display: {display} Expected display is '{CONTENT_RETRIEVAL_CODE_MAP.get(code)}'" + in issue.diagnostics + for issue in validator.result.issues + ) diff --git a/layer/nrlf/producer/fhir/r4/model.py b/layer/nrlf/producer/fhir/r4/model.py index f4bcf7eaa..8b0e3fc64 100644 --- a/layer/nrlf/producer/fhir/r4/model.py +++ b/layer/nrlf/producer/fhir/r4/model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-28T16:30:27+00:00 +# timestamp: 2025-09-05T08:22:07+00:00 from __future__ import annotations @@ -287,7 +287,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] class NRLFormatCode(Coding): diff --git a/layer/nrlf/producer/fhir/r4/strict_model.py b/layer/nrlf/producer/fhir/r4/strict_model.py index edac6b8a7..87cdefcdc 100644 --- a/layer/nrlf/producer/fhir/r4/strict_model.py +++ b/layer/nrlf/producer/fhir/r4/strict_model.py @@ -1,6 +1,6 @@ # generated by datamodel-codegen: # filename: swagger.yaml -# timestamp: 2025-08-28T16:30:28+00:00 +# timestamp: 2025-09-05T08:22:08+00:00 from __future__ import annotations @@ -258,7 +258,7 @@ class ContentStabilityExtensionCoding(Coding): class RetrievalMechanismExtensionCoding(Coding): system: Literal["https://fhir.nhs.uk/England/CodeSystem/England-RetrievalMechanism"] code: Literal["SSP", "Direct", "LDR"] - display: Literal["Spine Secure Proxy", "Direct", "Large Document Repository"] + display: Literal["Spine Secure Proxy", "Direct", "Large Document Retrieval"] class NRLFormatCode(Coding): diff --git a/resources/fhir/NRLF-Retrieval-CodeSystem.json b/resources/fhir/NRLF-Retrieval-CodeSystem.json index 776011d5e..9d28dc3d2 100644 --- a/resources/fhir/NRLF-Retrieval-CodeSystem.json +++ b/resources/fhir/NRLF-Retrieval-CodeSystem.json @@ -54,8 +54,8 @@ }, { "code": "LDR", - "display": "Large Document Repository", - "definition": "This document can be retrieved via the Large Document Repository proxy service." + "display": "Large Document Retrieval", + "definition": "This document can be retrieved via the Large Document Retrieval proxy service." } ] } diff --git a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json index 72b3bc649..6af55f053 100644 --- a/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json +++ b/resources/fhir/NRLF-RetrievalMechanism-ValueSet.json @@ -32,7 +32,7 @@ }, { "code": "LDR", - "display": "Large Document Repository" + "display": "Large Document Retrieval" } ] } diff --git a/tests/features/producer/createDocumentReference-failure.feature b/tests/features/producer/createDocumentReference-failure.feature index ab233e982..099ae18c1 100644 --- a/tests/features/producer/createDocumentReference-failure.feature +++ b/tests/features/producer/createDocumentReference-failure.feature @@ -1436,7 +1436,7 @@ Feature: Producer - createDocumentReference - Failure Scenarios } ] }, - "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Direct' or 'Large Document Repository', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", + "diagnostics": "Invalid content retrieval extension (content[0].extension[0].valueCodeableConcept.coding[0].display: Input should be 'Spine Secure Proxy', 'Direct' or 'Large Document Retrieval', see: https://fhir.nhs.uk/England/ValueSet/England-RetrievalMechanism)", "expression": ["content[0].extension[0].valueCodeableConcept.coding[0].display"] } """ From 1ffa66a2f295657943e129cc2cb7eb263815b9db Mon Sep 17 00:00:00 2001 From: "Axel Garcia K." Date: Fri, 5 Sep 2025 14:07:45 +0100 Subject: [PATCH 20/20] NRL-1554 Fix unit test --- layer/nrlf/core/tests/test_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/layer/nrlf/core/tests/test_validators.py b/layer/nrlf/core/tests/test_validators.py index 577a69819..03c458ca7 100644 --- a/layer/nrlf/core/tests/test_validators.py +++ b/layer/nrlf/core/tests/test_validators.py @@ -1859,7 +1859,7 @@ def test_no_content_extensions(): ) def test_validate_content_stability_extension_valid(code, display): validator = DocumentReferenceValidator() - ext = make_content_stability_extension("static", "Static") + ext = make_content_stability_extension(code, display) assert validator._validate_content_stability_extension(ext, 0, 0) is True assert validator.result.issues == []