Files
prowler/api/src/backend/tasks/tests/test_reports_nis2.py
T

1158 lines
41 KiB
Python

import io
from unittest.mock import Mock, patch
import pytest
from reportlab.lib.units import inch
from reportlab.platypus import Image, PageBreak, Paragraph, Table
from tasks.jobs.reports import FRAMEWORK_REGISTRY, ComplianceData, RequirementData
from tasks.jobs.reports import nis2 as nis2_report_module
from tasks.jobs.reports.nis2 import NIS2ReportGenerator, _extract_section_number
from tasks.tests.report_test_helpers import patch_chart_helpers
# Use string status values directly to avoid Django DB initialization
# These match api.models.StatusChoices values
class StatusChoices:
"""Mock StatusChoices to avoid Django DB initialization."""
PASS = "PASS"
FAIL = "FAIL"
MANUAL = "MANUAL"
# =============================================================================
# Fixtures
# =============================================================================
@pytest.fixture
def nis2_generator():
"""Create a NIS2ReportGenerator instance for testing."""
config = FRAMEWORK_REGISTRY["nis2"]
return NIS2ReportGenerator(config)
@pytest.fixture
def patched_nis2_charts(monkeypatch):
return patch_chart_helpers(
monkeypatch, nis2_report_module, ("create_horizontal_bar_chart",)
)
@pytest.fixture
def mock_nis2_requirement_attribute_section1():
"""Create a mock NIS2 requirement attribute for Section 1."""
mock = Mock()
mock.Section = "1 POLICY ON THE SECURITY OF NETWORK AND INFORMATION SYSTEMS"
mock.SubSection = "1.1 Policy establishment"
mock.Description = "Establish security policies for network and information systems"
return mock
@pytest.fixture
def mock_nis2_requirement_attribute_section2():
"""Create a mock NIS2 requirement attribute for Section 2."""
mock = Mock()
mock.Section = "2 RISK MANAGEMENT"
mock.SubSection = "2.1 Risk assessment"
mock.Description = "Conduct risk assessments for critical infrastructure"
return mock
@pytest.fixture
def mock_nis2_requirement_attribute_section11():
"""Create a mock NIS2 requirement attribute for Section 11."""
mock = Mock()
mock.Section = "11 ACCESS CONTROL"
mock.SubSection = "11.2 User access management"
mock.Description = "Manage user access to systems and data"
return mock
@pytest.fixture
def mock_nis2_requirement_attribute_no_subsection():
"""Create a mock NIS2 requirement attribute without subsection."""
mock = Mock()
mock.Section = "3 INCIDENT HANDLING"
mock.SubSection = ""
mock.Description = "Handle security incidents effectively"
return mock
@pytest.fixture
def basic_nis2_compliance_data():
"""Create basic ComplianceData for NIS2 testing."""
return ComplianceData(
tenant_id="tenant-123",
scan_id="scan-456",
provider_id="provider-789",
compliance_id="nis2_aws",
framework="NIS2",
name="NIS2 Directive (EU) 2022/2555",
version="2022",
description="EU directive on security of network and information systems",
)
# =============================================================================
# Section Number Extraction Tests
# =============================================================================
class TestSectionNumberExtraction:
"""Test suite for section number extraction utility."""
def test_extract_simple_section_number(self):
"""Test extracting single digit section number."""
result = _extract_section_number("1 POLICY ON SECURITY")
assert result == "1"
def test_extract_double_digit_section_number(self):
"""Test extracting double digit section number."""
result = _extract_section_number("11 ACCESS CONTROL")
assert result == "11"
def test_extract_section_number_with_spaces(self):
"""Test extracting section number with leading/trailing spaces."""
result = _extract_section_number(" 2 RISK MANAGEMENT ")
assert result == "2"
def test_extract_section_number_empty_string(self):
"""Test extracting from empty string returns 'Other'."""
result = _extract_section_number("")
assert result == "Other"
def test_extract_section_number_none_like(self):
"""Test extracting from empty/None-like returns 'Other'."""
# Note: The function expects str, so we test empty string behavior
result = _extract_section_number("")
assert result == "Other"
def test_extract_section_number_no_number(self):
"""Test extracting from string without number returns 'Other'."""
result = _extract_section_number("POLICY ON SECURITY")
assert result == "Other"
def test_extract_section_number_letter_first(self):
"""Test extracting from string starting with letter returns 'Other'."""
result = _extract_section_number("A. Some Section")
assert result == "Other"
# =============================================================================
# Generator Initialization Tests
# =============================================================================
class TestNIS2GeneratorInitialization:
"""Test suite for NIS2 generator initialization."""
def test_generator_creation(self, nis2_generator):
"""Test that NIS2 generator is created correctly."""
assert nis2_generator is not None
assert nis2_generator.config.name == "nis2"
assert nis2_generator.config.language == "en"
def test_generator_no_niveles(self, nis2_generator):
"""Test that NIS2 config does not use niveles."""
assert nis2_generator.config.has_niveles is False
def test_generator_no_dimensions(self, nis2_generator):
"""Test that NIS2 config does not use dimensions."""
assert nis2_generator.config.has_dimensions is False
def test_generator_no_risk_levels(self, nis2_generator):
"""Test that NIS2 config does not use risk levels."""
assert nis2_generator.config.has_risk_levels is False
def test_generator_no_weight(self, nis2_generator):
"""Test that NIS2 config does not use weight."""
assert nis2_generator.config.has_weight is False
# =============================================================================
# Cover Page Tests
# =============================================================================
class TestNIS2CoverPage:
"""Test suite for NIS2 cover page generation."""
@patch("tasks.jobs.reports.nis2.Image")
def test_cover_page_has_logos(
self, mock_image, nis2_generator, basic_nis2_compliance_data
):
"""Test that cover page contains logos."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_cover_page(basic_nis2_compliance_data)
assert len(elements) > 0
# Should have called Image at least twice (prowler + nis2 logos)
assert mock_image.call_count >= 2
def test_cover_page_has_title(self, nis2_generator, basic_nis2_compliance_data):
"""Test that cover page contains the NIS2 title."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_cover_page(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "NIS2" in content or "Directive" in content
def test_cover_page_has_metadata_table(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test that cover page contains metadata table."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_cover_page(basic_nis2_compliance_data)
tables = [e for e in elements if isinstance(e, Table)]
assert len(tables) >= 1
def test_cover_page_wraps_long_account_id(
self, nis2_generator, basic_nis2_compliance_data
):
provider = Mock()
provider.provider = "oraclecloud"
provider.uid = "ocid1.tenancy.oc1.." + "a" * 70
provider.alias = None
basic_nis2_compliance_data.provider_obj = provider
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_cover_page(basic_nis2_compliance_data)
table = next(
e
for e in elements
if isinstance(e, Table)
and any(row[0] == "Account ID:" for row in e._cellvalues)
)
values = [row[1] for row in table._cellvalues]
assert all(isinstance(v, Paragraph) for v in values)
account_id = next(v for v in values if v.text == provider.uid)
_, height = account_id.wrap(4 * inch, 1000)
assert height > account_id.style.leading
# =============================================================================
# Executive Summary Tests
# =============================================================================
class TestNIS2ExecutiveSummary:
"""Test suite for NIS2 executive summary generation."""
def test_executive_summary_has_english_title(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test that executive summary has English title."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "Executive Summary" in content
def test_executive_summary_calculates_compliance(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that executive summary calculates compliance percentage."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Passed requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Failed requirement",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
# Should contain tables with metrics
tables = [e for e in elements if isinstance(e, Table)]
assert len(tables) >= 1
def test_executive_summary_shows_all_statuses(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that executive summary shows passed, failed, and manual counts."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Passed",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Failed",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
),
RequirementData(
id="REQ-003",
description="Manual",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-003": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
# Should have a summary table with all statuses
assert len(elements) > 0
def test_executive_summary_excludes_manual_from_percentage(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that manual requirements are excluded from compliance percentage."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Passed",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Manual",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
# Should calculate 100% (only 1 evaluated requirement that passed)
assert len(elements) > 0
# =============================================================================
# Charts Section Tests
# =============================================================================
class TestNIS2ChartsSection:
"""Test suite for NIS2 charts section generation."""
def test_charts_section_has_section_chart_title(
self, nis2_generator, basic_nis2_compliance_data, patched_nis2_charts
):
"""Test that charts section has section compliance title."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_charts_section(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "Section" in content or "Compliance" in content
assert any(isinstance(e, Image) for e in elements)
chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"]
assert chart_kwargs["labels"] == []
assert chart_kwargs["values"] == []
assert chart_kwargs["xlabel"] == "Compliance (%)"
def test_charts_section_has_page_break(
self, nis2_generator, basic_nis2_compliance_data, patched_nis2_charts
):
"""Test that charts section has page breaks."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_charts_section(basic_nis2_compliance_data)
page_breaks = [e for e in elements if isinstance(e, PageBreak)]
assert len(page_breaks) >= 1
assert len(patched_nis2_charts["create_horizontal_bar_chart"]) == 1
def test_charts_section_has_subsection_breakdown(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
patched_nis2_charts,
):
"""Test that charts section includes subsection breakdown table."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Test requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
elements = nis2_generator.create_charts_section(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "SubSection" in content or "Breakdown" in content
assert any(isinstance(e, Image) for e in elements)
chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"]
assert chart_kwargs["labels"] == ["1. Policy on Security"]
assert chart_kwargs["values"] == [100.0]
assert chart_kwargs["color_func"]
# =============================================================================
# Section Chart Tests
# =============================================================================
class TestNIS2SectionChart:
"""Test suite for NIS2 section compliance chart."""
def test_section_chart_creation(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
patched_nis2_charts,
):
"""Test that section chart is created successfully."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Test requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
chart_buffer = nis2_generator._create_section_chart(basic_nis2_compliance_data)
assert isinstance(chart_buffer, io.BytesIO)
assert chart_buffer.getvalue() # Not empty
chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"]
assert chart_kwargs["labels"] == ["1. Policy on Security"]
assert chart_kwargs["values"] == [100.0]
assert chart_kwargs["xlabel"] == "Compliance (%)"
def test_section_chart_excludes_manual(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
patched_nis2_charts,
):
"""Test that manual requirements are excluded from section chart."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Auto requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Manual requirement",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
# Should not raise any errors
chart_buffer = nis2_generator._create_section_chart(basic_nis2_compliance_data)
assert isinstance(chart_buffer, io.BytesIO)
chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"]
assert chart_kwargs["labels"] == ["1. Policy on Security"]
assert chart_kwargs["values"] == [100.0]
def test_section_chart_multiple_sections(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
mock_nis2_requirement_attribute_section2,
mock_nis2_requirement_attribute_section11,
patched_nis2_charts,
):
"""Test section chart with multiple sections."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Section 1 requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Section 2 requirement",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
),
RequirementData(
id="REQ-003",
description="Section 11 requirement",
status=StatusChoices.PASS,
passed_findings=5,
failed_findings=0,
total_findings=5,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section2]
}
},
"REQ-003": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section11]
}
},
}
chart_buffer = nis2_generator._create_section_chart(basic_nis2_compliance_data)
assert isinstance(chart_buffer, io.BytesIO)
chart_kwargs = patched_nis2_charts["create_horizontal_bar_chart"][0]["kwargs"]
assert chart_kwargs["labels"] == [
"1. Policy on Security",
"2. Risk Management",
"11. Access Control",
]
assert chart_kwargs["values"] == [100.0, 0.0, 100.0]
# =============================================================================
# SubSection Table Tests
# =============================================================================
class TestNIS2SubSectionTable:
"""Test suite for NIS2 subsection breakdown table."""
def test_subsection_table_creation(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that subsection table is created successfully."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Test requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
table = nis2_generator._create_subsection_table(basic_nis2_compliance_data)
assert isinstance(table, Table)
def test_subsection_table_counts_statuses(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that subsection table counts passed, failed, and manual."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Passed",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Failed",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
),
RequirementData(
id="REQ-003",
description="Manual",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-003": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
table = nis2_generator._create_subsection_table(basic_nis2_compliance_data)
assert isinstance(table, Table)
def test_subsection_table_no_subsection(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_no_subsection,
):
"""Test subsection table when requirements have no subsection."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="No subsection requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_no_subsection]
}
},
}
table = nis2_generator._create_subsection_table(basic_nis2_compliance_data)
assert isinstance(table, Table)
# =============================================================================
# Requirements Index Tests
# =============================================================================
class TestNIS2RequirementsIndex:
"""Test suite for NIS2 requirements index generation."""
def test_requirements_index_has_title(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test that requirements index has English title."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
assert "Requirements Index" in content
def test_requirements_index_organized_by_section(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
mock_nis2_requirement_attribute_section2,
):
"""Test that requirements index is organized by section."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Section 1 requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Section 2 requirement",
status=StatusChoices.PASS,
passed_findings=5,
failed_findings=0,
total_findings=5,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section2]
}
},
}
elements = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
# Should have section headers
assert "Policy" in content or "Risk" in content or "1." in content
def test_requirements_index_shows_status_indicators(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test that requirements index shows pass/fail/manual indicators."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Passed requirement",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Failed requirement",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
),
RequirementData(
id="REQ-003",
description="Manual requirement",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-003": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
}
elements = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
paragraphs = [e for e in elements if isinstance(e, Paragraph)]
content = " ".join(str(p.text) for p in paragraphs)
# Should have status indicators
assert "✓" in content or "✗" in content or "⊙" in content
def test_requirements_index_truncates_long_descriptions(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test that long descriptions are truncated."""
mock_attr = Mock()
mock_attr.Section = "1 POLICY"
mock_attr.SubSection = "1.1 Long subsection name"
mock_attr.Description = "A" * 100 # Very long description
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="A" * 100,
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {"attributes": {"req_attributes": [mock_attr]}},
}
# Should not raise errors
elements = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
assert len(elements) > 0
# =============================================================================
# Section Key Sorting Tests
# =============================================================================
class TestNIS2SectionKeySorting:
"""Test suite for NIS2 section key sorting."""
def test_sort_simple_sections(self, nis2_generator):
"""Test sorting simple section numbers."""
result = nis2_generator._sort_section_key("1")
assert result == (1,)
result = nis2_generator._sort_section_key("2")
assert result == (2,)
def test_sort_subsections(self, nis2_generator):
"""Test sorting subsection numbers."""
result = nis2_generator._sort_section_key("1.1")
assert result == (1, 1)
result = nis2_generator._sort_section_key("1.2")
assert result == (1, 2)
def test_sort_double_digit_sections(self, nis2_generator):
"""Test sorting double digit section numbers."""
result = nis2_generator._sort_section_key("11")
assert result == (11,)
result = nis2_generator._sort_section_key("11.2")
assert result == (11, 2)
def test_sort_order_is_correct(self, nis2_generator):
"""Test that sort order is numerically correct."""
keys = ["11", "1", "2", "1.2", "1.1", "11.2", "2.1"]
sorted_keys = sorted(keys, key=nis2_generator._sort_section_key)
assert sorted_keys == ["1", "1.1", "1.2", "2", "2.1", "11", "11.2"]
def test_sort_invalid_key(self, nis2_generator):
"""Test sorting invalid section key."""
result = nis2_generator._sort_section_key("Other")
# Should contain infinity for non-numeric parts
assert result[0] == float("inf")
# =============================================================================
# Empty Data Tests
# =============================================================================
class TestNIS2EmptyData:
"""Test suite for NIS2 with empty or minimal data."""
def test_executive_summary_empty_requirements(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test executive summary with no requirements."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
assert len(elements) > 0
def test_charts_section_empty_requirements(
self, nis2_generator, basic_nis2_compliance_data
):
"""Test charts section with no requirements."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_charts_section(basic_nis2_compliance_data)
assert len(elements) > 0
def test_requirements_index_empty(self, nis2_generator, basic_nis2_compliance_data):
"""Test requirements index with no requirements."""
basic_nis2_compliance_data.requirements = []
basic_nis2_compliance_data.attributes_by_requirement_id = {}
elements = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
# Should at least have the title
assert len(elements) >= 1
# =============================================================================
# All Pass / All Fail Tests
# =============================================================================
class TestNIS2EdgeCases:
"""Test suite for NIS2 edge cases."""
def test_all_requirements_pass(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test with all requirements passing."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id=f"REQ-{i:03d}",
description=f"Passing requirement {i}",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
)
for i in range(1, 6)
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
f"REQ-{i:03d}": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
}
for i in range(1, 6)
}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
assert len(elements) > 0
def test_all_requirements_fail(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test with all requirements failing."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id=f"REQ-{i:03d}",
description=f"Failing requirement {i}",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=10,
total_findings=10,
)
for i in range(1, 6)
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
f"REQ-{i:03d}": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
}
for i in range(1, 6)
}
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
assert len(elements) > 0
def test_all_requirements_manual(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
):
"""Test with all requirements being manual."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id=f"REQ-{i:03d}",
description=f"Manual requirement {i}",
status=StatusChoices.MANUAL,
passed_findings=0,
failed_findings=0,
total_findings=0,
)
for i in range(1, 6)
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
f"REQ-{i:03d}": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
}
for i in range(1, 6)
}
# Should handle gracefully - compliance should be 100% when no evaluated
elements = nis2_generator.create_executive_summary(basic_nis2_compliance_data)
assert len(elements) > 0
# =============================================================================
# Integration Tests
# =============================================================================
class TestNIS2Integration:
"""Integration tests for NIS2 report generation."""
def test_full_report_generation_flow(
self,
nis2_generator,
basic_nis2_compliance_data,
mock_nis2_requirement_attribute_section1,
mock_nis2_requirement_attribute_section2,
):
"""Test the complete report generation flow."""
basic_nis2_compliance_data.requirements = [
RequirementData(
id="REQ-001",
description="Section 1 passed",
status=StatusChoices.PASS,
passed_findings=10,
failed_findings=0,
total_findings=10,
),
RequirementData(
id="REQ-002",
description="Section 2 failed",
status=StatusChoices.FAIL,
passed_findings=0,
failed_findings=5,
total_findings=5,
),
]
basic_nis2_compliance_data.attributes_by_requirement_id = {
"REQ-001": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section1]
}
},
"REQ-002": {
"attributes": {
"req_attributes": [mock_nis2_requirement_attribute_section2]
}
},
}
# Generate all sections
exec_summary = nis2_generator.create_executive_summary(
basic_nis2_compliance_data
)
charts = nis2_generator.create_charts_section(basic_nis2_compliance_data)
index = nis2_generator.create_requirements_index(basic_nis2_compliance_data)
# All sections should generate without errors
assert len(exec_summary) > 0
assert len(charts) > 0
assert len(index) > 0