from __future__ import annotations import os import tempfile from pathlib import Path from typing import LiteralString from unittest.mock import MagicMock from unittest.mock import patch import pytest from reader import Feed from reader import Reader from reader import make_reader from discord_rss_bot.feeds import extract_domain from discord_rss_bot.feeds import is_youtube_feed from discord_rss_bot.feeds import send_entry_to_discord from discord_rss_bot.feeds import send_to_discord from discord_rss_bot.feeds import should_send_embed_check from discord_rss_bot.feeds import truncate_webhook_message def test_send_to_discord() -> None: """Test sending to Discord.""" with tempfile.TemporaryDirectory() as temp_dir: # Create the temp directory. Path.mkdir(Path(temp_dir), exist_ok=True) assert Path.exists(Path(temp_dir)), f"The directory '{temp_dir}' should exist." # Create a temporary reader. reader: Reader = make_reader(url=str(Path(temp_dir) / "test_db.sqlite")) assert reader is not None, "The reader should not be None." # Add a feed to the reader. reader.add_feed("https://www.reddit.com/r/Python/.rss") # Update the feed to get the entries. reader.update_feeds() # Get the feed. feed: Feed = reader.get_feed("https://www.reddit.com/r/Python/.rss") assert feed is not None, f"The feed should not be None. Got: {feed}" # Get the webhook. webhook_url: str | None = os.environ.get("TEST_WEBHOOK_URL") if not webhook_url: reader.close() pytest.skip("No webhook URL provided.") assert webhook_url is not None, f"The webhook URL should not be None. Got: {webhook_url}" # Add tag to the feed and check if it is there. reader.set_tag(feed, "webhook", webhook_url) # pyright: ignore[reportArgumentType] assert reader.get_tag(feed, "webhook") == webhook_url, f"The webhook URL should be '{webhook_url}'." # Send the feed to Discord. send_to_discord(custom_reader=reader, feed=feed, do_once=True) # Close the reader, so we can delete the directory. reader.close() def test_truncate_webhook_message_short_message(): message = "This is a short message." assert_msg = "The message should remain unchanged if it's less than 4000 characters." assert truncate_webhook_message(message) == message, assert_msg def test_truncate_webhook_message_exact_length(): message: LiteralString = "A" * 4000 # Exact length of max_content_length assert_msg: str = f"The message should remain unchanged if it's exactly {4000} characters." assert truncate_webhook_message(message) == message, assert_msg def test_truncate_webhook_message_long_message(): message: str = "A" * 4100 # Exceeds max_content_length truncated_message: str = truncate_webhook_message(message) # Ensure the truncated message length is correct assert_msg = "The length of the truncated message should be between 3999 and 4000." assert 3999 <= len(truncated_message) <= 4000, assert_msg # Calculate half length for the truncated parts half_length = (4000 - 3) // 2 # Test the beginning of the message assert_msg = "The beginning of the truncated message should match the original message." assert truncated_message[:half_length] == "A" * half_length, assert_msg # Test the end of the message assert_msg = "The end of the truncated message should be '...' to indicate truncation." assert truncated_message[-half_length:] == "A" * half_length, assert_msg def test_is_youtube_feed(): """Test the is_youtube_feed function.""" # YouTube feed URLs assert is_youtube_feed("https://www.youtube.com/feeds/videos.xml?channel_id=123456") is True assert is_youtube_feed("https://www.youtube.com/feeds/videos.xml?user=username") is True # Non-YouTube feed URLs assert is_youtube_feed("https://www.example.com/feed.xml") is False assert is_youtube_feed("https://www.youtube.com/watch?v=123456") is False assert is_youtube_feed("https://www.reddit.com/r/Python/.rss") is False @patch("discord_rss_bot.feeds.logger") def test_should_send_embed_check_youtube_feeds(mock_logger: MagicMock) -> None: """Test should_send_embed_check returns False for YouTube feeds regardless of settings.""" # Create mocks mock_reader = MagicMock() mock_entry = MagicMock() # Configure a YouTube feed mock_entry.feed.url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456" # Set reader to return True for should_send_embed (would normally create an embed) mock_reader.get_tag.return_value = True # Result should be False, overriding the feed settings result = should_send_embed_check(mock_reader, mock_entry) assert result is False, "YouTube feeds should never use embeds" # Function should not even call get_tag for YouTube feeds mock_reader.get_tag.assert_not_called() @patch("discord_rss_bot.feeds.logger") def test_should_send_embed_check_normal_feeds(mock_logger: MagicMock) -> None: """Test should_send_embed_check returns feed settings for non-YouTube feeds.""" # Create mocks mock_reader = MagicMock() mock_entry = MagicMock() # Configure a normal feed mock_entry.feed.url = "https://www.example.com/feed.xml" # Test with should_send_embed set to True mock_reader.get_tag.return_value = True result = should_send_embed_check(mock_reader, mock_entry) assert result is True, "Normal feeds should use embeds when enabled" # Test with should_send_embed set to False mock_reader.get_tag.return_value = False result = should_send_embed_check(mock_reader, mock_entry) assert result is False, "Normal feeds should not use embeds when disabled" @patch("discord_rss_bot.feeds.get_reader") @patch("discord_rss_bot.feeds.get_custom_message") @patch("discord_rss_bot.feeds.replace_tags_in_text_message") @patch("discord_rss_bot.feeds.create_embed_webhook") @patch("discord_rss_bot.feeds.DiscordWebhook") @patch("discord_rss_bot.feeds.execute_webhook") def test_send_entry_to_discord_youtube_feed( mock_execute_webhook: MagicMock, mock_discord_webhook: MagicMock, mock_create_embed: MagicMock, mock_replace_tags: MagicMock, mock_get_custom_message: MagicMock, mock_get_reader: MagicMock, ): """Test send_entry_to_discord function with YouTube feeds.""" # Set up mocks mock_reader = MagicMock() mock_get_reader.return_value = mock_reader mock_entry = MagicMock() mock_feed = MagicMock() # Configure a YouTube feed mock_entry.feed = mock_feed mock_entry.feed.url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456" mock_entry.feed_url = "https://www.youtube.com/feeds/videos.xml?channel_id=123456" # Mock the tags mock_reader.get_tag.side_effect = lambda feed, tag, default=None: { # noqa: ARG005 "webhook": "https://discord.com/api/webhooks/123/abc", "should_send_embed": True, # This should be ignored for YouTube feeds }.get(tag, default) # Mock custom message mock_get_custom_message.return_value = "Custom message" mock_replace_tags.return_value = "Formatted message with {{entry_link}}" # Mock webhook mock_webhook = MagicMock() mock_discord_webhook.return_value = mock_webhook # Call the function send_entry_to_discord(mock_entry) # Assertions mock_create_embed.assert_not_called() mock_discord_webhook.assert_called_once() # Check webhook was created with the right message webhook_call_kwargs = mock_discord_webhook.call_args[1] assert "content" in webhook_call_kwargs, "Webhook should have content" assert webhook_call_kwargs["url"] == "https://discord.com/api/webhooks/123/abc" # Verify execute_webhook was called mock_execute_webhook.assert_called_once_with(mock_webhook, mock_entry) def test_extract_domain_youtube_feed() -> None: """Test extract_domain for YouTube feeds.""" url: str = "https://www.youtube.com/feeds/videos.xml?channel_id=123456" assert extract_domain(url) == "YouTube", "YouTube feeds should return 'YouTube' as the domain." def test_extract_domain_reddit_feed() -> None: """Test extract_domain for Reddit feeds.""" url: str = "https://www.reddit.com/r/Python/.rss" assert extract_domain(url) == "Reddit", "Reddit feeds should return 'Reddit' as the domain." def test_extract_domain_github_feed() -> None: """Test extract_domain for GitHub feeds.""" url: str = "https://www.github.com/user/repo" assert extract_domain(url) == "GitHub", "GitHub feeds should return 'GitHub' as the domain." def test_extract_domain_custom_domain() -> None: """Test extract_domain for custom domains.""" url: str = "https://www.example.com/feed" assert extract_domain(url) == "Example", "Custom domains should return the capitalized first part of the domain." def test_extract_domain_no_www_prefix() -> None: """Test extract_domain removes 'www.' prefix.""" url: str = "https://www.example.com/feed" assert extract_domain(url) == "Example", "The 'www.' prefix should be removed from the domain." def test_extract_domain_no_tld() -> None: """Test extract_domain for domains without a TLD.""" url: str = "https://localhost/feed" assert extract_domain(url) == "Localhost", "Domains without a TLD should return the capitalized domain." def test_extract_domain_invalid_url() -> None: """Test extract_domain for invalid URLs.""" url: str = "not-a-valid-url" assert extract_domain(url) == "Other", "Invalid URLs should return 'Other' as the domain." def test_extract_domain_empty_url() -> None: """Test extract_domain for empty URLs.""" url: str = "" assert extract_domain(url) == "Other", "Empty URLs should return 'Other' as the domain." def test_extract_domain_special_characters() -> None: """Test extract_domain for URLs with special characters.""" url: str = "https://www.ex-ample.com/feed" assert extract_domain(url) == "Ex-ample", "Domains with special characters should return the capitalized domain." @pytest.mark.parametrize( argnames=("url", "expected"), argvalues=[ ("https://blog.something.com", "Something"), ("https://www.something.com", "Something"), ("https://subdomain.example.co.uk", "Example"), ("https://github.com/user/repo", "GitHub"), ("https://youtube.com/feeds/videos.xml?channel_id=abc", "YouTube"), ("https://reddit.com/r/python/.rss", "Reddit"), ("", "Other"), ("not a url", "Other"), ("https://www.example.com", "Example"), ("https://foo.bar.baz.com", "Baz"), ], ) def test_extract_domain(url: str, expected: str) -> None: assert extract_domain(url) == expected