Manage Atom feeds in a persistent git repository
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) 16 17 18class TestUserConfig: 19 """Test UserConfig model.""" 20 21 def test_valid_user_config(self): 22 """Test creating valid user config.""" 23 config = UserConfig( 24 username="testuser", 25 feeds=["https://example.com/feed.xml"], 26 email="test@example.com", 27 homepage="https://example.com", 28 display_name="Test User", 29 ) 30 31 assert config.username == "testuser" 32 assert len(config.feeds) == 1 33 assert config.feeds[0] == HttpUrl("https://example.com/feed.xml") 34 assert config.email == "test@example.com" 35 assert config.display_name == "Test User" 36 37 def test_invalid_email(self): 38 """Test validation of invalid email.""" 39 with pytest.raises(ValidationError): 40 UserConfig( 41 username="testuser", 42 feeds=["https://example.com/feed.xml"], 43 email="invalid-email", 44 ) 45 46 def test_invalid_feed_url(self): 47 """Test validation of invalid feed URL.""" 48 with pytest.raises(ValidationError): 49 UserConfig( 50 username="testuser", 51 feeds=["not-a-url"], 52 ) 53 54 def test_optional_fields(self): 55 """Test optional fields with None values.""" 56 config = UserConfig( 57 username="testuser", 58 feeds=["https://example.com/feed.xml"], 59 ) 60 61 assert config.email is None 62 assert config.homepage is None 63 assert config.icon is None 64 assert config.display_name is None 65 66 67class TestThicketConfig: 68 """Test ThicketConfig model.""" 69 70 def test_valid_config(self, temp_dir): 71 """Test creating valid configuration.""" 72 config = ThicketConfig( 73 git_store=temp_dir / "git_store", 74 cache_dir=temp_dir / "cache", 75 users=[ 76 UserConfig( 77 username="testuser", 78 feeds=["https://example.com/feed.xml"], 79 ) 80 ], 81 ) 82 83 assert config.git_store == temp_dir / "git_store" 84 assert config.cache_dir == temp_dir / "cache" 85 assert len(config.users) == 1 86 assert config.users[0].username == "testuser" 87 88 def test_find_user(self, temp_dir): 89 """Test finding user by username.""" 90 config = ThicketConfig( 91 git_store=temp_dir / "git_store", 92 cache_dir=temp_dir / "cache", 93 users=[ 94 UserConfig(username="user1", feeds=["https://example.com/feed1.xml"]), 95 UserConfig(username="user2", feeds=["https://example.com/feed2.xml"]), 96 ], 97 ) 98 99 user = config.find_user("user1") 100 assert user is not None 101 assert user.username == "user1" 102 103 non_user = config.find_user("nonexistent") 104 assert non_user is None 105 106 def test_add_user(self, temp_dir): 107 """Test adding a new user.""" 108 config = ThicketConfig( 109 git_store=temp_dir / "git_store", 110 cache_dir=temp_dir / "cache", 111 users=[], 112 ) 113 114 new_user = UserConfig( 115 username="newuser", 116 feeds=["https://example.com/feed.xml"], 117 ) 118 119 config.add_user(new_user) 120 assert len(config.users) == 1 121 assert config.users[0].username == "newuser" 122 123 def test_add_feed_to_user(self, temp_dir): 124 """Test adding feed to existing user.""" 125 config = ThicketConfig( 126 git_store=temp_dir / "git_store", 127 cache_dir=temp_dir / "cache", 128 users=[ 129 UserConfig(username="testuser", feeds=["https://example.com/feed1.xml"]), 130 ], 131 ) 132 133 result = config.add_feed_to_user("testuser", HttpUrl("https://example.com/feed2.xml")) 134 assert result is True 135 136 user = config.find_user("testuser") 137 assert len(user.feeds) == 2 138 assert HttpUrl("https://example.com/feed2.xml") in user.feeds 139 140 # Test adding to non-existent user 141 result = config.add_feed_to_user("nonexistent", HttpUrl("https://example.com/feed.xml")) 142 assert result is False 143 144 145class TestAtomEntry: 146 """Test AtomEntry model.""" 147 148 def test_valid_entry(self): 149 """Test creating valid Atom entry.""" 150 entry = AtomEntry( 151 id="https://example.com/entry/1", 152 title="Test Entry", 153 link=HttpUrl("https://example.com/entry/1"), 154 updated=datetime.now(), 155 published=datetime.now(), 156 summary="Test summary", 157 content="<p>Test content</p>", 158 content_type="html", 159 author={"name": "Test Author"}, 160 categories=["test", "example"], 161 ) 162 163 assert entry.id == "https://example.com/entry/1" 164 assert entry.title == "Test Entry" 165 assert entry.summary == "Test summary" 166 assert entry.content == "<p>Test content</p>" 167 assert entry.content_type == "html" 168 assert entry.author["name"] == "Test Author" 169 assert "test" in entry.categories 170 171 def test_minimal_entry(self): 172 """Test creating minimal Atom entry.""" 173 entry = AtomEntry( 174 id="https://example.com/entry/1", 175 title="Test Entry", 176 link=HttpUrl("https://example.com/entry/1"), 177 updated=datetime.now(), 178 ) 179 180 assert entry.id == "https://example.com/entry/1" 181 assert entry.title == "Test Entry" 182 assert entry.published is None 183 assert entry.summary is None 184 assert entry.content is None 185 assert entry.content_type == "html" # default 186 assert entry.author is None 187 assert entry.categories == [] 188 189 190class TestDuplicateMap: 191 """Test DuplicateMap model.""" 192 193 def test_empty_duplicates(self): 194 """Test empty duplicate map.""" 195 dup_map = DuplicateMap() 196 assert len(dup_map.duplicates) == 0 197 assert not dup_map.is_duplicate("test") 198 assert dup_map.get_canonical("test") == "test" 199 200 def test_add_duplicate(self): 201 """Test adding duplicate mapping.""" 202 dup_map = DuplicateMap() 203 dup_map.add_duplicate("dup1", "canonical1") 204 205 assert len(dup_map.duplicates) == 1 206 assert dup_map.is_duplicate("dup1") 207 assert dup_map.get_canonical("dup1") == "canonical1" 208 assert dup_map.get_canonical("canonical1") == "canonical1" 209 210 def test_remove_duplicate(self): 211 """Test removing duplicate mapping.""" 212 dup_map = DuplicateMap() 213 dup_map.add_duplicate("dup1", "canonical1") 214 215 result = dup_map.remove_duplicate("dup1") 216 assert result is True 217 assert len(dup_map.duplicates) == 0 218 assert not dup_map.is_duplicate("dup1") 219 220 # Test removing non-existent duplicate 221 result = dup_map.remove_duplicate("nonexistent") 222 assert result is False 223 224 def test_get_duplicates_for_canonical(self): 225 """Test getting all duplicates for a canonical ID.""" 226 dup_map = DuplicateMap() 227 dup_map.add_duplicate("dup1", "canonical1") 228 dup_map.add_duplicate("dup2", "canonical1") 229 dup_map.add_duplicate("dup3", "canonical2") 230 231 dups = dup_map.get_duplicates_for_canonical("canonical1") 232 assert len(dups) == 2 233 assert "dup1" in dups 234 assert "dup2" in dups 235 236 dups = dup_map.get_duplicates_for_canonical("canonical2") 237 assert len(dups) == 1 238 assert "dup3" in dups 239 240 dups = dup_map.get_duplicates_for_canonical("nonexistent") 241 assert len(dups) == 0 242 243 244class TestFeedMetadata: 245 """Test FeedMetadata model.""" 246 247 def test_valid_metadata(self): 248 """Test creating valid feed metadata.""" 249 metadata = FeedMetadata( 250 title="Test Feed", 251 author_name="Test Author", 252 author_email="author@example.com", 253 author_uri=HttpUrl("https://example.com/author"), 254 link=HttpUrl("https://example.com"), 255 description="Test description", 256 ) 257 258 assert metadata.title == "Test Feed" 259 assert metadata.author_name == "Test Author" 260 assert metadata.author_email == "author@example.com" 261 assert metadata.link == HttpUrl("https://example.com") 262 263 def test_to_user_config(self): 264 """Test converting metadata to user config.""" 265 metadata = FeedMetadata( 266 title="Test Feed", 267 author_name="Test Author", 268 author_email="author@example.com", 269 author_uri=HttpUrl("https://example.com/author"), 270 link=HttpUrl("https://example.com"), 271 logo=HttpUrl("https://example.com/logo.png"), 272 ) 273 274 feed_url = HttpUrl("https://example.com/feed.xml") 275 user_config = metadata.to_user_config("testuser", feed_url) 276 277 assert user_config.username == "testuser" 278 assert user_config.feeds == [feed_url] 279 assert user_config.display_name == "Test Author" 280 assert user_config.email == "author@example.com" 281 assert user_config.homepage == HttpUrl("https://example.com/author") 282 assert user_config.icon == HttpUrl("https://example.com/logo.png") 283 284 def test_to_user_config_fallbacks(self): 285 """Test fallback logic in to_user_config.""" 286 metadata = FeedMetadata( 287 title="Test Feed", 288 link=HttpUrl("https://example.com"), 289 icon=HttpUrl("https://example.com/icon.png"), 290 ) 291 292 feed_url = HttpUrl("https://example.com/feed.xml") 293 user_config = metadata.to_user_config("testuser", feed_url) 294 295 assert user_config.display_name == "Test Feed" # Falls back to title 296 assert user_config.homepage == HttpUrl("https://example.com") # Falls back to link 297 assert user_config.icon == HttpUrl("https://example.com/icon.png") 298 assert user_config.email is None 299 300 301class TestUserMetadata: 302 """Test UserMetadata model.""" 303 304 def test_valid_metadata(self): 305 """Test creating valid user metadata.""" 306 now = datetime.now() 307 metadata = UserMetadata( 308 username="testuser", 309 directory="testuser", 310 created=now, 311 last_updated=now, 312 feeds=["https://example.com/feed.xml"], 313 entry_count=5, 314 ) 315 316 assert metadata.username == "testuser" 317 assert metadata.directory == "testuser" 318 assert metadata.entry_count == 5 319 assert len(metadata.feeds) == 1 320 321 def test_update_timestamp(self): 322 """Test updating timestamp.""" 323 now = datetime.now() 324 metadata = UserMetadata( 325 username="testuser", 326 directory="testuser", 327 created=now, 328 last_updated=now, 329 ) 330 331 original_time = metadata.last_updated 332 metadata.update_timestamp() 333 334 assert metadata.last_updated > original_time 335 336 def test_increment_entry_count(self): 337 """Test incrementing entry count.""" 338 metadata = UserMetadata( 339 username="testuser", 340 directory="testuser", 341 created=datetime.now(), 342 last_updated=datetime.now(), 343 entry_count=5, 344 ) 345 346 original_count = metadata.entry_count 347 original_time = metadata.last_updated 348 349 metadata.increment_entry_count(3) 350 351 assert metadata.entry_count == original_count + 3 352 assert metadata.last_updated > original_time