diff --git a/tests/test_data_sources.py b/tests/test_data_sources.py new file mode 100644 index 0000000..8d94404 --- /dev/null +++ b/tests/test_data_sources.py @@ -0,0 +1,220 @@ +""" +Tests for engine/data_sources/sources.py - data source implementations. + +Tests HeadlinesDataSource, PoetryDataSource, EmptyDataSource, and the +base DataSource class functionality. +""" + +from unittest.mock import patch + +import pytest + +from engine.data_sources.sources import ( + EmptyDataSource, + HeadlinesDataSource, + PoetryDataSource, + SourceItem, +) + + +class TestSourceItem: + """Test SourceItem dataclass.""" + + def test_source_item_creation(self): + """SourceItem can be created with required fields.""" + item = SourceItem( + content="Test headline", + source="test_source", + timestamp="2024-01-01", + ) + assert item.content == "Test headline" + assert item.source == "test_source" + assert item.timestamp == "2024-01-01" + assert item.metadata is None + + def test_source_item_with_metadata(self): + """SourceItem can include optional metadata.""" + metadata = {"author": "John", "category": "tech"} + item = SourceItem( + content="Test", + source="test", + timestamp="2024-01-01", + metadata=metadata, + ) + assert item.metadata == metadata + + +class TestEmptyDataSource: + """Test EmptyDataSource.""" + + def test_empty_source_name(self): + """EmptyDataSource has correct name.""" + source = EmptyDataSource() + assert source.name == "empty" + + def test_empty_source_is_not_dynamic(self): + """EmptyDataSource is static, not dynamic.""" + source = EmptyDataSource() + assert source.is_dynamic is False + + def test_empty_source_fetch_returns_blank_content(self): + """EmptyDataSource.fetch() returns blank lines.""" + source = EmptyDataSource(width=80, height=24) + items = source.fetch() + + assert len(items) == 1 + assert isinstance(items[0], SourceItem) + assert items[0].source == "empty" + # Content should be 24 lines of 80 spaces + lines = items[0].content.split("\n") + assert len(lines) == 24 + assert all(len(line) == 80 for line in lines) + + def test_empty_source_get_items_caches_result(self): + """EmptyDataSource.get_items() caches the result.""" + source = EmptyDataSource() + items1 = source.get_items() + items2 = source.get_items() + # Should return same cached items (same object reference) + assert items1 is items2 + + +class TestHeadlinesDataSource: + """Test HeadlinesDataSource.""" + + def test_headlines_source_name(self): + """HeadlinesDataSource has correct name.""" + source = HeadlinesDataSource() + assert source.name == "headlines" + + def test_headlines_source_is_static(self): + """HeadlinesDataSource is static.""" + source = HeadlinesDataSource() + assert source.is_dynamic is False + + def test_headlines_fetch_returns_source_items(self): + """HeadlinesDataSource.fetch() returns SourceItem list.""" + mock_items = [ + ("Test Article 1", "source1", "10:30"), + ("Test Article 2", "source2", "11:45"), + ] + with patch("engine.fetch.fetch_all") as mock_fetch_all: + mock_fetch_all.return_value = (mock_items, 2, 0) + + source = HeadlinesDataSource() + items = source.fetch() + + assert len(items) == 2 + assert all(isinstance(item, SourceItem) for item in items) + assert items[0].content == "Test Article 1" + assert items[0].source == "source1" + assert items[0].timestamp == "10:30" + + def test_headlines_fetch_with_empty_feed(self): + """HeadlinesDataSource handles empty feeds gracefully.""" + with patch("engine.fetch.fetch_all") as mock_fetch_all: + mock_fetch_all.return_value = ([], 0, 1) + + source = HeadlinesDataSource() + items = source.fetch() + + # Should return empty list + assert isinstance(items, list) + assert len(items) == 0 + + def test_headlines_get_items_caches_result(self): + """HeadlinesDataSource.get_items() caches the result.""" + mock_items = [("Test Article", "source", "12:00")] + with patch("engine.fetch.fetch_all") as mock_fetch_all: + mock_fetch_all.return_value = (mock_items, 1, 0) + + source = HeadlinesDataSource() + items1 = source.get_items() + items2 = source.get_items() + + # Should only call fetch once (cached) + assert mock_fetch_all.call_count == 1 + assert items1 is items2 + + def test_headlines_refresh_clears_cache(self): + """HeadlinesDataSource.refresh() clears cache and refetches.""" + mock_items = [("Test Article", "source", "12:00")] + with patch("engine.fetch.fetch_all") as mock_fetch_all: + mock_fetch_all.return_value = (mock_items, 1, 0) + + source = HeadlinesDataSource() + source.get_items() + source.refresh() + source.get_items() + + # Should call fetch twice (once for initial, once for refresh) + assert mock_fetch_all.call_count == 2 + + +class TestPoetryDataSource: + """Test PoetryDataSource.""" + + def test_poetry_source_name(self): + """PoetryDataSource has correct name.""" + source = PoetryDataSource() + assert source.name == "poetry" + + def test_poetry_source_is_static(self): + """PoetryDataSource is static.""" + source = PoetryDataSource() + assert source.is_dynamic is False + + def test_poetry_fetch_returns_source_items(self): + """PoetryDataSource.fetch() returns SourceItem list.""" + mock_items = [ + ("Poetry line 1", "Poetry Source 1", ""), + ("Poetry line 2", "Poetry Source 2", ""), + ] + with patch("engine.fetch.fetch_poetry") as mock_fetch_poetry: + mock_fetch_poetry.return_value = (mock_items, 2, 0) + + source = PoetryDataSource() + items = source.fetch() + + assert len(items) == 2 + assert all(isinstance(item, SourceItem) for item in items) + assert items[0].content == "Poetry line 1" + assert items[0].source == "Poetry Source 1" + + def test_poetry_get_items_caches_result(self): + """PoetryDataSource.get_items() caches result.""" + mock_items = [("Poetry line", "Poetry Source", "")] + with patch("engine.fetch.fetch_poetry") as mock_fetch_poetry: + mock_fetch_poetry.return_value = (mock_items, 1, 0) + + source = PoetryDataSource() + items1 = source.get_items() + items2 = source.get_items() + + # Should only fetch once (cached) + assert mock_fetch_poetry.call_count == 1 + assert items1 is items2 + + +class TestDataSourceInterface: + """Test DataSource base class interface.""" + + def test_data_source_stream_not_implemented(self): + """DataSource.stream() raises NotImplementedError.""" + source = EmptyDataSource() + with pytest.raises(NotImplementedError): + source.stream() + + def test_data_source_is_dynamic_defaults_false(self): + """DataSource.is_dynamic defaults to False.""" + source = EmptyDataSource() + assert source.is_dynamic is False + + def test_data_source_refresh_updates_cache(self): + """DataSource.refresh() updates internal cache.""" + source = EmptyDataSource() + source.get_items() + items_refreshed = source.refresh() + + # refresh() should return new items + assert isinstance(items_refreshed, list)