"""Tests for pydantic models.""" from datetime import datetime import pytest from pydantic import HttpUrl, ValidationError from thicket.models import ( AtomEntry, DuplicateMap, FeedMetadata, ThicketConfig, UserConfig, UserMetadata, ZulipAssociation, ) class TestUserConfig: """Test UserConfig model.""" def test_valid_user_config(self): """Test creating valid user config.""" config = UserConfig( username="testuser", feeds=["https://example.com/feed.xml"], email="test@example.com", homepage="https://example.com", display_name="Test User", ) assert config.username == "testuser" assert len(config.feeds) == 1 assert config.feeds[0] == HttpUrl("https://example.com/feed.xml") assert config.email == "test@example.com" assert config.display_name == "Test User" def test_invalid_email(self): """Test validation of invalid email.""" with pytest.raises(ValidationError): UserConfig( username="testuser", feeds=["https://example.com/feed.xml"], email="invalid-email", ) def test_invalid_feed_url(self): """Test validation of invalid feed URL.""" with pytest.raises(ValidationError): UserConfig( username="testuser", feeds=["not-a-url"], ) def test_optional_fields(self): """Test optional fields with None values.""" config = UserConfig( username="testuser", feeds=["https://example.com/feed.xml"], ) assert config.email is None assert config.homepage is None assert config.icon is None assert config.display_name is None class TestThicketConfig: """Test ThicketConfig model.""" def test_valid_config(self, temp_dir): """Test creating valid configuration.""" config = ThicketConfig( git_store=temp_dir / "git_store", cache_dir=temp_dir / "cache", users=[ UserConfig( username="testuser", feeds=["https://example.com/feed.xml"], ) ], ) assert config.git_store == temp_dir / "git_store" assert config.cache_dir == temp_dir / "cache" assert len(config.users) == 1 assert config.users[0].username == "testuser" def test_find_user(self, temp_dir): """Test finding user by username.""" config = ThicketConfig( git_store=temp_dir / "git_store", cache_dir=temp_dir / "cache", users=[ UserConfig(username="user1", feeds=["https://example.com/feed1.xml"]), UserConfig(username="user2", feeds=["https://example.com/feed2.xml"]), ], ) user = config.find_user("user1") assert user is not None assert user.username == "user1" non_user = config.find_user("nonexistent") assert non_user is None def test_add_user(self, temp_dir): """Test adding a new user.""" config = ThicketConfig( git_store=temp_dir / "git_store", cache_dir=temp_dir / "cache", users=[], ) new_user = UserConfig( username="newuser", feeds=["https://example.com/feed.xml"], ) config.add_user(new_user) assert len(config.users) == 1 assert config.users[0].username == "newuser" def test_add_feed_to_user(self, temp_dir): """Test adding feed to existing user.""" config = ThicketConfig( git_store=temp_dir / "git_store", cache_dir=temp_dir / "cache", users=[ UserConfig( username="testuser", feeds=["https://example.com/feed1.xml"] ), ], ) result = config.add_feed_to_user( "testuser", HttpUrl("https://example.com/feed2.xml") ) assert result is True user = config.find_user("testuser") assert len(user.feeds) == 2 assert HttpUrl("https://example.com/feed2.xml") in user.feeds # Test adding to non-existent user result = config.add_feed_to_user( "nonexistent", HttpUrl("https://example.com/feed.xml") ) assert result is False class TestAtomEntry: """Test AtomEntry model.""" def test_valid_entry(self): """Test creating valid Atom entry.""" entry = AtomEntry( id="https://example.com/entry/1", title="Test Entry", link=HttpUrl("https://example.com/entry/1"), updated=datetime.now(), published=datetime.now(), summary="Test summary", content="
Test content
", content_type="html", author={"name": "Test Author"}, categories=["test", "example"], ) assert entry.id == "https://example.com/entry/1" assert entry.title == "Test Entry" assert entry.summary == "Test summary" assert entry.content == "Test content
" assert entry.content_type == "html" assert entry.author["name"] == "Test Author" assert "test" in entry.categories def test_minimal_entry(self): """Test creating minimal Atom entry.""" entry = AtomEntry( id="https://example.com/entry/1", title="Test Entry", link=HttpUrl("https://example.com/entry/1"), updated=datetime.now(), ) assert entry.id == "https://example.com/entry/1" assert entry.title == "Test Entry" assert entry.published is None assert entry.summary is None assert entry.content is None assert entry.content_type == "html" # default assert entry.author is None assert entry.categories == [] class TestDuplicateMap: """Test DuplicateMap model.""" def test_empty_duplicates(self): """Test empty duplicate map.""" dup_map = DuplicateMap() assert len(dup_map.duplicates) == 0 assert not dup_map.is_duplicate("test") assert dup_map.get_canonical("test") == "test" def test_add_duplicate(self): """Test adding duplicate mapping.""" dup_map = DuplicateMap() dup_map.add_duplicate("dup1", "canonical1") assert len(dup_map.duplicates) == 1 assert dup_map.is_duplicate("dup1") assert dup_map.get_canonical("dup1") == "canonical1" assert dup_map.get_canonical("canonical1") == "canonical1" def test_remove_duplicate(self): """Test removing duplicate mapping.""" dup_map = DuplicateMap() dup_map.add_duplicate("dup1", "canonical1") result = dup_map.remove_duplicate("dup1") assert result is True assert len(dup_map.duplicates) == 0 assert not dup_map.is_duplicate("dup1") # Test removing non-existent duplicate result = dup_map.remove_duplicate("nonexistent") assert result is False def test_get_duplicates_for_canonical(self): """Test getting all duplicates for a canonical ID.""" dup_map = DuplicateMap() dup_map.add_duplicate("dup1", "canonical1") dup_map.add_duplicate("dup2", "canonical1") dup_map.add_duplicate("dup3", "canonical2") dups = dup_map.get_duplicates_for_canonical("canonical1") assert len(dups) == 2 assert "dup1" in dups assert "dup2" in dups dups = dup_map.get_duplicates_for_canonical("canonical2") assert len(dups) == 1 assert "dup3" in dups dups = dup_map.get_duplicates_for_canonical("nonexistent") assert len(dups) == 0 class TestFeedMetadata: """Test FeedMetadata model.""" def test_valid_metadata(self): """Test creating valid feed metadata.""" metadata = FeedMetadata( title="Test Feed", author_name="Test Author", author_email="author@example.com", author_uri=HttpUrl("https://example.com/author"), link=HttpUrl("https://example.com"), description="Test description", ) assert metadata.title == "Test Feed" assert metadata.author_name == "Test Author" assert metadata.author_email == "author@example.com" assert metadata.link == HttpUrl("https://example.com") def test_to_user_config(self): """Test converting metadata to user config.""" metadata = FeedMetadata( title="Test Feed", author_name="Test Author", author_email="author@example.com", author_uri=HttpUrl("https://example.com/author"), link=HttpUrl("https://example.com"), logo=HttpUrl("https://example.com/logo.png"), ) feed_url = HttpUrl("https://example.com/feed.xml") user_config = metadata.to_user_config("testuser", feed_url) assert user_config.username == "testuser" assert user_config.feeds == [feed_url] assert user_config.display_name == "Test Author" assert user_config.email == "author@example.com" assert user_config.homepage == HttpUrl("https://example.com/author") assert user_config.icon == HttpUrl("https://example.com/logo.png") def test_to_user_config_fallbacks(self): """Test fallback logic in to_user_config.""" metadata = FeedMetadata( title="Test Feed", link=HttpUrl("https://example.com"), icon=HttpUrl("https://example.com/icon.png"), ) feed_url = HttpUrl("https://example.com/feed.xml") user_config = metadata.to_user_config("testuser", feed_url) assert user_config.display_name == "Test Feed" # Falls back to title assert user_config.homepage == HttpUrl( "https://example.com" ) # Falls back to link assert user_config.icon == HttpUrl("https://example.com/icon.png") assert user_config.email is None class TestUserMetadata: """Test UserMetadata model.""" def test_valid_metadata(self): """Test creating valid user metadata.""" now = datetime.now() metadata = UserMetadata( username="testuser", directory="testuser", created=now, last_updated=now, feeds=["https://example.com/feed.xml"], entry_count=5, ) assert metadata.username == "testuser" assert metadata.directory == "testuser" assert metadata.entry_count == 5 assert len(metadata.feeds) == 1 def test_update_timestamp(self): """Test updating timestamp.""" now = datetime.now() metadata = UserMetadata( username="testuser", directory="testuser", created=now, last_updated=now, ) original_time = metadata.last_updated metadata.update_timestamp() assert metadata.last_updated > original_time def test_increment_entry_count(self): """Test incrementing entry count.""" metadata = UserMetadata( username="testuser", directory="testuser", created=datetime.now(), last_updated=datetime.now(), entry_count=5, ) original_count = metadata.entry_count original_time = metadata.last_updated metadata.increment_entry_count(3) assert metadata.entry_count == original_count + 3 assert metadata.last_updated > original_time def test_zulip_associations(self): """Test Zulip association methods.""" metadata = UserMetadata( username="testuser", directory="testuser", created=datetime.now(), last_updated=datetime.now(), ) # Test adding association result = metadata.add_zulip_association("example.zulipchat.com", "alice") assert result is True assert len(metadata.zulip_associations) == 1 assert metadata.zulip_associations[0].server == "example.zulipchat.com" assert metadata.zulip_associations[0].user_id == "alice" # Test adding duplicate association result = metadata.add_zulip_association("example.zulipchat.com", "alice") assert result is False assert len(metadata.zulip_associations) == 1 # Test adding different association result = metadata.add_zulip_association("other.zulipchat.com", "alice") assert result is True assert len(metadata.zulip_associations) == 2 # Test get_zulip_mention mention = metadata.get_zulip_mention("example.zulipchat.com") assert mention == "alice" mention = metadata.get_zulip_mention("other.zulipchat.com") assert mention == "alice" mention = metadata.get_zulip_mention("nonexistent.zulipchat.com") assert mention is None # Test removing association result = metadata.remove_zulip_association("example.zulipchat.com", "alice") assert result is True assert len(metadata.zulip_associations) == 1 # Test removing non-existent association result = metadata.remove_zulip_association("example.zulipchat.com", "alice") assert result is False assert len(metadata.zulip_associations) == 1 class TestZulipAssociation: """Test ZulipAssociation model.""" def test_valid_association(self): """Test creating valid Zulip association.""" assoc = ZulipAssociation( server="example.zulipchat.com", user_id="alice@example.com" ) assert assoc.server == "example.zulipchat.com" assert assoc.user_id == "alice@example.com" def test_association_hash(self): """Test that associations are hashable.""" assoc1 = ZulipAssociation(server="example.zulipchat.com", user_id="alice") assoc2 = ZulipAssociation(server="example.zulipchat.com", user_id="alice") assoc3 = ZulipAssociation(server="other.zulipchat.com", user_id="alice") # Same associations should have same hash assert hash(assoc1) == hash(assoc2) # Different associations should have different hash assert hash(assoc1) != hash(assoc3) # Can be used in sets assoc_set = {assoc1, assoc2, assoc3} assert len(assoc_set) == 2 # assoc1 and assoc2 are considered the same