From 15409ff4e17feaa2f235b22672a45b7837784461 Mon Sep 17 00:00:00 2001 From: aserper Date: Fri, 12 Dec 2025 23:43:07 -0500 Subject: [PATCH] Refactor integration tests to use feedparser mocking 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. --- requirements-test.txt | 3 - test_integration.py | 205 +++++++++++------------------------------- 2 files changed, 50 insertions(+), 158 deletions(-) diff --git a/requirements-test.txt b/requirements-test.txt index 1346600..4c7875f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -9,9 +9,6 @@ pytest==7.4.4 pytest-cov==4.1.0 pytest-mock==3.12.0 -# HTTP mocking for integration tests -responses==0.24.1 - # Code quality flake8==7.0.0 black==24.1.1 diff --git a/test_integration.py b/test_integration.py index 2c5a367..dfe04f1 100644 --- a/test_integration.py +++ b/test_integration.py @@ -4,10 +4,7 @@ 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): @@ -31,37 +28,17 @@ class TestRSSFeedIntegration(unittest.TestCase): if os.path.exists(self.test_config["state_file"]): os.remove(self.test_config["state_file"]) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_end_to_end_rss_to_mastodon(self, mock_mastodon): + def test_end_to_end_rss_to_mastodon(self, mock_mastodon, mock_parse): """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.encode("utf-8"), - status=200, - content_type="application/xml; charset=utf-8", - ) + # 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() @@ -82,31 +59,16 @@ class TestRSSFeedIntegration(unittest.TestCase): self.assertIn("Second Article", calls[1][0][0]) self.assertIn("https://example.com/article2", calls[1][0][0]) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_atom_feed_parsing(self, mock_mastodon): + def test_atom_feed_parsing(self, mock_mastodon, mock_parse): """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.encode("utf-8"), - status=200, - content_type="application/atom+xml; charset=utf-8", - ) + # 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 @@ -118,28 +80,14 @@ class TestRSSFeedIntegration(unittest.TestCase): calls = mock_instance.status_post.call_args_list self.assertIn("Atom Article", calls[0][0][0]) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_persistence_across_runs(self, mock_mastodon): + def test_persistence_across_runs(self, mock_mastodon, mock_parse): """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.encode("utf-8"), - status=200, - content_type="application/xml; charset=utf-8", - ) + # 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 @@ -157,70 +105,31 @@ class TestRSSFeedIntegration(unittest.TestCase): # Total posts should be 1 self.assertEqual(mock_instance.status_post.call_count, 1) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_incremental_feed_updates(self, mock_mastodon): + def test_incremental_feed_updates(self, mock_mastodon, mock_parse): """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 + # 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) - # 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 - 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() @@ -229,16 +138,12 @@ class TestRSSFeedIntegration(unittest.TestCase): # Verify only 3 total posts self.assertEqual(mock_instance.status_post.call_count, 3) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_network_error_handling(self, mock_mastodon): + def test_network_error_handling(self, mock_mastodon, mock_parse): """Test handling of network errors when fetching feed""" - responses.add( - responses.GET, - "https://example.com/feed.xml", - body="Network error", - status=500, - ) + # Simulate network error by returning None + mock_parse.return_value = None mock_instance = Mock() mock_mastodon.return_value = mock_instance @@ -250,25 +155,15 @@ class TestRSSFeedIntegration(unittest.TestCase): self.assertEqual(count, 0) self.assertEqual(mock_instance.status_post.call_count, 0) - @responses.activate + @patch("bot.feedparser.parse") @patch("bot.Mastodon") - def test_malformed_xml_handling(self, mock_mastodon): + def test_malformed_xml_handling(self, mock_mastodon, mock_parse): """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", - ) + # 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