/ tests / unit / test_rules_engine.py
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