diff --git a/.codacy.yml b/.codacy.yml new file mode 100644 index 00000000..151570f8 --- /dev/null +++ b/.codacy.yml @@ -0,0 +1,8 @@ +--- +exclude_paths: + - "**/*.pyc" + - "**/__pycache__/**" + - ".git/**" + - ".github/**" + - "uv.lock" + - "results.sarif" diff --git a/main.py b/main.py index 7e758616..0f6bc3a8 100644 --- a/main.py +++ b/main.py @@ -1131,19 +1131,26 @@ def validate_folder_data(data: Dict[str, Any], url: str) -> bool: ) return False if "rules" in rg: - if not isinstance (rg["rules"], list): - log. error ( - f"Invalid data from {sanitize_for_log(url)} : rule_groups[fil].rules must be a list." + rules = rg["rules"] + if not isinstance(rules, list): + log.error( + f"Invalid data from {sanitize_for_log(url)}: rule_groups[{i}].rules must be a list." ) return False -# Ensure each rule within the group is an object (dict), -# because later code treats each rule as a mapping (e.g., rule.get(...)). -for j, rule in enumerate (rgi"rules"1): -if not isinstance (rule, dict): - log. error ( - f"Invalid data from {sanitize_for_log(u rl)}: rule_groups[fiłl.rules[kił] must be an object." - ) - return False + + # Ensure each rule within the group is an object (dict). + # Optimization: Iterate directly to avoid enumerate() overhead in the happy path (99.9% of cases). + for rule in rules: + if not isinstance(rule, dict): + # Slow path: Re-iterate to find the index for logging + for j, bad_rule in enumerate(rules): + if not isinstance(bad_rule, dict): + log.error( + f"Invalid data from {sanitize_for_log(url)}: rule_groups[{i}].rules[{j}] must be an object." + ) + return False + return False + return True # Lock to protect updates to _api_stats in multi-threaded contexts. # Without this, concurrent increments can lose updates because `+=` is not atomic. diff --git a/tests/test_rule_validation.py b/tests/test_rule_validation.py new file mode 100644 index 00000000..1fee79b1 --- /dev/null +++ b/tests/test_rule_validation.py @@ -0,0 +1,49 @@ +from unittest.mock import MagicMock +import pytest +import main + +def test_rule_validation_structure(): + """Verify that rule validation handles structural errors correctly.""" + mock_log = MagicMock() + original_log = main.log + main.log = mock_log + + try: + # Case 1: Valid structure + valid_data = { + "group": {"group": "Valid"}, + "rule_groups": [ + { + "rules": [{"pk": "rule1"}, {"pk": "rule2"}] + } + ] + } + assert main.validate_folder_data(valid_data, "http://valid.com") is True + + # Case 2: rules is not a list + invalid_rules_type = { + "group": {"group": "Invalid Rules Type"}, + "rule_groups": [ + { + "rules": "not-a-list" + } + ] + } + assert main.validate_folder_data(invalid_rules_type, "http://invalid.com") is False + mock_log.error.assert_called_with("Invalid data from http://invalid.com: rule_groups[0].rules must be a list.") + + # Case 3: rule item is not a dict + invalid_rule_item = { + "group": {"group": "Invalid Rule Item"}, + "rule_groups": [ + { + "rules": [{"pk": "rule1"}, "not-a-dict", {"pk": "rule3"}] + } + ] + } + assert main.validate_folder_data(invalid_rule_item, "http://invalid-item.com") is False + # Verify that the log message contains the correct index (1) + mock_log.error.assert_called_with("Invalid data from http://invalid-item.com: rule_groups[0].rules[1] must be an object.") + + finally: + main.log = original_log