""" Tests for engine.fetch module. """ import json from unittest.mock import MagicMock, patch from engine.fetch import ( _fetch_gutenberg, fetch_all, fetch_feed, fetch_poetry, load_cache, save_cache, ) class TestFetchFeed: """Tests for fetch_feed function.""" @patch("engine.fetch.urllib.request.urlopen") def test_fetch_success(self, mock_urlopen): """Successful feed fetch returns parsed feed.""" mock_response = MagicMock() mock_response.read.return_value = b"test" mock_urlopen.return_value = mock_response result = fetch_feed("http://example.com/feed") assert result is not None @patch("engine.fetch.urllib.request.urlopen") def test_fetch_network_error(self, mock_urlopen): """Network error returns tuple with None feed.""" mock_urlopen.side_effect = Exception("Network error") url, feed = fetch_feed("http://example.com/feed") assert feed is None class TestFetchAll: """Tests for fetch_all function.""" @patch("engine.fetch.fetch_feed") @patch("engine.fetch.strip_tags") @patch("engine.fetch.skip") @patch("engine.fetch.boot_ln") def test_fetch_all_success(self, mock_boot, mock_skip, mock_strip, mock_fetch_feed): """Successful fetch returns items.""" mock_feed = MagicMock() mock_feed.bozo = False mock_feed.entries = [ {"title": "Headline 1", "published_parsed": (2024, 1, 1, 12, 0, 0)}, {"title": "Headline 2", "updated_parsed": (2024, 1, 2, 12, 0, 0)}, ] mock_fetch_feed.return_value = ("http://example.com", mock_feed) mock_skip.return_value = False mock_strip.side_effect = lambda x: x items, linked, failed = fetch_all() assert linked > 0 assert failed == 0 @patch("engine.fetch.fetch_feed") @patch("engine.fetch.boot_ln") def test_fetch_all_feed_error(self, mock_boot, mock_fetch_feed): """Feed error increments failed count.""" mock_fetch_feed.return_value = ("http://example.com", None) items, linked, failed = fetch_all() assert failed > 0 @patch("engine.fetch.fetch_feed") @patch("engine.fetch.strip_tags") @patch("engine.fetch.skip") @patch("engine.fetch.boot_ln") def test_fetch_all_skips_filtered( self, mock_boot, mock_skip, mock_strip, mock_fetch_feed ): """Filtered headlines are skipped.""" mock_feed = MagicMock() mock_feed.bozo = False mock_feed.entries = [ {"title": "Sports scores"}, {"title": "Valid headline"}, ] mock_fetch_feed.return_value = ("http://example.com", mock_feed) mock_skip.side_effect = lambda x: x == "Sports scores" mock_strip.side_effect = lambda x: x items, linked, failed = fetch_all() assert any("Valid headline" in item[0] for item in items) class TestFetchGutenberg: """Tests for _fetch_gutenberg function.""" @patch("engine.fetch.urllib.request.urlopen") def test_gutenberg_success(self, mock_urlopen): """Successful gutenberg fetch returns items.""" text = """Project Gutenberg *** START OF THE PROJECT GUTENBERG *** This is a test poem with multiple lines that should be parsed as a block. Another stanza with more content here. *** END OF THE PROJECT GUTENBERG *** """ mock_response = MagicMock() mock_response.read.return_value = text.encode("utf-8") mock_urlopen.return_value = mock_response result = _fetch_gutenberg("http://example.com/test", "Test") assert len(result) > 0 @patch("engine.fetch.urllib.request.urlopen") def test_gutenberg_network_error(self, mock_urlopen): """Network error returns empty list.""" mock_urlopen.side_effect = Exception("Network error") result = _fetch_gutenberg("http://example.com/test", "Test") assert result == [] @patch("engine.fetch.urllib.request.urlopen") def test_gutenberg_skips_short_blocks(self, mock_urlopen): """Blocks shorter than 20 chars are skipped.""" text = """*** START OF THE *** Short *** END OF THE *** """ mock_response = MagicMock() mock_response.read.return_value = text.encode("utf-8") mock_urlopen.return_value = mock_response result = _fetch_gutenberg("http://example.com/test", "Test") assert result == [] @patch("engine.fetch.urllib.request.urlopen") def test_gutenberg_skips_all_caps_headers(self, mock_urlopen): """All-caps lines are skipped as headers.""" text = """*** START OF THE *** THIS IS ALL CAPS HEADER more content here *** END OF THE *** """ mock_response = MagicMock() mock_response.read.return_value = text.encode("utf-8") mock_urlopen.return_value = mock_response result = _fetch_gutenberg("http://example.com/test", "Test") assert len(result) > 0 class TestFetchPoetry: """Tests for fetch_poetry function.""" @patch("engine.fetch._fetch_gutenberg") @patch("engine.fetch.boot_ln") def test_fetch_poetry_success(self, mock_boot, mock_fetch): """Successful poetry fetch returns items.""" mock_fetch.return_value = [ ("Stanza 1 content here", "Test", ""), ("Stanza 2 content here", "Test", ""), ] items, linked, failed = fetch_poetry() assert linked > 0 assert failed == 0 @patch("engine.fetch._fetch_gutenberg") @patch("engine.fetch.boot_ln") def test_fetch_poetry_failure(self, mock_boot, mock_fetch): """Failed fetch increments failed count.""" mock_fetch.return_value = [] items, linked, failed = fetch_poetry() assert failed > 0 class TestCache: """Tests for cache functions.""" @patch("engine.fetch._cache_path") def test_load_cache_success(self, mock_path): """Successful cache load returns items.""" mock_path.return_value.__str__ = MagicMock(return_value="/tmp/cache") mock_path.return_value.exists.return_value = True mock_path.return_value.read_text.return_value = json.dumps( {"items": [("title", "source", "time")]} ) result = load_cache() assert result is not None @patch("engine.fetch._cache_path") def test_load_cache_missing_file(self, mock_path): """Missing cache file returns None.""" mock_path.return_value.exists.return_value = False result = load_cache() assert result is None @patch("engine.fetch._cache_path") def test_load_cache_invalid_json(self, mock_path): """Invalid JSON returns None.""" mock_path.return_value.exists.return_value = True mock_path.return_value.read_text.side_effect = json.JSONDecodeError("", "", 0) result = load_cache() assert result is None @patch("engine.fetch._cache_path") def test_save_cache_success(self, mock_path): """Save cache writes to file.""" mock_path.return_value.__truediv__ = MagicMock( return_value=mock_path.return_value ) save_cache([("title", "source", "time")])