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