diff --git a/packages/validation/tests/test_validation.py b/packages/validation/tests/test_validation.py index 24682e6c..eaf77da2 100644 --- a/packages/validation/tests/test_validation.py +++ b/packages/validation/tests/test_validation.py @@ -1,9 +1,92 @@ from pathlib import Path +import pytest + +from validation import main as validation_main from validation import validate_eicr +class FakeAssert: + local_name = "failed-assert" + + def get_attribute_value(self, attribute: str) -> str: + values = { + "id": "ttc-labTestNameOrdered-noCode", + "location": "/ClinicalDocument/component/structuredBody/component/section/entry/observation", + } + + return values[attribute] + + +class FakeRoot: + def __init__(self) -> None: + """Represents the root of the SVRL output, which contains failed-assert children.""" + self.children = [FakeAssert()] + + +class FakeExecutable: + def apply_templates_returning_value(self, xdm_value: str) -> list[list[FakeRoot]]: + """Simulates applying the XSLT stylesheet to the XML document and returning SVRL output.""" + return [[FakeRoot()]] + + +class FakeXsltProcessor: + def __init__(self) -> None: + """Simulates the XSLT processor, recording transformations applied.""" + self.transforms: list[tuple[str, str, str]] = [] + + def transform_to_file(self, source_file: str, stylesheet_file: str, output_file: str) -> None: + """Simulates transforming an XML file with an XSLT stylesheet and writing to an output file.""" + self.transforms.append((source_file, stylesheet_file, output_file)) + Path(output_file).write_text("") + + def compile_stylesheet(self, stylesheet_file: str) -> FakeExecutable: + """Simulates compiling an XSLT stylesheet into an executable.""" + return FakeExecutable() + + +class FakeSaxonProcessor: + version = "Fake Saxon/C" + """Simulates the Saxon/C processor.""" + + def __init__(self, license: bool) -> None: + """Initializes the Saxon/C processor with a license flag and an XSLT processor.""" + self.license = license + self.xslt_processor = FakeXsltProcessor() + + def __enter__(self) -> "FakeSaxonProcessor": + """Enters the context manager, returning itself to be used for transformations.""" + return self + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Exits the context manager, performing any necessary cleanup (none in this fake implementation).""" + return + + def new_xslt30_processor(self) -> FakeXsltProcessor: + """Returns the XSLT processor to be used for transformations.""" + return self.xslt_processor + + def parse_xml(self, xml_text: str | None) -> str: + """Simulates parsing an XML string into an XDM node, which in this fake implementation is just the string itself.""" + return xml_text or "" + + +class BrokenSaxonProcessor: + def __init__(self, license: bool) -> None: + """Simulates a Saxon/C processor that raises an error when used, to test error handling in the validation function.""" + self.license = license + + def __enter__(self) -> "BrokenSaxonProcessor": + """Raises a RuntimeError to simulate a failure when entering the context manager.""" + raise RuntimeError("validator failed") + + def __exit__(self, exc_type: object, exc_value: object, traceback: object) -> None: + """Exits the context manager, which in this case is not reached due to the error raised in __enter__.""" + return + + def test_validation(): + """Tests that the validate_eicr function correctly processes an eICR and returns expected validation results.""" with Path.open("packages/validation/tests/assets/test_eicr.xml") as f: eicr = f.read() results = validate_eicr(eicr) @@ -17,8 +100,82 @@ def test_validation(): def test_validation_no_errors(): + """Tests that the validate_eicr function returns an empty list when there are no validation errors.""" with Path.open("e2e/snapshots/test_e2e/test_upload_and_process/augmented_eicr.xml") as f: eicr = f.read() results = validate_eicr(eicr) assert results == [] + + +def test_validation_redoes_all_steps(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """Tests that the validate_eicr function redoes all steps of the validation process when redo_all_steps is True, and that it returns expected validation results.""" + stage1_output = tmp_path / "stage1.sch.tmp" + stage2_output = tmp_path / "stage2.sch.tmp" + validator_output = tmp_path / "validator.xsl.tmp" + + stage1_output.write_text("old stage 1") + stage2_output.write_text("old stage 2") + validator_output.write_text("old validator") + + monkeypatch.setattr(validation_main, "STAGE1_OUTPUT", stage1_output) + monkeypatch.setattr(validation_main, "STAGE2_OUTPUT", stage2_output) + monkeypatch.setattr(validation_main, "VALIDATOR_OUTPUT", validator_output) + monkeypatch.setattr(validation_main, "APHL_SCHEMATRON", tmp_path / "schema.sch") + monkeypatch.setattr(validation_main, "XSLT_INCLUDE", tmp_path / "include.xsl") + monkeypatch.setattr(validation_main, "XSLT_EXPAND", tmp_path / "expand.xsl") + monkeypatch.setattr(validation_main, "XSLT_COMPILE", tmp_path / "compile.xsl") + monkeypatch.setattr(validation_main, "PySaxonProcessor", FakeSaxonProcessor) + + results = validate_eicr("", redo_all_steps=True) + + assert results == [ + { + "error_id": "ttc-labTestNameOrdered-noCode", + "location": "/ClinicalDocument/component/structuredBody/component/section/entry/observation", + } + ] + assert stage1_output.read_text() == "" + assert stage2_output.read_text() == "" + assert validator_output.read_text() == "" + + +def test_validation_uses_existing_generated_files(monkeypatch: pytest.MonkeyPatch, tmp_path: Path): + """Tests that the validate_eicr function uses existing generated files for steps 1-3 of the validation process when redo_all_steps is False, and that it returns expected validation results.""" + stage1_output = tmp_path / "stage1.sch.tmp" + stage2_output = tmp_path / "stage2.sch.tmp" + validator_output = tmp_path / "validator.xsl.tmp" + + stage1_output.write_text("existing stage 1") + stage2_output.write_text("existing stage 2") + validator_output.write_text("existing validator") + + monkeypatch.setattr(validation_main, "STAGE1_OUTPUT", stage1_output) + monkeypatch.setattr(validation_main, "STAGE2_OUTPUT", stage2_output) + monkeypatch.setattr(validation_main, "VALIDATOR_OUTPUT", validator_output) + monkeypatch.setattr(validation_main, "PySaxonProcessor", FakeSaxonProcessor) + + results = validate_eicr("") + + assert results == [ + { + "error_id": "ttc-labTestNameOrdered-noCode", + "location": "/ClinicalDocument/component/structuredBody/component/section/entry/observation", + } + ] + assert stage1_output.read_text() == "existing stage 1" + assert stage2_output.read_text() == "existing stage 2" + assert validator_output.read_text() == "existing validator" + + +def test_validation_returns_empty_list_when_validator_errors( + monkeypatch: pytest.MonkeyPatch, + caplog: pytest.LogCaptureFixture, +): + """Tests that the validate_eicr function returns an empty list and logs an error when the validator fails.""" + monkeypatch.setattr(validation_main, "PySaxonProcessor", BrokenSaxonProcessor) + + results = validate_eicr("") + + assert results == [] + assert "An error occurred during validation: validator failed" in caplog.text