diff --git a/tests/trestle/core/crm/exports_reader_test.py b/tests/trestle/core/crm/exports_reader_test.py index e90b73897..b4e397ca7 100644 --- a/tests/trestle/core/crm/exports_reader_test.py +++ b/tests/trestle/core/crm/exports_reader_test.py @@ -14,11 +14,15 @@ """Tests for the ssp_generator module.""" import pathlib +import uuid + +import pytest from tests import test_utils import trestle.common.const as const import trestle.core.crm.export_reader as exportreader +import trestle.core.generators as gens import trestle.oscal.ssp as ossp from trestle.common.model_utils import ModelUtils from trestle.core.models.file_content_type import FileContentType @@ -33,6 +37,16 @@ example_responsibility_uuid = '4b34c68f-75fa-4b38-baf0-e50158c13ac2' +@pytest.fixture(scope='function') +def sample_implemented_requirement() -> ossp.ImplementedRequirement: + """Return a valid ComponentDefinition object with some contents.""" + # one component has no properties - the other has two + impl_req: ossp.ImplementedRequirement = gens.generate_sample_model(ossp.ImplementedRequirement) + by_comp: ossp.ByComponent = gens.generate_sample_model(ossp.ByComponent) + impl_req.by_components = [by_comp] + return impl_req + + def prep_inheritance_dir(ac_appliance_dir: pathlib.Path) -> None: """Prepare inheritance directory with basic information.""" ac_2 = ac_appliance_dir.joinpath('ac-2') @@ -180,3 +194,35 @@ def test_read_inheritance_markdown_dir_with_multiple_leveraged_components(tmp_tr assert len(inheritance_info[0]) == 2 assert len(inheritance_info[1]) == 2 + + +def test_update_type_with_by_comp(sample_implemented_requirement: ossp.ImplementedRequirement) -> None: + """Test update type with by component.""" + test_ssp: ossp.SystemSecurityPlan = gens.generate_sample_model(ossp.SystemSecurityPlan) + reader = exportreader.ExportReader('', test_ssp) + + test_inherited: ossp.Inherited = gens.generate_sample_model(ossp.Inherited) + test_satisfied: ossp.Satisfied = gens.generate_sample_model(ossp.Satisfied) + + test_comp_uuid = str(uuid.uuid4()) + + test_by_comp_dict: exportreader.ByComponentDict = {test_comp_uuid: ([test_inherited], [test_satisfied])} + + assert len(sample_implemented_requirement.by_components) == 1 + + reader._update_type_with_by_comp(sample_implemented_requirement, test_by_comp_dict) + + # Ensure a new by_comp was added, but the original was not removed + assert len(sample_implemented_requirement.by_components) == 2 + + # Test update the existing without adding a new component + test_satisfied.description = 'Updated Description' + test_by_comp_dict: exportreader.ByComponentDict = {test_comp_uuid: ([test_inherited], [test_satisfied])} + reader._update_type_with_by_comp(sample_implemented_requirement, test_by_comp_dict) + + assert len(sample_implemented_requirement.by_components) == 2 + new_by_comp = sample_implemented_requirement.by_components[1] # type: ignore + + assert new_by_comp.component_uuid == test_comp_uuid + assert new_by_comp.satisfied is not None + assert new_by_comp.satisfied[0].description == 'Updated Description' diff --git a/trestle/core/crm/export_reader.py b/trestle/core/crm/export_reader.py index dcf88519a..ae7d827b0 100644 --- a/trestle/core/crm/export_reader.py +++ b/trestle/core/crm/export_reader.py @@ -20,6 +20,7 @@ import trestle.core.generators as gens import trestle.oscal.ssp as ossp +from trestle.common.common_types import TypeWithByComps from trestle.common.list_utils import as_list, none_if_empty from trestle.core.crm.bycomp_interface import ByComponentInterface from trestle.core.crm.leveraged_statements import InheritanceMarkdownReader @@ -89,27 +90,9 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the control id existing in the markdown, then update the by_components if implemented_requirement.control_id in markdown_dict: - new_by_comp: List[ossp.ByComponent] = [] by_comp_dict: ByComponentDict = markdown_dict[implemented_requirement.control_id] - for by_comp in as_list(implemented_requirement.by_components): - - if by_comp.component_uuid in by_comp_dict: - comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - - bycomp_interface = ByComponentInterface(by_comp) - by_comp = bycomp_interface.reconcile_inheritance_by_component( - comp_inheritance_info[0], comp_inheritance_info[1] - ) - - # Delete the entry from the by_comp_dict once processed to avoid duplicates - del by_comp_dict[by_comp.component_uuid] - - new_by_comp.append(by_comp) - - # Add any new by_components that were not in the original implemented requirement - new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - implemented_requirement.by_components = new_by_comp + self._update_type_with_by_comp(implemented_requirement, by_comp_dict) # Delete the entry from the markdown_dict once processed to avoid duplicates del markdown_dict[implemented_requirement.control_id] @@ -123,34 +106,40 @@ def _merge_exports_implemented_requirements(self, markdown_dict: InheritanceView # If the statement id existing in the markdown, then update the by_components if statement_id in markdown_dict: - new_by_comp: List[ossp.ByComponent] = [] by_comp_dict: ByComponentDict = markdown_dict[statement_id] - for by_comp in as_list(stm.by_components): + self._update_type_with_by_comp(stm, by_comp_dict) - if by_comp.component_uuid in by_comp_dict: - comp_inheritance_info = by_comp_dict[by_comp.component_uuid] + # Delete the entry from the markdown_dict once processed to avoid duplicates + del markdown_dict[statement_id] - bycomp_interface = ByComponentInterface(by_comp) - by_comp = bycomp_interface.reconcile_inheritance_by_component( - comp_inheritance_info[0], comp_inheritance_info[1] - ) + new_statements.append(stm) - # Delete the entry from the by_comp_dict once processed to avoid duplicates - del by_comp_dict[by_comp.component_uuid] + implemented_requirement.statements = none_if_empty(new_statements) - new_by_comp.append(by_comp) + def _update_type_with_by_comp(self, with_bycomp: TypeWithByComps, by_comp_dict: ByComponentDict) -> None: + """Update the by_components for a type with by_components.""" + new_by_comp: List[ossp.ByComponent] = [] - # Add any new by_components that were not in the original statement - new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) - stm.by_components = new_by_comp + by_comp: ossp.ByComponent + for by_comp in as_list(with_bycomp.by_components): - # Delete the entry from the markdown_dict once processed to avoid duplicates - del markdown_dict[statement_id] + if by_comp.component_uuid in by_comp_dict: + comp_inheritance_info = by_comp_dict[by_comp.component_uuid] - new_statements.append(stm) + bycomp_interface = ByComponentInterface(by_comp) + by_comp = bycomp_interface.reconcile_inheritance_by_component( + comp_inheritance_info[0], comp_inheritance_info[1] + ) - implemented_requirement.statements = none_if_empty(new_statements) + # Delete the entry from the by_comp_dict once processed to avoid duplicates + del by_comp_dict[by_comp.component_uuid] + + new_by_comp.append(by_comp) + + # Add any new by_components that were not in the original statement + new_by_comp.extend(ExportReader._add_new_by_comps(by_comp_dict)) + with_bycomp.by_components = none_if_empty(new_by_comp) def _add_control_mappings_to_implemented_requirements( self, control_mapping: str, by_comps: ByComponentDict