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)