test_rules_engine.py
1 """ 2 Unit tests for rules engine execution. 3 4 Tests rule loading, execution, return types, and error handling for 5 representative deterministic and probabilistic rules. 6 """ 7 8 import importlib 9 10 import pytest 11 12 13 @pytest.mark.unit 14 def test_rule_d_schema_03_accepts_valid_event(sample_torrent_event): 15 """D-SCHEMA-03: Valid infohash should pass.""" 16 rule = importlib.import_module("rules.D-SCHEMA-03") 17 result = rule.main(sample_torrent_event) 18 19 assert isinstance(result, dict) 20 assert "passed" in result 21 assert result["passed"] is True 22 23 24 @pytest.mark.unit 25 def test_rule_d_schema_03_rejects_missing_infohash(sample_invalid_event): 26 """D-SCHEMA-03: Missing 'x' tag (infohash) should fail.""" 27 rule = importlib.import_module("rules.D-SCHEMA-03") 28 result = rule.main(sample_invalid_event) 29 30 assert isinstance(result, dict) 31 assert "passed" in result 32 assert result["passed"] is False 33 34 35 @pytest.mark.unit 36 def test_rule_d_schema_03_rejects_invalid_infohash_format(make_event): 37 """D-SCHEMA-03: Invalid infohash format should fail.""" 38 event = make_event(tags=[ 39 ["title", "Test"], 40 ["x", "invalid-hash"], # Not 40-char hex or 32-char base32 41 ]) 42 rule = importlib.import_module("rules.D-SCHEMA-03") 43 result = rule.main(event) 44 45 assert result["passed"] is False 46 47 48 @pytest.mark.unit 49 def test_rule_d_schema_03_returns_binary_result(sample_torrent_event): 50 """D-SCHEMA-03: Must return BinaryRuleResult (no score field).""" 51 rule = importlib.import_module("rules.D-SCHEMA-03") 52 result = rule.main(sample_torrent_event) 53 54 assert "passed" in result 55 assert "score" not in result # Deterministic rules don't have scores 56 57 58 @pytest.mark.unit 59 def test_rule_p_quality_01_returns_probabilistic_result(sample_torrent_event): 60 """P-QUALITY-01: Must return ProbabilityRuleResult with score.""" 61 rule = importlib.import_module("rules.P-QUALITY-01") 62 result = rule.main(sample_torrent_event) 63 64 assert isinstance(result, dict) 65 assert "passed" in result 66 assert "score" in result 67 assert isinstance(result["score"], (int, float)) 68 assert 0.0 <= result["score"] <= 1.0 69 70 71 @pytest.mark.unit 72 def test_rule_p_quality_01_accepts_valid_event(sample_torrent_event): 73 """P-QUALITY-01: Valid event should pass with neutral score.""" 74 rule = importlib.import_module("rules.P-QUALITY-01") 75 result = rule.main(sample_torrent_event) 76 77 assert result["passed"] is True 78 # Current placeholder implementation returns 0.5 79 assert result["score"] == 0.5 80 81 82 @pytest.mark.unit 83 @pytest.mark.parametrize("event_fixture,expected_type", [ 84 ("sample_torrent_event", dict), 85 ("sample_invalid_event", dict), 86 ("sample_large_event", dict), 87 ]) 88 def test_rules_return_dict_type(event_fixture, expected_type, request): 89 """All rules must return dict results.""" 90 event = request.getfixturevalue(event_fixture) 91 92 # Test both deterministic and probabilistic rules 93 for rule_name in ["D-SCHEMA-03", "P-QUALITY-01"]: 94 rule = importlib.import_module(f"rules.{rule_name}") 95 result = rule.main(event) 96 assert isinstance(result, expected_type) 97 assert "passed" in result 98 99 100 @pytest.mark.unit 101 def test_rule_handles_malformed_input_gracefully(): 102 """Rules should handle malformed input without crashing.""" 103 rule = importlib.import_module("rules.D-SCHEMA-03") 104 105 # Test with missing 'tags' field 106 malformed_event = { 107 "id": "0" * 64, 108 "pubkey": "1" * 64, 109 "kind": 2003, 110 # Missing 'tags' field 111 } 112 113 # Should not raise exception, should return failure result 114 try: 115 result = rule.main(malformed_event) 116 assert isinstance(result, dict) 117 assert "passed" in result 118 except Exception as e: 119 pytest.fail(f"Rule should handle malformed input gracefully, but raised: {e}") 120 121 122 @pytest.mark.unit 123 def test_rule_handles_empty_tags_list(): 124 """Rules should handle events with empty tags list.""" 125 rule = importlib.import_module("rules.D-SCHEMA-03") 126 127 event = { 128 "id": "0" * 64, 129 "pubkey": "1" * 64, 130 "created_at": 1234567890, 131 "kind": 2003, 132 "tags": [], # Empty tags 133 "content": "", 134 "sig": "2" * 128, 135 } 136 137 result = rule.main(event) 138 assert isinstance(result, dict) 139 assert result["passed"] is False # No infohash = fail 140 141 142 @pytest.mark.unit 143 def test_rule_handles_malformed_tags(): 144 """Rules should handle malformed tags (non-list items).""" 145 rule = importlib.import_module("rules.D-SCHEMA-03") 146 147 event = { 148 "id": "0" * 64, 149 "pubkey": "1" * 64, 150 "created_at": 1234567890, 151 "kind": 2003, 152 "tags": [ 153 "not-a-list", # Should be a list, not a string 154 ["x", "abc123def456abc123def456abc123def4567890"], 155 ], 156 "content": "", 157 "sig": "2" * 128, 158 } 159 160 try: 161 result = rule.main(event) 162 assert isinstance(result, dict) 163 except Exception as e: 164 pytest.fail(f"Rule should handle malformed tags gracefully, but raised: {e}") 165 166 167 @pytest.mark.unit 168 def test_multiple_rules_same_event(sample_torrent_event): 169 """Test that multiple rules can process the same event independently.""" 170 rules = ["D-SCHEMA-03", "P-QUALITY-01"] 171 results = {} 172 173 for rule_name in rules: 174 rule = importlib.import_module(f"rules.{rule_name}") 175 results[rule_name] = rule.main(sample_torrent_event) 176 177 # All rules should return results 178 assert len(results) == len(rules) 179 180 # D-SCHEMA-03 should be deterministic (no score) 181 assert "score" not in results["D-SCHEMA-03"] 182 assert results["D-SCHEMA-03"]["passed"] is True 183 184 # P-QUALITY-01 should be probabilistic (has score) 185 assert "score" in results["P-QUALITY-01"] 186 assert results["P-QUALITY-01"]["passed"] is True 187 188 189 @pytest.mark.unit 190 def test_rule_execution_is_idempotent(sample_torrent_event): 191 """Rule execution should be idempotent (same input = same output).""" 192 rule = importlib.import_module("rules.D-SCHEMA-03") 193 194 result1 = rule.main(sample_torrent_event) 195 result2 = rule.main(sample_torrent_event) 196 197 assert result1 == result2 198 199 200 @pytest.mark.unit 201 def test_rule_does_not_modify_input_event(sample_torrent_event): 202 """Rules should not modify the input event.""" 203 import copy 204 rule = importlib.import_module("rules.D-SCHEMA-03") 205 206 event_before = copy.deepcopy(sample_torrent_event) 207 rule.main(sample_torrent_event) 208 event_after = sample_torrent_event 209 210 assert event_before == event_after