feat(websocket): add WebSocket display backend for browser client
This commit is contained in:
234
tests/test_fetch.py
Normal file
234
tests/test_fetch.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
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"<rss>test</rss>"
|
||||
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 None."""
|
||||
mock_urlopen.side_effect = Exception("Network error")
|
||||
|
||||
result = fetch_feed("http://example.com/feed")
|
||||
|
||||
assert result 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 = 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 = 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 = 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")])
|
||||
Reference in New Issue
Block a user