Manage Atom feeds in a persistent git repository
at main 15 kB view raw
1"""Tests for pydantic models.""" 2 3from datetime import datetime 4 5import pytest 6from pydantic import HttpUrl, ValidationError 7 8from thicket.models import ( 9 AtomEntry, 10 DuplicateMap, 11 FeedMetadata, 12 ThicketConfig, 13 UserConfig, 14 UserMetadata, 15 ZulipAssociation, 16) 17 18 19class TestUserConfig: 20 """Test UserConfig model.""" 21 22 def test_valid_user_config(self): 23 """Test creating valid user config.""" 24 config = UserConfig( 25 username="testuser", 26 feeds=["https://example.com/feed.xml"], 27 email="test@example.com", 28 homepage="https://example.com", 29 display_name="Test User", 30 ) 31 32 assert config.username == "testuser" 33 assert len(config.feeds) == 1 34 assert config.feeds[0] == HttpUrl("https://example.com/feed.xml") 35 assert config.email == "test@example.com" 36 assert config.display_name == "Test User" 37 38 def test_invalid_email(self): 39 """Test validation of invalid email.""" 40 with pytest.raises(ValidationError): 41 UserConfig( 42 username="testuser", 43 feeds=["https://example.com/feed.xml"], 44 email="invalid-email", 45 ) 46 47 def test_invalid_feed_url(self): 48 """Test validation of invalid feed URL.""" 49 with pytest.raises(ValidationError): 50 UserConfig( 51 username="testuser", 52 feeds=["not-a-url"], 53 ) 54 55 def test_optional_fields(self): 56 """Test optional fields with None values.""" 57 config = UserConfig( 58 username="testuser", 59 feeds=["https://example.com/feed.xml"], 60 ) 61 62 assert config.email is None 63 assert config.homepage is None 64 assert config.icon is None 65 assert config.display_name is None 66 67 68class TestThicketConfig: 69 """Test ThicketConfig model.""" 70 71 def test_valid_config(self, temp_dir): 72 """Test creating valid configuration.""" 73 config = ThicketConfig( 74 git_store=temp_dir / "git_store", 75 cache_dir=temp_dir / "cache", 76 users=[ 77 UserConfig( 78 username="testuser", 79 feeds=["https://example.com/feed.xml"], 80 ) 81 ], 82 ) 83 84 assert config.git_store == temp_dir / "git_store" 85 assert config.cache_dir == temp_dir / "cache" 86 assert len(config.users) == 1 87 assert config.users[0].username == "testuser" 88 89 def test_find_user(self, temp_dir): 90 """Test finding user by username.""" 91 config = ThicketConfig( 92 git_store=temp_dir / "git_store", 93 cache_dir=temp_dir / "cache", 94 users=[ 95 UserConfig(username="user1", feeds=["https://example.com/feed1.xml"]), 96 UserConfig(username="user2", feeds=["https://example.com/feed2.xml"]), 97 ], 98 ) 99 100 user = config.find_user("user1") 101 assert user is not None 102 assert user.username == "user1" 103 104 non_user = config.find_user("nonexistent") 105 assert non_user is None 106 107 def test_add_user(self, temp_dir): 108 """Test adding a new user.""" 109 config = ThicketConfig( 110 git_store=temp_dir / "git_store", 111 cache_dir=temp_dir / "cache", 112 users=[], 113 ) 114 115 new_user = UserConfig( 116 username="newuser", 117 feeds=["https://example.com/feed.xml"], 118 ) 119 120 config.add_user(new_user) 121 assert len(config.users) == 1 122 assert config.users[0].username == "newuser" 123 124 def test_add_feed_to_user(self, temp_dir): 125 """Test adding feed to existing user.""" 126 config = ThicketConfig( 127 git_store=temp_dir / "git_store", 128 cache_dir=temp_dir / "cache", 129 users=[ 130 UserConfig( 131 username="testuser", feeds=["https://example.com/feed1.xml"] 132 ), 133 ], 134 ) 135 136 result = config.add_feed_to_user( 137 "testuser", HttpUrl("https://example.com/feed2.xml") 138 ) 139 assert result is True 140 141 user = config.find_user("testuser") 142 assert len(user.feeds) == 2 143 assert HttpUrl("https://example.com/feed2.xml") in user.feeds 144 145 # Test adding to non-existent user 146 result = config.add_feed_to_user( 147 "nonexistent", HttpUrl("https://example.com/feed.xml") 148 ) 149 assert result is False 150 151 152class TestAtomEntry: 153 """Test AtomEntry model.""" 154 155 def test_valid_entry(self): 156 """Test creating valid Atom entry.""" 157 entry = AtomEntry( 158 id="https://example.com/entry/1", 159 title="Test Entry", 160 link=HttpUrl("https://example.com/entry/1"), 161 updated=datetime.now(), 162 published=datetime.now(), 163 summary="Test summary", 164 content="<p>Test content</p>", 165 content_type="html", 166 author={"name": "Test Author"}, 167 categories=["test", "example"], 168 ) 169 170 assert entry.id == "https://example.com/entry/1" 171 assert entry.title == "Test Entry" 172 assert entry.summary == "Test summary" 173 assert entry.content == "<p>Test content</p>" 174 assert entry.content_type == "html" 175 assert entry.author["name"] == "Test Author" 176 assert "test" in entry.categories 177 178 def test_minimal_entry(self): 179 """Test creating minimal Atom entry.""" 180 entry = AtomEntry( 181 id="https://example.com/entry/1", 182 title="Test Entry", 183 link=HttpUrl("https://example.com/entry/1"), 184 updated=datetime.now(), 185 ) 186 187 assert entry.id == "https://example.com/entry/1" 188 assert entry.title == "Test Entry" 189 assert entry.published is None 190 assert entry.summary is None 191 assert entry.content is None 192 assert entry.content_type == "html" # default 193 assert entry.author is None 194 assert entry.categories == [] 195 196 197class TestDuplicateMap: 198 """Test DuplicateMap model.""" 199 200 def test_empty_duplicates(self): 201 """Test empty duplicate map.""" 202 dup_map = DuplicateMap() 203 assert len(dup_map.duplicates) == 0 204 assert not dup_map.is_duplicate("test") 205 assert dup_map.get_canonical("test") == "test" 206 207 def test_add_duplicate(self): 208 """Test adding duplicate mapping.""" 209 dup_map = DuplicateMap() 210 dup_map.add_duplicate("dup1", "canonical1") 211 212 assert len(dup_map.duplicates) == 1 213 assert dup_map.is_duplicate("dup1") 214 assert dup_map.get_canonical("dup1") == "canonical1" 215 assert dup_map.get_canonical("canonical1") == "canonical1" 216 217 def test_remove_duplicate(self): 218 """Test removing duplicate mapping.""" 219 dup_map = DuplicateMap() 220 dup_map.add_duplicate("dup1", "canonical1") 221 222 result = dup_map.remove_duplicate("dup1") 223 assert result is True 224 assert len(dup_map.duplicates) == 0 225 assert not dup_map.is_duplicate("dup1") 226 227 # Test removing non-existent duplicate 228 result = dup_map.remove_duplicate("nonexistent") 229 assert result is False 230 231 def test_get_duplicates_for_canonical(self): 232 """Test getting all duplicates for a canonical ID.""" 233 dup_map = DuplicateMap() 234 dup_map.add_duplicate("dup1", "canonical1") 235 dup_map.add_duplicate("dup2", "canonical1") 236 dup_map.add_duplicate("dup3", "canonical2") 237 238 dups = dup_map.get_duplicates_for_canonical("canonical1") 239 assert len(dups) == 2 240 assert "dup1" in dups 241 assert "dup2" in dups 242 243 dups = dup_map.get_duplicates_for_canonical("canonical2") 244 assert len(dups) == 1 245 assert "dup3" in dups 246 247 dups = dup_map.get_duplicates_for_canonical("nonexistent") 248 assert len(dups) == 0 249 250 251class TestFeedMetadata: 252 """Test FeedMetadata model.""" 253 254 def test_valid_metadata(self): 255 """Test creating valid feed metadata.""" 256 metadata = FeedMetadata( 257 title="Test Feed", 258 author_name="Test Author", 259 author_email="author@example.com", 260 author_uri=HttpUrl("https://example.com/author"), 261 link=HttpUrl("https://example.com"), 262 description="Test description", 263 ) 264 265 assert metadata.title == "Test Feed" 266 assert metadata.author_name == "Test Author" 267 assert metadata.author_email == "author@example.com" 268 assert metadata.link == HttpUrl("https://example.com") 269 270 def test_to_user_config(self): 271 """Test converting metadata to user config.""" 272 metadata = FeedMetadata( 273 title="Test Feed", 274 author_name="Test Author", 275 author_email="author@example.com", 276 author_uri=HttpUrl("https://example.com/author"), 277 link=HttpUrl("https://example.com"), 278 logo=HttpUrl("https://example.com/logo.png"), 279 ) 280 281 feed_url = HttpUrl("https://example.com/feed.xml") 282 user_config = metadata.to_user_config("testuser", feed_url) 283 284 assert user_config.username == "testuser" 285 assert user_config.feeds == [feed_url] 286 assert user_config.display_name == "Test Author" 287 assert user_config.email == "author@example.com" 288 assert user_config.homepage == HttpUrl("https://example.com/author") 289 assert user_config.icon == HttpUrl("https://example.com/logo.png") 290 291 def test_to_user_config_fallbacks(self): 292 """Test fallback logic in to_user_config.""" 293 metadata = FeedMetadata( 294 title="Test Feed", 295 link=HttpUrl("https://example.com"), 296 icon=HttpUrl("https://example.com/icon.png"), 297 ) 298 299 feed_url = HttpUrl("https://example.com/feed.xml") 300 user_config = metadata.to_user_config("testuser", feed_url) 301 302 assert user_config.display_name == "Test Feed" # Falls back to title 303 assert user_config.homepage == HttpUrl( 304 "https://example.com" 305 ) # Falls back to link 306 assert user_config.icon == HttpUrl("https://example.com/icon.png") 307 assert user_config.email is None 308 309 310class TestUserMetadata: 311 """Test UserMetadata model.""" 312 313 def test_valid_metadata(self): 314 """Test creating valid user metadata.""" 315 now = datetime.now() 316 metadata = UserMetadata( 317 username="testuser", 318 directory="testuser", 319 created=now, 320 last_updated=now, 321 feeds=["https://example.com/feed.xml"], 322 entry_count=5, 323 ) 324 325 assert metadata.username == "testuser" 326 assert metadata.directory == "testuser" 327 assert metadata.entry_count == 5 328 assert len(metadata.feeds) == 1 329 330 def test_update_timestamp(self): 331 """Test updating timestamp.""" 332 now = datetime.now() 333 metadata = UserMetadata( 334 username="testuser", 335 directory="testuser", 336 created=now, 337 last_updated=now, 338 ) 339 340 original_time = metadata.last_updated 341 metadata.update_timestamp() 342 343 assert metadata.last_updated > original_time 344 345 def test_increment_entry_count(self): 346 """Test incrementing entry count.""" 347 metadata = UserMetadata( 348 username="testuser", 349 directory="testuser", 350 created=datetime.now(), 351 last_updated=datetime.now(), 352 entry_count=5, 353 ) 354 355 original_count = metadata.entry_count 356 original_time = metadata.last_updated 357 358 metadata.increment_entry_count(3) 359 360 assert metadata.entry_count == original_count + 3 361 assert metadata.last_updated > original_time 362 363 def test_zulip_associations(self): 364 """Test Zulip association methods.""" 365 metadata = UserMetadata( 366 username="testuser", 367 directory="testuser", 368 created=datetime.now(), 369 last_updated=datetime.now(), 370 ) 371 372 # Test adding association 373 result = metadata.add_zulip_association("example.zulipchat.com", "alice") 374 assert result is True 375 assert len(metadata.zulip_associations) == 1 376 assert metadata.zulip_associations[0].server == "example.zulipchat.com" 377 assert metadata.zulip_associations[0].user_id == "alice" 378 379 # Test adding duplicate association 380 result = metadata.add_zulip_association("example.zulipchat.com", "alice") 381 assert result is False 382 assert len(metadata.zulip_associations) == 1 383 384 # Test adding different association 385 result = metadata.add_zulip_association("other.zulipchat.com", "alice") 386 assert result is True 387 assert len(metadata.zulip_associations) == 2 388 389 # Test get_zulip_mention 390 mention = metadata.get_zulip_mention("example.zulipchat.com") 391 assert mention == "alice" 392 393 mention = metadata.get_zulip_mention("other.zulipchat.com") 394 assert mention == "alice" 395 396 mention = metadata.get_zulip_mention("nonexistent.zulipchat.com") 397 assert mention is None 398 399 # Test removing association 400 result = metadata.remove_zulip_association("example.zulipchat.com", "alice") 401 assert result is True 402 assert len(metadata.zulip_associations) == 1 403 404 # Test removing non-existent association 405 result = metadata.remove_zulip_association("example.zulipchat.com", "alice") 406 assert result is False 407 assert len(metadata.zulip_associations) == 1 408 409 410class TestZulipAssociation: 411 """Test ZulipAssociation model.""" 412 413 def test_valid_association(self): 414 """Test creating valid Zulip association.""" 415 assoc = ZulipAssociation( 416 server="example.zulipchat.com", user_id="alice@example.com" 417 ) 418 419 assert assoc.server == "example.zulipchat.com" 420 assert assoc.user_id == "alice@example.com" 421 422 def test_association_hash(self): 423 """Test that associations are hashable.""" 424 assoc1 = ZulipAssociation(server="example.zulipchat.com", user_id="alice") 425 assoc2 = ZulipAssociation(server="example.zulipchat.com", user_id="alice") 426 assoc3 = ZulipAssociation(server="other.zulipchat.com", user_id="alice") 427 428 # Same associations should have same hash 429 assert hash(assoc1) == hash(assoc2) 430 431 # Different associations should have different hash 432 assert hash(assoc1) != hash(assoc3) 433 434 # Can be used in sets 435 assoc_set = {assoc1, assoc2, assoc3} 436 assert len(assoc_set) == 2 # assoc1 and assoc2 are considered the same