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