mirror of
https://github.com/prowler-cloud/prowler.git
synced 2025-12-19 05:17:47 +00:00
1134 lines
40 KiB
Python
1134 lines
40 KiB
Python
import io
|
|
|
|
import pytest
|
|
from reportlab.lib.units import inch
|
|
from reportlab.platypus import Image, LongTable, Paragraph, Spacer, Table
|
|
from tasks.jobs.reports import ( # Configuration; Colors; Components; Charts; Base
|
|
CHART_COLOR_GREEN_1,
|
|
CHART_COLOR_GREEN_2,
|
|
CHART_COLOR_ORANGE,
|
|
CHART_COLOR_RED,
|
|
CHART_COLOR_YELLOW,
|
|
COLOR_BLUE,
|
|
COLOR_DARK_GRAY,
|
|
COLOR_HIGH_RISK,
|
|
COLOR_LOW_RISK,
|
|
COLOR_MEDIUM_RISK,
|
|
COLOR_SAFE,
|
|
FRAMEWORK_REGISTRY,
|
|
BaseComplianceReportGenerator,
|
|
ColumnConfig,
|
|
ComplianceData,
|
|
FrameworkConfig,
|
|
RequirementData,
|
|
create_badge,
|
|
create_data_table,
|
|
create_findings_table,
|
|
create_horizontal_bar_chart,
|
|
create_info_table,
|
|
create_multi_badge_row,
|
|
create_pdf_styles,
|
|
create_pie_chart,
|
|
create_radar_chart,
|
|
create_risk_component,
|
|
create_section_header,
|
|
create_stacked_bar_chart,
|
|
create_status_badge,
|
|
create_summary_table,
|
|
create_vertical_bar_chart,
|
|
get_chart_color_for_percentage,
|
|
get_color_for_compliance,
|
|
get_color_for_risk_level,
|
|
get_color_for_weight,
|
|
get_framework_config,
|
|
get_status_color,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Configuration Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestFrameworkConfig:
|
|
"""Tests for FrameworkConfig dataclass."""
|
|
|
|
def test_framework_config_creation(self):
|
|
"""Test creating a FrameworkConfig with required fields."""
|
|
config = FrameworkConfig(
|
|
name="test_framework",
|
|
display_name="Test Framework",
|
|
)
|
|
|
|
assert config.name == "test_framework"
|
|
assert config.display_name == "Test Framework"
|
|
assert config.logo_filename is None
|
|
assert config.language == "en"
|
|
assert config.has_risk_levels is False
|
|
|
|
def test_framework_config_with_all_fields(self):
|
|
"""Test creating a FrameworkConfig with all fields."""
|
|
config = FrameworkConfig(
|
|
name="custom",
|
|
display_name="Custom Framework",
|
|
logo_filename="custom_logo.png",
|
|
primary_color=COLOR_BLUE,
|
|
secondary_color=COLOR_SAFE,
|
|
attribute_fields=["Section", "SubSection"],
|
|
sections=["1. Security", "2. Compliance"],
|
|
language="es",
|
|
has_risk_levels=True,
|
|
has_dimensions=True,
|
|
has_niveles=True,
|
|
has_weight=True,
|
|
)
|
|
|
|
assert config.name == "custom"
|
|
assert config.logo_filename == "custom_logo.png"
|
|
assert config.language == "es"
|
|
assert config.has_risk_levels is True
|
|
assert config.has_dimensions is True
|
|
assert len(config.attribute_fields) == 2
|
|
assert len(config.sections) == 2
|
|
|
|
|
|
class TestFrameworkRegistry:
|
|
"""Tests for the framework registry."""
|
|
|
|
def test_registry_contains_threatscore(self):
|
|
"""Test that ThreatScore is in the registry."""
|
|
assert "prowler_threatscore" in FRAMEWORK_REGISTRY
|
|
config = FRAMEWORK_REGISTRY["prowler_threatscore"]
|
|
assert config.has_risk_levels is True
|
|
assert config.has_weight is True
|
|
|
|
def test_registry_contains_ens(self):
|
|
"""Test that ENS is in the registry."""
|
|
assert "ens" in FRAMEWORK_REGISTRY
|
|
config = FRAMEWORK_REGISTRY["ens"]
|
|
assert config.language == "es"
|
|
assert config.has_niveles is True
|
|
assert config.has_dimensions is True
|
|
|
|
def test_registry_contains_nis2(self):
|
|
"""Test that NIS2 is in the registry."""
|
|
assert "nis2" in FRAMEWORK_REGISTRY
|
|
config = FRAMEWORK_REGISTRY["nis2"]
|
|
assert config.language == "en"
|
|
|
|
def test_get_framework_config_threatscore(self):
|
|
"""Test getting ThreatScore config."""
|
|
config = get_framework_config("prowler_threatscore_aws")
|
|
assert config is not None
|
|
assert config.name == "prowler_threatscore"
|
|
|
|
def test_get_framework_config_ens(self):
|
|
"""Test getting ENS config."""
|
|
config = get_framework_config("ens_rd2022_aws")
|
|
assert config is not None
|
|
assert config.name == "ens"
|
|
|
|
def test_get_framework_config_nis2(self):
|
|
"""Test getting NIS2 config."""
|
|
config = get_framework_config("nis2_aws")
|
|
assert config is not None
|
|
assert config.name == "nis2"
|
|
|
|
def test_get_framework_config_unknown(self):
|
|
"""Test getting unknown framework returns None."""
|
|
config = get_framework_config("unknown_framework")
|
|
assert config is None
|
|
|
|
|
|
# =============================================================================
|
|
# Color Helper Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestColorHelpers:
|
|
"""Tests for color helper functions."""
|
|
|
|
def test_get_color_for_risk_level_high(self):
|
|
"""Test high risk level returns red."""
|
|
assert get_color_for_risk_level(5) == COLOR_HIGH_RISK
|
|
assert get_color_for_risk_level(4) == COLOR_HIGH_RISK
|
|
|
|
def test_get_color_for_risk_level_very_high(self):
|
|
"""Test very high risk level (>5) still returns high risk color."""
|
|
assert get_color_for_risk_level(10) == COLOR_HIGH_RISK
|
|
assert get_color_for_risk_level(100) == COLOR_HIGH_RISK
|
|
|
|
def test_get_color_for_risk_level_medium(self):
|
|
"""Test medium risk level returns orange."""
|
|
assert get_color_for_risk_level(3) == COLOR_MEDIUM_RISK
|
|
|
|
def test_get_color_for_risk_level_low(self):
|
|
"""Test low risk level returns yellow."""
|
|
assert get_color_for_risk_level(2) == COLOR_LOW_RISK
|
|
|
|
def test_get_color_for_risk_level_safe(self):
|
|
"""Test safe risk level returns green."""
|
|
assert get_color_for_risk_level(1) == COLOR_SAFE
|
|
assert get_color_for_risk_level(0) == COLOR_SAFE
|
|
|
|
def test_get_color_for_risk_level_negative(self):
|
|
"""Test negative risk level returns safe color."""
|
|
assert get_color_for_risk_level(-1) == COLOR_SAFE
|
|
|
|
def test_get_color_for_weight_high(self):
|
|
"""Test high weight returns red."""
|
|
assert get_color_for_weight(150) == COLOR_HIGH_RISK
|
|
assert get_color_for_weight(101) == COLOR_HIGH_RISK
|
|
|
|
def test_get_color_for_weight_medium(self):
|
|
"""Test medium weight returns yellow."""
|
|
assert get_color_for_weight(100) == COLOR_LOW_RISK
|
|
assert get_color_for_weight(51) == COLOR_LOW_RISK
|
|
|
|
def test_get_color_for_weight_low(self):
|
|
"""Test low weight returns green."""
|
|
assert get_color_for_weight(50) == COLOR_SAFE
|
|
assert get_color_for_weight(0) == COLOR_SAFE
|
|
|
|
def test_get_color_for_compliance_high(self):
|
|
"""Test high compliance returns green."""
|
|
assert get_color_for_compliance(100) == COLOR_SAFE
|
|
assert get_color_for_compliance(80) == COLOR_SAFE
|
|
|
|
def test_get_color_for_compliance_medium(self):
|
|
"""Test medium compliance returns yellow."""
|
|
assert get_color_for_compliance(79) == COLOR_LOW_RISK
|
|
assert get_color_for_compliance(60) == COLOR_LOW_RISK
|
|
|
|
def test_get_color_for_compliance_low(self):
|
|
"""Test low compliance returns red."""
|
|
assert get_color_for_compliance(59) == COLOR_HIGH_RISK
|
|
assert get_color_for_compliance(0) == COLOR_HIGH_RISK
|
|
|
|
def test_get_status_color_pass(self):
|
|
"""Test PASS status returns green."""
|
|
assert get_status_color("PASS") == COLOR_SAFE
|
|
assert get_status_color("pass") == COLOR_SAFE
|
|
|
|
def test_get_status_color_fail(self):
|
|
"""Test FAIL status returns red."""
|
|
assert get_status_color("FAIL") == COLOR_HIGH_RISK
|
|
assert get_status_color("fail") == COLOR_HIGH_RISK
|
|
|
|
def test_get_status_color_manual(self):
|
|
"""Test MANUAL status returns gray."""
|
|
assert get_status_color("MANUAL") == COLOR_DARK_GRAY
|
|
|
|
|
|
class TestChartColorHelpers:
|
|
"""Tests for chart color functions."""
|
|
|
|
def test_chart_color_for_high_percentage(self):
|
|
"""Test high percentage returns green."""
|
|
assert get_chart_color_for_percentage(100) == CHART_COLOR_GREEN_1
|
|
assert get_chart_color_for_percentage(80) == CHART_COLOR_GREEN_1
|
|
|
|
def test_chart_color_for_medium_high_percentage(self):
|
|
"""Test medium-high percentage returns light green."""
|
|
assert get_chart_color_for_percentage(79) == CHART_COLOR_GREEN_2
|
|
assert get_chart_color_for_percentage(60) == CHART_COLOR_GREEN_2
|
|
|
|
def test_chart_color_for_medium_percentage(self):
|
|
"""Test medium percentage returns yellow."""
|
|
assert get_chart_color_for_percentage(59) == CHART_COLOR_YELLOW
|
|
assert get_chart_color_for_percentage(40) == CHART_COLOR_YELLOW
|
|
|
|
def test_chart_color_for_medium_low_percentage(self):
|
|
"""Test medium-low percentage returns orange."""
|
|
assert get_chart_color_for_percentage(39) == CHART_COLOR_ORANGE
|
|
assert get_chart_color_for_percentage(20) == CHART_COLOR_ORANGE
|
|
|
|
def test_chart_color_for_low_percentage(self):
|
|
"""Test low percentage returns red."""
|
|
assert get_chart_color_for_percentage(19) == CHART_COLOR_RED
|
|
assert get_chart_color_for_percentage(0) == CHART_COLOR_RED
|
|
|
|
def test_chart_color_boundary_values(self):
|
|
"""Test chart color at exact boundary values."""
|
|
# Exact boundaries
|
|
assert get_chart_color_for_percentage(80) == CHART_COLOR_GREEN_1
|
|
assert get_chart_color_for_percentage(60) == CHART_COLOR_GREEN_2
|
|
assert get_chart_color_for_percentage(40) == CHART_COLOR_YELLOW
|
|
assert get_chart_color_for_percentage(20) == CHART_COLOR_ORANGE
|
|
|
|
|
|
# =============================================================================
|
|
# Component Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestBadgeComponents:
|
|
"""Tests for badge component functions."""
|
|
|
|
def test_create_badge_returns_table(self):
|
|
"""Test create_badge returns a Table object."""
|
|
badge = create_badge("Test", COLOR_BLUE)
|
|
assert isinstance(badge, Table)
|
|
|
|
def test_create_badge_with_custom_width(self):
|
|
"""Test create_badge with custom width."""
|
|
badge = create_badge("Test", COLOR_BLUE, width=2 * inch)
|
|
assert badge is not None
|
|
|
|
def test_create_status_badge_pass(self):
|
|
"""Test status badge for PASS."""
|
|
badge = create_status_badge("PASS")
|
|
assert isinstance(badge, Table)
|
|
|
|
def test_create_status_badge_fail(self):
|
|
"""Test status badge for FAIL."""
|
|
badge = create_status_badge("FAIL")
|
|
assert badge is not None
|
|
|
|
def test_create_multi_badge_row_with_badges(self):
|
|
"""Test multi-badge row with data."""
|
|
badges = [
|
|
("A", COLOR_BLUE),
|
|
("B", COLOR_SAFE),
|
|
]
|
|
table = create_multi_badge_row(badges)
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_multi_badge_row_empty(self):
|
|
"""Test multi-badge row with empty list."""
|
|
table = create_multi_badge_row([])
|
|
assert table is not None
|
|
|
|
|
|
class TestRiskComponent:
|
|
"""Tests for risk component function."""
|
|
|
|
def test_create_risk_component_returns_table(self):
|
|
"""Test risk component returns a Table."""
|
|
component = create_risk_component(risk_level=4, weight=100, score=50)
|
|
assert isinstance(component, Table)
|
|
|
|
def test_create_risk_component_high_risk(self):
|
|
"""Test risk component with high risk level."""
|
|
component = create_risk_component(risk_level=5, weight=150, score=100)
|
|
assert component is not None
|
|
|
|
def test_create_risk_component_low_risk(self):
|
|
"""Test risk component with low risk level."""
|
|
component = create_risk_component(risk_level=1, weight=10, score=10)
|
|
assert component is not None
|
|
|
|
|
|
class TestTableComponents:
|
|
"""Tests for table component functions."""
|
|
|
|
def test_create_info_table(self):
|
|
"""Test info table creation."""
|
|
rows = [
|
|
("Label 1:", "Value 1"),
|
|
("Label 2:", "Value 2"),
|
|
]
|
|
table = create_info_table(rows)
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_info_table_with_custom_widths(self):
|
|
"""Test info table with custom column widths."""
|
|
rows = [("Test:", "Value")]
|
|
table = create_info_table(rows, label_width=3 * inch, value_width=3 * inch)
|
|
assert table is not None
|
|
|
|
def test_create_data_table(self):
|
|
"""Test data table creation."""
|
|
data = [
|
|
{"name": "Item 1", "value": "100"},
|
|
{"name": "Item 2", "value": "200"},
|
|
]
|
|
columns = [
|
|
ColumnConfig("Name", 2 * inch, "name"),
|
|
ColumnConfig("Value", 1 * inch, "value"),
|
|
]
|
|
table = create_data_table(data, columns)
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_data_table_with_callable_field(self):
|
|
"""Test data table with callable field."""
|
|
data = [{"raw_value": 100}]
|
|
columns = [
|
|
ColumnConfig("Formatted", 2 * inch, lambda x: f"${x['raw_value']}"),
|
|
]
|
|
table = create_data_table(data, columns)
|
|
assert table is not None
|
|
|
|
def test_create_summary_table(self):
|
|
"""Test summary table creation."""
|
|
table = create_summary_table(
|
|
label="Score:",
|
|
value="85%",
|
|
value_color=COLOR_SAFE,
|
|
)
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_summary_table_with_custom_widths(self):
|
|
"""Test summary table with custom widths."""
|
|
table = create_summary_table(
|
|
label="ThreatScore:",
|
|
value="92.5%",
|
|
value_color=COLOR_SAFE,
|
|
label_width=3 * inch,
|
|
value_width=2.5 * inch,
|
|
)
|
|
assert isinstance(table, Table)
|
|
|
|
|
|
class TestFindingsTable:
|
|
"""Tests for findings table component."""
|
|
|
|
def test_create_findings_table_with_dicts(self):
|
|
"""Test findings table creation with dict data."""
|
|
findings = [
|
|
{
|
|
"title": "Finding 1",
|
|
"resource_name": "resource-1",
|
|
"severity": "HIGH",
|
|
"status": "FAIL",
|
|
"region": "us-east-1",
|
|
},
|
|
{
|
|
"title": "Finding 2",
|
|
"resource_name": "resource-2",
|
|
"severity": "LOW",
|
|
"status": "PASS",
|
|
"region": "eu-west-1",
|
|
},
|
|
]
|
|
table = create_findings_table(findings)
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_findings_table_with_custom_columns(self):
|
|
"""Test findings table with custom column configuration."""
|
|
findings = [{"name": "Test", "value": "100"}]
|
|
columns = [
|
|
ColumnConfig("Name", 2 * inch, "name"),
|
|
ColumnConfig("Value", 1 * inch, "value"),
|
|
]
|
|
table = create_findings_table(findings, columns=columns)
|
|
assert table is not None
|
|
|
|
def test_create_findings_table_empty(self):
|
|
"""Test findings table with empty list."""
|
|
table = create_findings_table([])
|
|
assert table is not None
|
|
|
|
|
|
class TestSectionHeader:
|
|
"""Tests for section header component."""
|
|
|
|
def test_create_section_header_with_spacer(self):
|
|
"""Test section header with spacer."""
|
|
styles = create_pdf_styles()
|
|
elements = create_section_header("Test Header", styles["h1"])
|
|
|
|
assert len(elements) == 2
|
|
assert isinstance(elements[0], Paragraph)
|
|
assert isinstance(elements[1], Spacer)
|
|
|
|
def test_create_section_header_without_spacer(self):
|
|
"""Test section header without spacer."""
|
|
styles = create_pdf_styles()
|
|
elements = create_section_header("Test Header", styles["h1"], add_spacer=False)
|
|
|
|
assert len(elements) == 1
|
|
assert isinstance(elements[0], Paragraph)
|
|
|
|
def test_create_section_header_custom_spacer_height(self):
|
|
"""Test section header with custom spacer height."""
|
|
styles = create_pdf_styles()
|
|
elements = create_section_header("Test Header", styles["h2"], spacer_height=0.5)
|
|
|
|
assert len(elements) == 2
|
|
|
|
|
|
# =============================================================================
|
|
# Chart Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestChartCreation:
|
|
"""Tests for chart creation functions."""
|
|
|
|
def test_create_vertical_bar_chart(self):
|
|
"""Test vertical bar chart creation."""
|
|
buffer = create_vertical_bar_chart(
|
|
labels=["A", "B", "C"],
|
|
values=[80, 60, 40],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
assert buffer.getvalue() # Not empty
|
|
|
|
def test_create_vertical_bar_chart_with_options(self):
|
|
"""Test vertical bar chart with custom options."""
|
|
buffer = create_vertical_bar_chart(
|
|
labels=["Section 1", "Section 2"],
|
|
values=[90, 70],
|
|
ylabel="Compliance",
|
|
title="Test Chart",
|
|
figsize=(8, 6),
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_horizontal_bar_chart(self):
|
|
"""Test horizontal bar chart creation."""
|
|
buffer = create_horizontal_bar_chart(
|
|
labels=["Category 1", "Category 2", "Category 3"],
|
|
values=[85, 65, 45],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
assert buffer.getvalue()
|
|
|
|
def test_create_horizontal_bar_chart_with_options(self):
|
|
"""Test horizontal bar chart with custom options."""
|
|
buffer = create_horizontal_bar_chart(
|
|
labels=["A", "B"],
|
|
values=[100, 50],
|
|
xlabel="Percentage",
|
|
title="Custom Chart",
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_radar_chart(self):
|
|
"""Test radar chart creation."""
|
|
buffer = create_radar_chart(
|
|
labels=["Dim 1", "Dim 2", "Dim 3", "Dim 4", "Dim 5"],
|
|
values=[80, 70, 60, 90, 75],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
assert buffer.getvalue()
|
|
|
|
def test_create_radar_chart_with_options(self):
|
|
"""Test radar chart with custom options."""
|
|
buffer = create_radar_chart(
|
|
labels=["A", "B", "C"],
|
|
values=[50, 60, 70],
|
|
color="#FF0000",
|
|
fill_alpha=0.5,
|
|
title="Custom Radar",
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_pie_chart(self):
|
|
"""Test pie chart creation."""
|
|
buffer = create_pie_chart(
|
|
labels=["Pass", "Fail"],
|
|
values=[80, 20],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
assert buffer.getvalue()
|
|
|
|
def test_create_pie_chart_with_options(self):
|
|
"""Test pie chart with custom options."""
|
|
buffer = create_pie_chart(
|
|
labels=["Pass", "Fail", "Manual"],
|
|
values=[60, 30, 10],
|
|
colors=["#4CAF50", "#F44336", "#9E9E9E"],
|
|
title="Status Distribution",
|
|
autopct="%1.0f%%",
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_stacked_bar_chart(self):
|
|
"""Test stacked bar chart creation."""
|
|
buffer = create_stacked_bar_chart(
|
|
labels=["Section 1", "Section 2", "Section 3"],
|
|
data_series={
|
|
"Pass": [8, 6, 4],
|
|
"Fail": [2, 4, 6],
|
|
},
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
assert buffer.getvalue()
|
|
|
|
def test_create_stacked_bar_chart_with_options(self):
|
|
"""Test stacked bar chart with custom options."""
|
|
buffer = create_stacked_bar_chart(
|
|
labels=["A", "B"],
|
|
data_series={
|
|
"Pass": [10, 5],
|
|
"Fail": [2, 3],
|
|
"Manual": [1, 2],
|
|
},
|
|
colors={
|
|
"Pass": "#4CAF50",
|
|
"Fail": "#F44336",
|
|
"Manual": "#9E9E9E",
|
|
},
|
|
xlabel="Categories",
|
|
ylabel="Requirements",
|
|
title="Requirements by Status",
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_stacked_bar_chart_without_legend(self):
|
|
"""Test stacked bar chart without legend."""
|
|
buffer = create_stacked_bar_chart(
|
|
labels=["X", "Y"],
|
|
data_series={"A": [1, 2]},
|
|
show_legend=False,
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_vertical_bar_chart_without_labels(self):
|
|
"""Test vertical bar chart without value labels."""
|
|
buffer = create_vertical_bar_chart(
|
|
labels=["A", "B"],
|
|
values=[50, 75],
|
|
show_labels=False,
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_vertical_bar_chart_with_explicit_colors(self):
|
|
"""Test vertical bar chart with explicit color list."""
|
|
buffer = create_vertical_bar_chart(
|
|
labels=["Pass", "Fail"],
|
|
values=[80, 20],
|
|
colors=["#4CAF50", "#F44336"],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_horizontal_bar_chart_auto_figsize(self):
|
|
"""Test horizontal bar chart auto-calculates figure size for many items."""
|
|
labels = [f"Item {i}" for i in range(20)]
|
|
values = [50 + i * 2 for i in range(20)]
|
|
buffer = create_horizontal_bar_chart(
|
|
labels=labels,
|
|
values=values,
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_horizontal_bar_chart_with_explicit_colors(self):
|
|
"""Test horizontal bar chart with explicit colors."""
|
|
buffer = create_horizontal_bar_chart(
|
|
labels=["A", "B", "C"],
|
|
values=[80, 60, 40],
|
|
colors=["#4CAF50", "#FFEB3B", "#F44336"],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_create_radar_chart_with_custom_ticks(self):
|
|
"""Test radar chart with custom y-axis ticks."""
|
|
buffer = create_radar_chart(
|
|
labels=["A", "B", "C", "D"],
|
|
values=[25, 50, 75, 100],
|
|
y_ticks=[0, 25, 50, 75, 100],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
|
|
# =============================================================================
|
|
# Data Class Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestDataClasses:
|
|
"""Tests for data classes."""
|
|
|
|
def test_requirement_data_creation(self):
|
|
"""Test RequirementData creation."""
|
|
req = RequirementData(
|
|
id="REQ-001",
|
|
description="Test requirement",
|
|
status="PASS",
|
|
passed_findings=10,
|
|
total_findings=10,
|
|
)
|
|
assert req.id == "REQ-001"
|
|
assert req.status == "PASS"
|
|
assert req.passed_findings == 10
|
|
|
|
def test_requirement_data_with_failed_findings(self):
|
|
"""Test RequirementData with failed findings."""
|
|
req = RequirementData(
|
|
id="REQ-002",
|
|
description="Failed requirement",
|
|
status="FAIL",
|
|
passed_findings=3,
|
|
failed_findings=7,
|
|
total_findings=10,
|
|
)
|
|
assert req.failed_findings == 7
|
|
assert req.total_findings == 10
|
|
|
|
def test_requirement_data_defaults(self):
|
|
"""Test RequirementData default values."""
|
|
req = RequirementData(
|
|
id="REQ-003",
|
|
description="Minimal requirement",
|
|
status="MANUAL",
|
|
)
|
|
assert req.passed_findings == 0
|
|
assert req.failed_findings == 0
|
|
assert req.total_findings == 0
|
|
|
|
def test_compliance_data_creation(self):
|
|
"""Test ComplianceData creation."""
|
|
data = ComplianceData(
|
|
tenant_id="tenant-123",
|
|
scan_id="scan-456",
|
|
provider_id="provider-789",
|
|
compliance_id="test_compliance",
|
|
framework="Test",
|
|
name="Test Compliance",
|
|
version="1.0",
|
|
description="Test description",
|
|
)
|
|
assert data.tenant_id == "tenant-123"
|
|
assert data.framework == "Test"
|
|
assert data.requirements == []
|
|
|
|
def test_compliance_data_with_requirements(self):
|
|
"""Test ComplianceData with requirements list."""
|
|
reqs = [
|
|
RequirementData(id="R1", description="Req 1", status="PASS"),
|
|
RequirementData(id="R2", description="Req 2", status="FAIL"),
|
|
]
|
|
data = ComplianceData(
|
|
tenant_id="t1",
|
|
scan_id="s1",
|
|
provider_id="p1",
|
|
compliance_id="c1",
|
|
framework="Test",
|
|
name="Test",
|
|
version="1.0",
|
|
description="",
|
|
requirements=reqs,
|
|
)
|
|
assert len(data.requirements) == 2
|
|
assert data.requirements[0].id == "R1"
|
|
|
|
def test_compliance_data_with_attributes(self):
|
|
"""Test ComplianceData with attributes dictionary."""
|
|
data = ComplianceData(
|
|
tenant_id="t1",
|
|
scan_id="s1",
|
|
provider_id="p1",
|
|
compliance_id="c1",
|
|
framework="Test",
|
|
name="Test",
|
|
version="1.0",
|
|
description="",
|
|
attributes_by_requirement_id={
|
|
"R1": {"attributes": {"key": "value"}},
|
|
},
|
|
)
|
|
assert "R1" in data.attributes_by_requirement_id
|
|
assert data.attributes_by_requirement_id["R1"]["attributes"]["key"] == "value"
|
|
|
|
|
|
# =============================================================================
|
|
# PDF Styles Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestPDFStyles:
|
|
"""Tests for PDF styles."""
|
|
|
|
def test_create_pdf_styles_returns_dict(self):
|
|
"""Test that create_pdf_styles returns a dictionary."""
|
|
styles = create_pdf_styles()
|
|
assert isinstance(styles, dict)
|
|
|
|
def test_create_pdf_styles_has_required_keys(self):
|
|
"""Test that styles dict has all required keys."""
|
|
styles = create_pdf_styles()
|
|
required_keys = ["title", "h1", "h2", "h3", "normal", "normal_center"]
|
|
for key in required_keys:
|
|
assert key in styles
|
|
|
|
def test_create_pdf_styles_caches_result(self):
|
|
"""Test that styles are cached."""
|
|
styles1 = create_pdf_styles()
|
|
styles2 = create_pdf_styles()
|
|
assert styles1 is styles2
|
|
|
|
|
|
# =============================================================================
|
|
# Base Generator Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestBaseComplianceReportGenerator:
|
|
"""Tests for BaseComplianceReportGenerator."""
|
|
|
|
def test_cannot_instantiate_directly(self):
|
|
"""Test that base class cannot be instantiated directly."""
|
|
config = FrameworkConfig(name="test", display_name="Test")
|
|
with pytest.raises(TypeError):
|
|
BaseComplianceReportGenerator(config)
|
|
|
|
def test_concrete_implementation(self):
|
|
"""Test that a concrete implementation can be created."""
|
|
|
|
class ConcreteGenerator(BaseComplianceReportGenerator):
|
|
def create_executive_summary(self, data):
|
|
return []
|
|
|
|
def create_charts_section(self, data):
|
|
return []
|
|
|
|
def create_requirements_index(self, data):
|
|
return []
|
|
|
|
config = FrameworkConfig(name="test", display_name="Test")
|
|
generator = ConcreteGenerator(config)
|
|
assert generator.config.name == "test"
|
|
assert generator.styles is not None
|
|
|
|
def test_get_footer_text_english(self):
|
|
"""Test footer text in English."""
|
|
|
|
class ConcreteGenerator(BaseComplianceReportGenerator):
|
|
def create_executive_summary(self, data):
|
|
return []
|
|
|
|
def create_charts_section(self, data):
|
|
return []
|
|
|
|
def create_requirements_index(self, data):
|
|
return []
|
|
|
|
config = FrameworkConfig(name="test", display_name="Test", language="en")
|
|
generator = ConcreteGenerator(config)
|
|
left, right = generator.get_footer_text(1)
|
|
assert left == "Page 1"
|
|
assert right == "Powered by Prowler"
|
|
|
|
def test_get_footer_text_spanish(self):
|
|
"""Test footer text in Spanish."""
|
|
|
|
class ConcreteGenerator(BaseComplianceReportGenerator):
|
|
def create_executive_summary(self, data):
|
|
return []
|
|
|
|
def create_charts_section(self, data):
|
|
return []
|
|
|
|
def create_requirements_index(self, data):
|
|
return []
|
|
|
|
config = FrameworkConfig(name="test", display_name="Test", language="es")
|
|
generator = ConcreteGenerator(config)
|
|
left, right = generator.get_footer_text(1)
|
|
assert left == "Página 1"
|
|
|
|
|
|
# =============================================================================
|
|
# Integration Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestExampleReportGenerator:
|
|
"""Integration tests using an example report generator."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures."""
|
|
|
|
class ExampleGenerator(BaseComplianceReportGenerator):
|
|
"""Example concrete implementation for testing."""
|
|
|
|
def create_executive_summary(self, data):
|
|
return [
|
|
Paragraph("Executive Summary", self.styles["h1"]),
|
|
Paragraph(
|
|
f"Total requirements: {len(data.requirements)}",
|
|
self.styles["normal"],
|
|
),
|
|
]
|
|
|
|
def create_charts_section(self, data):
|
|
chart_buffer = create_vertical_bar_chart(
|
|
labels=["Pass", "Fail"],
|
|
values=[80, 20],
|
|
)
|
|
return [Image(chart_buffer, width=6 * inch, height=4 * inch)]
|
|
|
|
def create_requirements_index(self, data):
|
|
elements = [Paragraph("Requirements Index", self.styles["h1"])]
|
|
for req in data.requirements:
|
|
elements.append(
|
|
Paragraph(
|
|
f"- {req.id}: {req.description}", self.styles["normal"]
|
|
)
|
|
)
|
|
return elements
|
|
|
|
self.generator_class = ExampleGenerator
|
|
|
|
def test_example_generator_creation(self):
|
|
"""Test creating example generator."""
|
|
config = FrameworkConfig(name="example", display_name="Example Framework")
|
|
generator = self.generator_class(config)
|
|
assert generator is not None
|
|
|
|
def test_example_generator_executive_summary(self):
|
|
"""Test executive summary generation."""
|
|
config = FrameworkConfig(name="example", display_name="Example Framework")
|
|
generator = self.generator_class(config)
|
|
|
|
data = ComplianceData(
|
|
tenant_id="t1",
|
|
scan_id="s1",
|
|
provider_id="p1",
|
|
compliance_id="c1",
|
|
framework="Test",
|
|
name="Test",
|
|
version="1.0",
|
|
description="",
|
|
requirements=[
|
|
RequirementData(id="R1", description="Req 1", status="PASS"),
|
|
RequirementData(id="R2", description="Req 2", status="FAIL"),
|
|
],
|
|
)
|
|
|
|
elements = generator.create_executive_summary(data)
|
|
assert len(elements) == 2
|
|
|
|
def test_example_generator_charts_section(self):
|
|
"""Test charts section generation."""
|
|
config = FrameworkConfig(name="example", display_name="Example Framework")
|
|
generator = self.generator_class(config)
|
|
|
|
data = ComplianceData(
|
|
tenant_id="t1",
|
|
scan_id="s1",
|
|
provider_id="p1",
|
|
compliance_id="c1",
|
|
framework="Test",
|
|
name="Test",
|
|
version="1.0",
|
|
description="",
|
|
)
|
|
|
|
elements = generator.create_charts_section(data)
|
|
assert len(elements) == 1
|
|
|
|
def test_example_generator_requirements_index(self):
|
|
"""Test requirements index generation."""
|
|
config = FrameworkConfig(name="example", display_name="Example Framework")
|
|
generator = self.generator_class(config)
|
|
|
|
data = ComplianceData(
|
|
tenant_id="t1",
|
|
scan_id="s1",
|
|
provider_id="p1",
|
|
compliance_id="c1",
|
|
framework="Test",
|
|
name="Test",
|
|
version="1.0",
|
|
description="",
|
|
requirements=[
|
|
RequirementData(id="R1", description="Requirement 1", status="PASS"),
|
|
],
|
|
)
|
|
|
|
elements = generator.create_requirements_index(data)
|
|
assert len(elements) == 2 # Header + 1 requirement
|
|
|
|
|
|
# =============================================================================
|
|
# Edge Case Tests
|
|
# =============================================================================
|
|
|
|
|
|
class TestChartEdgeCases:
|
|
"""Tests for chart edge cases."""
|
|
|
|
def test_vertical_bar_chart_empty_data(self):
|
|
"""Test vertical bar chart with empty data."""
|
|
buffer = create_vertical_bar_chart(labels=[], values=[])
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_vertical_bar_chart_single_item(self):
|
|
"""Test vertical bar chart with single item."""
|
|
buffer = create_vertical_bar_chart(labels=["Single"], values=[75.0])
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_horizontal_bar_chart_empty_data(self):
|
|
"""Test horizontal bar chart with empty data."""
|
|
buffer = create_horizontal_bar_chart(labels=[], values=[])
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_horizontal_bar_chart_single_item(self):
|
|
"""Test horizontal bar chart with single item."""
|
|
buffer = create_horizontal_bar_chart(labels=["Single"], values=[50.0])
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_radar_chart_minimum_points(self):
|
|
"""Test radar chart with minimum number of points (3)."""
|
|
buffer = create_radar_chart(
|
|
labels=["A", "B", "C"],
|
|
values=[30.0, 60.0, 90.0],
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_pie_chart_single_slice(self):
|
|
"""Test pie chart with single slice."""
|
|
buffer = create_pie_chart(labels=["Only"], values=[100.0])
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_pie_chart_many_slices(self):
|
|
"""Test pie chart with many slices."""
|
|
labels = [f"Item {i}" for i in range(10)]
|
|
values = [10.0] * 10
|
|
buffer = create_pie_chart(labels=labels, values=values)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_stacked_bar_chart_single_series(self):
|
|
"""Test stacked bar chart with single series."""
|
|
buffer = create_stacked_bar_chart(
|
|
labels=["A", "B"],
|
|
data_series={"Only": [10.0, 20.0]},
|
|
)
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
def test_stacked_bar_chart_empty_data(self):
|
|
"""Test stacked bar chart with empty data."""
|
|
buffer = create_stacked_bar_chart(labels=[], data_series={})
|
|
assert isinstance(buffer, io.BytesIO)
|
|
|
|
|
|
class TestComponentEdgeCases:
|
|
"""Tests for component edge cases."""
|
|
|
|
def test_create_badge_empty_text(self):
|
|
"""Test badge with empty text."""
|
|
badge = create_badge("", COLOR_BLUE)
|
|
assert badge is not None
|
|
|
|
def test_create_badge_long_text(self):
|
|
"""Test badge with very long text."""
|
|
long_text = "A" * 100
|
|
badge = create_badge(long_text, COLOR_BLUE, width=5 * inch)
|
|
assert badge is not None
|
|
|
|
def test_create_status_badge_unknown_status(self):
|
|
"""Test status badge with unknown status."""
|
|
badge = create_status_badge("UNKNOWN")
|
|
assert badge is not None
|
|
|
|
def test_create_multi_badge_row_single_badge(self):
|
|
"""Test multi-badge row with single badge."""
|
|
badges = [("A", COLOR_BLUE)]
|
|
table = create_multi_badge_row(badges)
|
|
assert table is not None
|
|
|
|
def test_create_multi_badge_row_many_badges(self):
|
|
"""Test multi-badge row with many badges."""
|
|
badges = [(chr(65 + i), COLOR_BLUE) for i in range(10)] # A-J
|
|
table = create_multi_badge_row(badges)
|
|
assert table is not None
|
|
|
|
def test_create_info_table_empty(self):
|
|
"""Test info table with empty rows."""
|
|
table = create_info_table([])
|
|
assert isinstance(table, Table)
|
|
|
|
def test_create_info_table_long_values(self):
|
|
"""Test info table with very long values wraps properly."""
|
|
rows = [
|
|
("Key:", "A" * 200), # Very long value
|
|
]
|
|
styles = create_pdf_styles()
|
|
table = create_info_table(rows, normal_style=styles["normal"])
|
|
assert table is not None
|
|
|
|
def test_create_data_table_empty(self):
|
|
"""Test data table with empty data."""
|
|
columns = [
|
|
ColumnConfig("Name", 2 * inch, "name"),
|
|
]
|
|
table = create_data_table([], columns)
|
|
assert table is not None
|
|
|
|
def test_create_data_table_large_dataset(self):
|
|
"""Test data table with large dataset uses LongTable."""
|
|
# Create more than 50 rows to trigger LongTable
|
|
data = [{"name": f"Item {i}"} for i in range(60)]
|
|
columns = [ColumnConfig("Name", 2 * inch, "name")]
|
|
table = create_data_table(data, columns)
|
|
# Should be a LongTable for large datasets
|
|
assert isinstance(table, LongTable)
|
|
|
|
def test_create_risk_component_zero_values(self):
|
|
"""Test risk component with zero values."""
|
|
component = create_risk_component(risk_level=0, weight=0, score=0)
|
|
assert component is not None
|
|
|
|
def test_create_risk_component_max_values(self):
|
|
"""Test risk component with maximum values."""
|
|
component = create_risk_component(risk_level=5, weight=200, score=1000)
|
|
assert component is not None
|
|
|
|
|
|
class TestColorEdgeCases:
|
|
"""Tests for color function edge cases."""
|
|
|
|
def test_get_color_for_compliance_boundary_80(self):
|
|
"""Test compliance color at exactly 80%."""
|
|
assert get_color_for_compliance(80) == COLOR_SAFE
|
|
|
|
def test_get_color_for_compliance_boundary_60(self):
|
|
"""Test compliance color at exactly 60%."""
|
|
assert get_color_for_compliance(60) == COLOR_LOW_RISK
|
|
|
|
def test_get_color_for_compliance_over_100(self):
|
|
"""Test compliance color for values over 100."""
|
|
assert get_color_for_compliance(150) == COLOR_SAFE
|
|
|
|
def test_get_color_for_weight_boundary_100(self):
|
|
"""Test weight color at exactly 100."""
|
|
assert get_color_for_weight(100) == COLOR_LOW_RISK
|
|
|
|
def test_get_color_for_weight_boundary_50(self):
|
|
"""Test weight color at exactly 50."""
|
|
assert get_color_for_weight(50) == COLOR_SAFE
|
|
|
|
def test_get_status_color_case_insensitive(self):
|
|
"""Test that status color is case insensitive."""
|
|
assert get_status_color("PASS") == get_status_color("pass")
|
|
assert get_status_color("FAIL") == get_status_color("Fail")
|
|
assert get_status_color("MANUAL") == get_status_color("manual")
|
|
|
|
|
|
class TestFrameworkConfigEdgeCases:
|
|
"""Tests for FrameworkConfig edge cases."""
|
|
|
|
def test_framework_config_empty_sections(self):
|
|
"""Test FrameworkConfig with empty sections list."""
|
|
config = FrameworkConfig(
|
|
name="test",
|
|
display_name="Test",
|
|
sections=[],
|
|
)
|
|
assert config.sections == []
|
|
|
|
def test_framework_config_empty_attribute_fields(self):
|
|
"""Test FrameworkConfig with empty attribute fields."""
|
|
config = FrameworkConfig(
|
|
name="test",
|
|
display_name="Test",
|
|
attribute_fields=[],
|
|
)
|
|
assert config.attribute_fields == []
|
|
|
|
def test_get_framework_config_case_variations(self):
|
|
"""Test get_framework_config with different case variations."""
|
|
# Test case insensitivity
|
|
assert get_framework_config("PROWLER_THREATSCORE_AWS") is not None
|
|
assert get_framework_config("ENS_RD2022_AWS") is not None
|
|
assert get_framework_config("NIS2_AWS") is not None
|
|
|
|
def test_get_framework_config_partial_match(self):
|
|
"""Test that partial matches work correctly."""
|
|
# Should match based on substring
|
|
assert get_framework_config("my_custom_threatscore_compliance") is not None
|
|
assert get_framework_config("ens_something_else") is not None
|
|
assert get_framework_config("nis2_gcp") is not None
|