mirror of
https://github.com/aserper/masto-rss.git
synced 2025-12-17 13:25:25 +00:00
Replace responses library HTTP mocking with direct feedparser.parse() mocking to eliminate XML parsing compatibility issues. All integration tests now mock feedparser output directly, avoiding HTTP layer complexity. Changes: - Replace responses.activate decorators with feedparser.parse patches - Mock feed objects directly instead of mocking HTTP responses - Remove responses library dependency from requirements-test.txt - Simplify test setup by eliminating XML string encoding issues This approach provides more reliable testing by directly controlling feedparser behavior rather than relying on HTTP mocking layer.
245 lines
8.6 KiB
Python
245 lines
8.6 KiB
Python
"""Integration tests for Mastodon RSS Bot"""
|
|
|
|
import unittest
|
|
from unittest.mock import Mock, patch
|
|
import tempfile
|
|
import os
|
|
from bot import MastodonRSSBot
|
|
|
|
|
|
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"])
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_end_to_end_rss_to_mastodon(self, mock_mastodon, mock_parse):
|
|
"""Test complete flow from RSS feed to Mastodon post"""
|
|
# Create mock feed object
|
|
mock_feed = Mock()
|
|
mock_feed.entries = [
|
|
{"title": "First Article", "link": "https://example.com/article1"},
|
|
{"title": "Second Article", "link": "https://example.com/article2"},
|
|
]
|
|
mock_parse.return_value = mock_feed
|
|
|
|
# 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])
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_atom_feed_parsing(self, mock_mastodon, mock_parse):
|
|
"""Test parsing Atom feeds"""
|
|
# Create mock Atom feed object
|
|
mock_feed = Mock()
|
|
mock_feed.entries = [
|
|
{"title": "Atom Article", "link": "https://example.com/atom1"}
|
|
]
|
|
mock_parse.return_value = mock_feed
|
|
|
|
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])
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_persistence_across_runs(self, mock_mastodon, mock_parse):
|
|
"""Test that processed entries persist across multiple bot runs"""
|
|
# Create mock feed object
|
|
mock_feed = Mock()
|
|
mock_feed.entries = [{"title": "Article 1", "link": "https://example.com/1"}]
|
|
mock_parse.return_value = mock_feed
|
|
|
|
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)
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_incremental_feed_updates(self, mock_mastodon, mock_parse):
|
|
"""Test handling of new entries added to feed over time"""
|
|
mock_instance = Mock()
|
|
mock_mastodon.return_value = mock_instance
|
|
|
|
# First run - initial feed with 2 articles
|
|
mock_feed = Mock()
|
|
mock_feed.entries = [
|
|
{"title": "Article 1", "link": "https://example.com/1"},
|
|
{"title": "Article 2", "link": "https://example.com/2"},
|
|
]
|
|
mock_parse.return_value = mock_feed
|
|
|
|
bot = MastodonRSSBot(**self.test_config)
|
|
count1 = bot.process_new_entries()
|
|
self.assertEqual(count1, 2)
|
|
|
|
# Second run - updated feed with 1 new article
|
|
mock_feed.entries = [
|
|
{"title": "Article 3", "link": "https://example.com/3"},
|
|
{"title": "Article 2", "link": "https://example.com/2"},
|
|
{"title": "Article 1", "link": "https://example.com/1"},
|
|
]
|
|
|
|
# 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)
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_network_error_handling(self, mock_mastodon, mock_parse):
|
|
"""Test handling of network errors when fetching feed"""
|
|
# Simulate network error by returning None
|
|
mock_parse.return_value = None
|
|
|
|
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)
|
|
|
|
@patch("bot.feedparser.parse")
|
|
@patch("bot.Mastodon")
|
|
def test_malformed_xml_handling(self, mock_mastodon, mock_parse):
|
|
"""Test handling of malformed XML feeds"""
|
|
# Create mock feed with bozo_exception (feedparser's error indicator)
|
|
mock_feed = Mock()
|
|
mock_feed.entries = []
|
|
mock_feed.bozo_exception = Exception("XML parsing error")
|
|
mock_parse.return_value = mock_feed
|
|
|
|
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()
|