Manage Atom feeds in a persistent git repository
1"""Tests for pydantic models.""" 2 3from datetime import datetime 4from pathlib import Path 5 6import pytest 7from pydantic import HttpUrl, ValidationError 8 9from thicket.models import ( 10 AtomEntry, 11 DuplicateMap, 12 FeedMetadata, 13 ThicketConfig, 14 UserConfig, 15 UserMetadata, 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(username="testuser", feeds=["https://example.com/feed1.xml"]), 131 ], 132 ) 133 134 result = config.add_feed_to_user("testuser", HttpUrl("https://example.com/feed2.xml")) 135 assert result is True 136 137 user = config.find_user("testuser") 138 assert len(user.feeds) == 2 139 assert HttpUrl("https://example.com/feed2.xml") in user.feeds 140 141 # Test adding to non-existent user 142 result = config.add_feed_to_user("nonexistent", HttpUrl("https://example.com/feed.xml")) 143 assert result is False 144 145 146class TestAtomEntry: 147 """Test AtomEntry model.""" 148 149 def test_valid_entry(self): 150 """Test creating valid Atom entry.""" 151 entry = AtomEntry( 152 id="https://example.com/entry/1", 153 title="Test Entry", 154 link=HttpUrl("https://example.com/entry/1"), 155 updated=datetime.now(), 156 published=datetime.now(), 157 summary="Test summary", 158 content="<p>Test content</p>", 159 content_type="html", 160 author={"name": "Test Author"}, 161 categories=["test", "example"], 162 ) 163 164 assert entry.id == "https://example.com/entry/1" 165 assert entry.title == "Test Entry" 166 assert entry.summary == "Test summary" 167 assert entry.content == "<p>Test content</p>" 168 assert entry.content_type == "html" 169 assert entry.author["name"] == "Test Author" 170 assert "test" in entry.categories 171 172 def test_minimal_entry(self): 173 """Test creating minimal Atom entry.""" 174 entry = AtomEntry( 175 id="https://example.com/entry/1", 176 title="Test Entry", 177 link=HttpUrl("https://example.com/entry/1"), 178 updated=datetime.now(), 179 ) 180 181 assert entry.id == "https://example.com/entry/1" 182 assert entry.title == "Test Entry" 183 assert entry.published is None 184 assert entry.summary is None 185 assert entry.content is None 186 assert entry.content_type == "html" # default 187 assert entry.author is None 188 assert entry.categories == [] 189 190 191class TestDuplicateMap: 192 """Test DuplicateMap model.""" 193 194 def test_empty_duplicates(self): 195 """Test empty duplicate map.""" 196 dup_map = DuplicateMap() 197 assert len(dup_map.duplicates) == 0 198 assert not dup_map.is_duplicate("test") 199 assert dup_map.get_canonical("test") == "test" 200 201 def test_add_duplicate(self): 202 """Test adding duplicate mapping.""" 203 dup_map = DuplicateMap() 204 dup_map.add_duplicate("dup1", "canonical1") 205 206 assert len(dup_map.duplicates) == 1 207 assert dup_map.is_duplicate("dup1") 208 assert dup_map.get_canonical("dup1") == "canonical1" 209 assert dup_map.get_canonical("canonical1") == "canonical1" 210 211 def test_remove_duplicate(self): 212 """Test removing duplicate mapping.""" 213 dup_map = DuplicateMap() 214 dup_map.add_duplicate("dup1", "canonical1") 215 216 result = dup_map.remove_duplicate("dup1") 217 assert result is True 218 assert len(dup_map.duplicates) == 0 219 assert not dup_map.is_duplicate("dup1") 220 221 # Test removing non-existent duplicate 222 result = dup_map.remove_duplicate("nonexistent") 223 assert result is False 224 225 def test_get_duplicates_for_canonical(self): 226 """Test getting all duplicates for a canonical ID.""" 227 dup_map = DuplicateMap() 228 dup_map.add_duplicate("dup1", "canonical1") 229 dup_map.add_duplicate("dup2", "canonical1") 230 dup_map.add_duplicate("dup3", "canonical2") 231 232 dups = dup_map.get_duplicates_for_canonical("canonical1") 233 assert len(dups) == 2 234 assert "dup1" in dups 235 assert "dup2" in dups 236 237 dups = dup_map.get_duplicates_for_canonical("canonical2") 238 assert len(dups) == 1 239 assert "dup3" in dups 240 241 dups = dup_map.get_duplicates_for_canonical("nonexistent") 242 assert len(dups) == 0 243 244 245class TestFeedMetadata: 246 """Test FeedMetadata model.""" 247 248 def test_valid_metadata(self): 249 """Test creating valid feed metadata.""" 250 metadata = FeedMetadata( 251 title="Test Feed", 252 author_name="Test Author", 253 author_email="author@example.com", 254 author_uri=HttpUrl("https://example.com/author"), 255 link=HttpUrl("https://example.com"), 256 description="Test description", 257 ) 258 259 assert metadata.title == "Test Feed" 260 assert metadata.author_name == "Test Author" 261 assert metadata.author_email == "author@example.com" 262 assert metadata.link == HttpUrl("https://example.com") 263 264 def test_to_user_config(self): 265 """Test converting metadata to user config.""" 266 metadata = FeedMetadata( 267 title="Test Feed", 268 author_name="Test Author", 269 author_email="author@example.com", 270 author_uri=HttpUrl("https://example.com/author"), 271 link=HttpUrl("https://example.com"), 272 logo=HttpUrl("https://example.com/logo.png"), 273 ) 274 275 feed_url = HttpUrl("https://example.com/feed.xml") 276 user_config = metadata.to_user_config("testuser", feed_url) 277 278 assert user_config.username == "testuser" 279 assert user_config.feeds == [feed_url] 280 assert user_config.display_name == "Test Author" 281 assert user_config.email == "author@example.com" 282 assert user_config.homepage == HttpUrl("https://example.com/author") 283 assert user_config.icon == HttpUrl("https://example.com/logo.png") 284 285 def test_to_user_config_fallbacks(self): 286 """Test fallback logic in to_user_config.""" 287 metadata = FeedMetadata( 288 title="Test Feed", 289 link=HttpUrl("https://example.com"), 290 icon=HttpUrl("https://example.com/icon.png"), 291 ) 292 293 feed_url = HttpUrl("https://example.com/feed.xml") 294 user_config = metadata.to_user_config("testuser", feed_url) 295 296 assert user_config.display_name == "Test Feed" # Falls back to title 297 assert user_config.homepage == HttpUrl("https://example.com") # Falls back to link 298 assert user_config.icon == HttpUrl("https://example.com/icon.png") 299 assert user_config.email is None 300 301 302class TestUserMetadata: 303 """Test UserMetadata model.""" 304 305 def test_valid_metadata(self): 306 """Test creating valid user metadata.""" 307 now = datetime.now() 308 metadata = UserMetadata( 309 username="testuser", 310 directory="testuser", 311 created=now, 312 last_updated=now, 313 feeds=["https://example.com/feed.xml"], 314 entry_count=5, 315 ) 316 317 assert metadata.username == "testuser" 318 assert metadata.directory == "testuser" 319 assert metadata.entry_count == 5 320 assert len(metadata.feeds) == 1 321 322 def test_update_timestamp(self): 323 """Test updating timestamp.""" 324 now = datetime.now() 325 metadata = UserMetadata( 326 username="testuser", 327 directory="testuser", 328 created=now, 329 last_updated=now, 330 ) 331 332 original_time = metadata.last_updated 333 metadata.update_timestamp() 334 335 assert metadata.last_updated > original_time 336 337 def test_increment_entry_count(self): 338 """Test incrementing entry count.""" 339 metadata = UserMetadata( 340 username="testuser", 341 directory="testuser", 342 created=datetime.now(), 343 last_updated=datetime.now(), 344 entry_count=5, 345 ) 346 347 original_count = metadata.entry_count 348 original_time = metadata.last_updated 349 350 metadata.increment_entry_count(3) 351 352 assert metadata.entry_count == original_count + 3 353 assert metadata.last_updated > original_time