"""Integration tests for Mastodon RSS Bot""" import unittest from unittest.mock import Mock, patch import tempfile import os import time from bot import MastodonRSSBot import responses import feedparser class TestRSSFeedIntegration(unittest.TestCase): """Integration tests for RSS feed parsing""" def setUp(self): """Set up test fixtures""" self.test_config = { "client_id": "test_client_id", "client_secret": "test_client_secret", "access_token": "test_access_token", "instance_url": "https://mastodon.test", "feed_url": "https://example.com/feed.xml", "toot_visibility": "public", "check_interval": 1, "state_file": tempfile.mktemp(), } def tearDown(self): """Clean up test files""" if os.path.exists(self.test_config["state_file"]): os.remove(self.test_config["state_file"]) @responses.activate @patch("bot.Mastodon") def test_end_to_end_rss_to_mastodon(self, mock_mastodon): """Test complete flow from RSS feed to Mastodon post""" # Mock RSS feed response rss_feed = """ Test Feed https://example.com Test RSS Feed First Article https://example.com/article1 This is the first article Second Article https://example.com/article2 This is the second article """ responses.add( responses.GET, "https://example.com/feed.xml", body=rss_feed, status=200, content_type="application/xml", ) # Mock Mastodon instance mock_instance = Mock() mock_mastodon.return_value = mock_instance # Create bot and process entries bot = MastodonRSSBot(**self.test_config) count = bot.process_new_entries() # Verify results self.assertEqual(count, 2) self.assertEqual(mock_instance.status_post.call_count, 2) # Verify the content of posts calls = mock_instance.status_post.call_args_list self.assertIn("First Article", calls[0][0][0]) self.assertIn("https://example.com/article1", calls[0][0][0]) self.assertIn("Second Article", calls[1][0][0]) self.assertIn("https://example.com/article2", calls[1][0][0]) @responses.activate @patch("bot.Mastodon") def test_atom_feed_parsing(self, mock_mastodon): """Test parsing Atom feeds""" atom_feed = """ Test Atom Feed 2024-01-01T00:00:00Z Atom Article https://example.com/atom1 2024-01-01T00:00:00Z This is an atom article """ responses.add( responses.GET, "https://example.com/feed.xml", body=atom_feed, status=200, content_type="application/atom+xml", ) mock_instance = Mock() mock_mastodon.return_value = mock_instance bot = MastodonRSSBot(**self.test_config) count = bot.process_new_entries() self.assertEqual(count, 1) calls = mock_instance.status_post.call_args_list self.assertIn("Atom Article", calls[0][0][0]) @responses.activate @patch("bot.Mastodon") def test_persistence_across_runs(self, mock_mastodon): """Test that processed entries persist across multiple bot runs""" rss_feed = """ Test Feed Article 1 https://example.com/1 """ responses.add( responses.GET, "https://example.com/feed.xml", body=rss_feed, status=200, content_type="application/xml", ) mock_instance = Mock() mock_mastodon.return_value = mock_instance # First run - should post the article bot1 = MastodonRSSBot(**self.test_config) count1 = bot1.process_new_entries() self.assertEqual(count1, 1) # Second run - should NOT post again (already processed) bot2 = MastodonRSSBot(**self.test_config) count2 = bot2.process_new_entries() self.assertEqual(count2, 0) # Total posts should be 1 self.assertEqual(mock_instance.status_post.call_count, 1) @responses.activate @patch("bot.Mastodon") def test_incremental_feed_updates(self, mock_mastodon): """Test handling of new entries added to feed over time""" # Initial feed with 2 articles initial_feed = """ Test Feed Article 1 https://example.com/1 Article 2 https://example.com/2 """ responses.add( responses.GET, "https://example.com/feed.xml", body=initial_feed, status=200, content_type="application/xml", ) mock_instance = Mock() mock_mastodon.return_value = mock_instance # First run bot = MastodonRSSBot(**self.test_config) count1 = bot.process_new_entries() self.assertEqual(count1, 2) # Update feed with 1 new article responses.reset() updated_feed = """ Test Feed Article 3 https://example.com/3 Article 2 https://example.com/2 Article 1 https://example.com/1 """ responses.add( responses.GET, "https://example.com/feed.xml", body=updated_feed, status=200, content_type="application/xml", ) # Second run - should only post the new article count2 = bot.process_new_entries() self.assertEqual(count2, 1) # Verify only 3 total posts self.assertEqual(mock_instance.status_post.call_count, 3) @responses.activate @patch("bot.Mastodon") def test_network_error_handling(self, mock_mastodon): """Test handling of network errors when fetching feed""" responses.add( responses.GET, "https://example.com/feed.xml", body="Network error", status=500, ) mock_instance = Mock() mock_mastodon.return_value = mock_instance bot = MastodonRSSBot(**self.test_config) count = bot.process_new_entries() # Should handle error gracefully self.assertEqual(count, 0) self.assertEqual(mock_instance.status_post.call_count, 0) @responses.activate @patch("bot.Mastodon") def test_malformed_xml_handling(self, mock_mastodon): """Test handling of malformed XML feeds""" malformed_feed = """ Broken Feed <item> <title>Article """ # Intentionally malformed responses.add( responses.GET, "https://example.com/feed.xml", body=malformed_feed, status=200, content_type="application/xml", ) mock_instance = Mock() mock_mastodon.return_value = mock_instance bot = MastodonRSSBot(**self.test_config) # Should handle malformed feed gracefully count = bot.process_new_entries() # feedparser is lenient and may parse some entries # The important thing is it doesn't crash self.assertIsInstance(count, int) class TestMastodonAPIIntegration(unittest.TestCase): """Integration tests for Mastodon API interaction""" def setUp(self): """Set up test fixtures""" self.test_config = { "client_id": "test_client_id", "client_secret": "test_client_secret", "access_token": "test_access_token", "instance_url": "https://mastodon.test", "feed_url": "https://example.com/feed.xml", "toot_visibility": "public", "check_interval": 1, "state_file": tempfile.mktemp(), } def tearDown(self): """Clean up test files""" if os.path.exists(self.test_config["state_file"]): os.remove(self.test_config["state_file"]) @patch("bot.Mastodon") def test_different_visibility_levels(self, mock_mastodon): """Test posting with different visibility levels""" mock_instance = Mock() mock_mastodon.return_value = mock_instance visibility_levels = ["public", "unlisted", "private", "direct"] for visibility in visibility_levels: self.test_config["toot_visibility"] = visibility bot = MastodonRSSBot(**self.test_config) bot.post_to_mastodon("Test status") # Verify all visibility levels were used calls = mock_instance.status_post.call_args_list for idx, visibility in enumerate(visibility_levels): self.assertEqual(calls[idx][1]["visibility"], visibility) @patch("bot.Mastodon") def test_retry_on_rate_limit(self, mock_mastodon): """Test that rate limit errors are handled appropriately""" from mastodon import MastodonRatelimitError mock_instance = Mock() # First call raises rate limit error, second succeeds mock_instance.status_post.side_effect = [ MastodonRatelimitError("Rate limited"), None, ] mock_mastodon.return_value = mock_instance bot = MastodonRSSBot(**self.test_config) # First attempt should fail result1 = bot.post_to_mastodon("Test status") self.assertFalse(result1) # Second attempt should succeed result2 = bot.post_to_mastodon("Test status") self.assertTrue(result2) if __name__ == "__main__": unittest.main()