/ tests / unit / test_custom_types.py
test_custom_types.py
  1  """
  2  Unit tests for custom_types/ Pydantic models and simple_types.
  3  """
  4  
  5  import pytest
  6  from pydantic import ValidationError
  7  
  8  from custom_types.NIP_35 import Nip35Kind2003Event
  9  from custom_types.rules_rulesets import Rule, RuleSet
 10  from rules.simple_types import BinaryRuleResult, ProbabilityRuleResult
 11  
 12  # ==============================
 13  # Tests for Nip35Kind2003Event
 14  # ==============================
 15  
 16  @pytest.mark.unit
 17  def test_nip35_valid_event(valid_nip35_event):
 18      """Test Nip35Kind2003Event validates a correct event."""
 19      event = Nip35Kind2003Event(**valid_nip35_event)
 20      assert event.kind == 2003
 21      assert event.id == "a" * 64
 22      assert event.pubkey == "b" * 64
 23  
 24  
 25  @pytest.mark.unit
 26  def test_nip35_missing_title():
 27      """Test Nip35Kind2003Event rejects event without title tag."""
 28      event_data = {
 29          "id": "a" * 64,
 30          "pubkey": "b" * 64,
 31          "created_at": 1234567890,
 32          "kind": 2003,
 33          "tags": [
 34              ["x", "c" * 40],
 35              ["file", "example.mkv", "1073741824"],
 36          ],
 37          "content": "Missing title",
 38          "sig": "d" * 128,
 39      }
 40      with pytest.raises(ValidationError, match="missing required tag.*title"):
 41          Nip35Kind2003Event(**event_data)
 42  
 43  
 44  @pytest.mark.unit
 45  def test_nip35_missing_infohash():
 46      """Test Nip35Kind2003Event rejects event without 'x' tag."""
 47      event_data = {
 48          "id": "a" * 64,
 49          "pubkey": "b" * 64,
 50          "created_at": 1234567890,
 51          "kind": 2003,
 52          "tags": [
 53              ["title", "Example"],
 54              ["file", "example.mkv", "1073741824"],
 55          ],
 56          "content": "Missing infohash",
 57          "sig": "d" * 128,
 58      }
 59      with pytest.raises(ValidationError, match="missing required tag.*x"):
 60          Nip35Kind2003Event(**event_data)
 61  
 62  
 63  @pytest.mark.unit
 64  def test_nip35_missing_file():
 65      """Test Nip35Kind2003Event rejects event without file tag."""
 66      event_data = {
 67          "id": "a" * 64,
 68          "pubkey": "b" * 64,
 69          "created_at": 1234567890,
 70          "kind": 2003,
 71          "tags": [
 72              ["title", "Example"],
 73              ["x", "c" * 40],
 74          ],
 75          "content": "Missing file",
 76          "sig": "d" * 128,
 77      }
 78      with pytest.raises(ValidationError, match="missing required tag.*file"):
 79          Nip35Kind2003Event(**event_data)
 80  
 81  
 82  @pytest.mark.unit
 83  def test_nip35_invalid_infohash():
 84      """Test Nip35Kind2003Event rejects event with invalid infohash."""
 85      event_data = {
 86          "id": "a" * 64,
 87          "pubkey": "b" * 64,
 88          "created_at": 1234567890,
 89          "kind": 2003,
 90          "tags": [
 91              ["title", "Example"],
 92              ["x", "invalid"],  # Not 40-char hex or 32-char base32
 93              ["file", "example.mkv", "1073741824"],
 94          ],
 95          "content": "Invalid infohash",
 96          "sig": "d" * 128,
 97      }
 98      with pytest.raises(ValidationError, match="btih v1"):
 99          Nip35Kind2003Event(**event_data)
100  
101  
102  @pytest.mark.unit
103  def test_nip35_wrong_kind():
104      """Test Nip35Kind2003Event rejects event with wrong kind."""
105      event_data = {
106          "id": "a" * 64,
107          "pubkey": "b" * 64,
108          "created_at": 1234567890,
109          "kind": 1,  # Wrong kind
110          "tags": [
111              ["title", "Example"],
112              ["x", "c" * 40],
113              ["file", "example.mkv", "1073741824"],
114          ],
115          "content": "Wrong kind",
116          "sig": "d" * 128,
117      }
118      with pytest.raises(ValidationError):
119          Nip35Kind2003Event(**event_data)
120  
121  
122  @pytest.mark.unit
123  def test_nip35_invalid_id_length():
124      """Test Nip35Kind2003Event rejects invalid id (not 64-char hex)."""
125      event_data = {
126          "id": "abc123",  # Too short
127          "pubkey": "b" * 64,
128          "created_at": 1234567890,
129          "kind": 2003,
130          "tags": [
131              ["title", "Example"],
132              ["x", "c" * 40],
133              ["file", "example.mkv", "1073741824"],
134          ],
135          "content": "Invalid id",
136          "sig": "d" * 128,
137      }
138      with pytest.raises(ValidationError, match="id must be 32-byte hex"):
139          Nip35Kind2003Event(**event_data)
140  
141  
142  @pytest.mark.unit
143  def test_nip35_invalid_file_size():
144      """Test Nip35Kind2003Event rejects non-numeric file size."""
145      event_data = {
146          "id": "a" * 64,
147          "pubkey": "b" * 64,
148          "created_at": 1234567890,
149          "kind": 2003,
150          "tags": [
151              ["title", "Example"],
152              ["x", "c" * 40],
153              ["file", "example.mkv", "not-a-number"],
154          ],
155          "content": "Invalid file size",
156          "sig": "d" * 128,
157      }
158      with pytest.raises(ValidationError, match="file size must be a decimal string"):
159          Nip35Kind2003Event(**event_data)
160  
161  
162  @pytest.mark.unit
163  def test_nip35_valid_tracker():
164      """Test Nip35Kind2003Event accepts valid tracker tag."""
165      event_data = {
166          "id": "a" * 64,
167          "pubkey": "b" * 64,
168          "created_at": 1234567890,
169          "kind": 2003,
170          "tags": [
171              ["title", "Example"],
172              ["x", "c" * 40],
173              ["file", "example.mkv", "1073741824"],
174              ["tracker", "udp://tracker.example.com:6969/announce"],
175          ],
176          "content": "With tracker",
177          "sig": "d" * 128,
178      }
179      event = Nip35Kind2003Event(**event_data)
180      assert any(tag[0] == "tracker" for tag in event.tags)
181  
182  
183  # ==============================
184  # Tests for Rule and RuleSet
185  # ==============================
186  
187  @pytest.mark.unit
188  def test_rule_model():
189      """Test Rule Pydantic model validation."""
190      rule = Rule(
191          id="D-SCHEMA-03",
192          type="semantic",
193          output="binary",
194          requirements=["pyyaml>=6.0"],
195          name="Schema validation",
196          description="Validates event schema",
197          spec_ref="8.1.1"
198      )
199      assert rule.id == "D-SCHEMA-03"
200      assert rule.type == "semantic"
201      assert rule.output == "binary"
202  
203  
204  @pytest.mark.unit
205  def test_rule_forbids_extra_fields():
206      """Test Rule model rejects extra fields."""
207      with pytest.raises(ValidationError):
208          Rule(
209              id="D-TEST-01",
210              type="semantic",
211              output="binary",
212              requirements=[],
213              name="Test",
214              description="Test rule",
215              spec_ref="1.0",
216              extra_field="not allowed"
217          )
218  
219  
220  @pytest.mark.unit
221  def test_ruleset_model():
222      """Test RuleSet Pydantic model validation."""
223      ruleset = RuleSet(
224          ruleset_id="test-ruleset",
225          version="1.0.0",
226          ruleset_type="semantic",
227          source_spec="NIP-35",
228          name="Test Ruleset",
229          description="Test ruleset",
230          rules=[
231              Rule(
232                  id="D-TEST-01",
233                  type="semantic",
234                  output="binary",
235                  requirements=[],
236                  name="Test",
237                  description="Test rule",
238                  spec_ref="1.0"
239              )
240          ]
241      )
242      assert ruleset.ruleset_id == "test-ruleset"
243      assert len(ruleset.rules) == 1
244  
245  
246  # ==============================
247  # Tests for simple_types
248  # ==============================
249  
250  @pytest.mark.unit
251  def test_binary_rule_result_type():
252      """Test BinaryRuleResult TypedDict structure."""
253      result: BinaryRuleResult = {"passed": True}
254      assert "passed" in result
255      assert isinstance(result["passed"], bool)
256  
257  
258  @pytest.mark.unit
259  def test_probability_rule_result_type():
260      """Test ProbabilityRuleResult TypedDict structure."""
261      result: ProbabilityRuleResult = {"score": 0.75}
262      assert "score" in result
263      assert isinstance(result["score"], float)