diff --git a/tests/test_processors/test_download.py b/tests/test_processors/test_download.py index 63860db..dc1f0e3 100644 --- a/tests/test_processors/test_download.py +++ b/tests/test_processors/test_download.py @@ -125,3 +125,189 @@ class TestDownloadOutputSchema: ) assert output.success is False assert output.error_message == "Network error" + + +class TestDownloadProcessorMetadata: + """Tests for DownloadProcessor metadata fetching.""" + + @pytest.fixture + def mock_bilibili_adapter(self): + """Create a mock BilibiliAdapter.""" + adapter = MagicMock() + adapter.fetch_metadata = AsyncMock(return_value={ + "metadata": { + "title": "Test Video Title", + "mid": 12345, + "bvid": "BV1xx", + }, + "stats": { + "view": 1000, + "like": 100, + }, + "description": "Test description" + }) + adapter.get_output_filename = MagicMock( + return_value="bilibili_BV1xx_metadata.json" + ) + return adapter + + @pytest.fixture + def mock_zhihu_adapter(self): + """Create a mock ZhihuAdapter.""" + adapter = MagicMock() + adapter.fetch_metadata = AsyncMock(return_value={ + "title": "Test Answer Title", + "author": "Test Author", + "url": "https://www.zhihu.com/question/123/answer/456", + "content_markdown": "Test content" + }) + adapter.get_output_filename = MagicMock( + return_value="zhihu_456_metadata.json" + ) + return adapter + + @pytest.fixture + def mock_state_manager(self): + """Create a mock WorkflowStateManager.""" + manager = MagicMock() + manager.load = MagicMock(return_value=None) + manager.save = MagicMock() + return manager + + @pytest.mark.asyncio + async def test_metadata_fetches_bilibili_video_metadata_correctly( + self, mock_bilibili_adapter, mock_state_manager, tmp_path + ): + """Test 1: DownloadProcessor fetches Bilibili video metadata correctly""" + from kb_pipeline.processors.download import DownloadProcessor + from kb_pipeline.schemas import DownloadInput + + input_data = DownloadInput( + content_id="BV1xx", + platform="bilibili", + content_dir=str(tmp_path), + content_type="video" + ) + + with patch.object( + DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter + ): + processor = DownloadProcessor(state_manager=mock_state_manager) + result = await processor.process(input_data) + + assert result.success is True + assert result.title == "Test Video Title" + assert "bilibili_BV1xx_metadata.json" in result.metadata_file + + # Verify adapter was called correctly + mock_bilibili_adapter.fetch_metadata.assert_called_once_with( + "BV1xx", "video" + ) + + @pytest.mark.asyncio + async def test_metadata_fetches_zhihu_answer_metadata_correctly( + self, mock_zhihu_adapter, mock_state_manager, tmp_path + ): + """Test 2: DownloadProcessor fetches Zhihu answer metadata correctly""" + from kb_pipeline.processors.download import DownloadProcessor + from kb_pipeline.schemas import DownloadInput + + input_data = DownloadInput( + content_id="456", + platform="zhihu", + content_dir=str(tmp_path), + content_type="answer" + ) + + with patch.object( + DownloadProcessor, '_get_adapter', return_value=mock_zhihu_adapter + ): + processor = DownloadProcessor(state_manager=mock_state_manager) + result = await processor.process(input_data) + + assert result.success is True + assert result.title == "Test Answer Title" + + # Verify adapter was called correctly + mock_zhihu_adapter.fetch_metadata.assert_called_once_with( + "456", "answer" + ) + + @pytest.mark.asyncio + async def test_metadata_records_state_on_success( + self, mock_bilibili_adapter, mock_state_manager, tmp_path + ): + """Test 3: DownloadProcessor records state on success""" + from kb_pipeline.processors.download import DownloadProcessor + from kb_pipeline.schemas import DownloadInput, WorkflowState + + # Create a mock state that will be updated + mock_state = MagicMock(spec=WorkflowState) + mock_state.is_step_completed = MagicMock(return_value=False) + mock_state_manager.load.return_value = mock_state + + input_data = DownloadInput( + content_id="BV1xx", + platform="bilibili", + content_dir=str(tmp_path), + content_type="video" + ) + + with patch.object( + DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter + ): + processor = DownloadProcessor(state_manager=mock_state_manager) + result = await processor.process(input_data) + + assert result.success is True + + # Verify state was recorded + mock_state.mark_step_completed.assert_called_once() + call_args = mock_state.mark_step_completed.call_args + assert call_args[0][0] == "download" # step_name + mock_state_manager.save.assert_called_once_with(mock_state) + + @pytest.mark.asyncio + async def test_metadata_records_state_on_failure( + self, mock_bilibili_adapter, mock_state_manager, tmp_path + ): + """Test 4: DownloadProcessor records state on failure""" + from kb_pipeline.processors.download import DownloadProcessor + from kb_pipeline.schemas import DownloadInput, WorkflowState + from kb_pipeline.errors import NetworkError + + # Make adapter raise an error + mock_bilibili_adapter.fetch_metadata = AsyncMock( + side_effect=NetworkError( + step_name="download", + input_summary="BV1xx", + cause=Exception("Network timeout") + ) + ) + + # Create a mock state that will be updated + mock_state = MagicMock(spec=WorkflowState) + mock_state.is_step_completed = MagicMock(return_value=False) + mock_state_manager.load.return_value = mock_state + + input_data = DownloadInput( + content_id="BV1xx", + platform="bilibili", + content_dir=str(tmp_path), + content_type="video" + ) + + with patch.object( + DownloadProcessor, '_get_adapter', return_value=mock_bilibili_adapter + ): + processor = DownloadProcessor(state_manager=mock_state_manager) + result = await processor.process(input_data) + + assert result.success is False + assert "Network timeout" in result.error_message + + # Verify failure was recorded + mock_state.mark_step_failed.assert_called_once() + call_args = mock_state.mark_step_failed.call_args + assert call_args[0][0] == "download" # step_name + mock_state_manager.save.assert_called_once_with(mock_state)