"""Tests for DownloadProcessor and DownloadInput/DownloadOutput schemas. Test coverage: - DownloadInput schema validation - DownloadOutput schema validation - DownloadProcessor metadata fetching - DownloadProcessor content downloading - DownloadProcessor state recording """ import pytest from datetime import datetime from unittest.mock import AsyncMock, MagicMock, patch from pathlib import Path from kb_pipeline.schemas import DownloadInput, DownloadOutput class TestDownloadInputSchema: """Tests for DownloadInput schema validation.""" def test_schema_download_input_validates_required_fields(self): """Test 1: DownloadInput validates required fields""" # Test with all required fields input_data = DownloadInput( content_id="BV1xx", platform="bilibili", content_dir="/tmp/content", content_type="video" ) assert input_data.content_id == "BV1xx" assert input_data.platform == "bilibili" assert input_data.content_dir == "/tmp/content" assert input_data.content_type == "video" def test_schema_download_input_requires_content_dir(self): """DownloadInput should require content_dir""" with pytest.raises(Exception): # ValidationError from pydantic DownloadInput( content_id="BV1xx", platform="bilibili", content_type="video" # Missing content_dir ) def test_schema_download_input_requires_content_type(self): """DownloadInput should require content_type""" with pytest.raises(Exception): # ValidationError from pydantic DownloadInput( content_id="BV1xx", platform="bilibili", content_dir="/tmp/content" # Missing content_type ) class TestDownloadOutputSchema: """Tests for DownloadOutput schema validation.""" def test_schema_download_output_validates_required_fields(self): """Test 2: DownloadOutput validates required fields""" output = DownloadOutput( success=True, metadata_file="/tmp/content/bilibili_BV1xx_metadata.json", content_file="/tmp/content/bilibili_BV1xx_content.md", title="Test Video", author="Test Author" ) assert output.success is True assert output.metadata_file == "/tmp/content/bilibili_BV1xx_metadata.json" assert output.content_file == "/tmp/content/bilibili_BV1xx_content.md" assert output.title == "Test Video" assert output.author == "Test Author" assert output.duration is None # Optional field def test_schema_download_output_handles_video_content_type(self): """Test 3: DownloadOutput handles video content type with duration""" output = DownloadOutput( success=True, metadata_file="/tmp/content/bilibili_BV1xx_metadata.json", content_file="", # Video content handled by ASR title="Test Video", author="Test Author", duration=300 # 5 minutes ) assert output.success is True assert output.duration == 300 assert output.content_file == "" def test_schema_download_output_handles_article_content_type(self): """Test 3: DownloadOutput handles article content type without duration""" output = DownloadOutput( success=True, metadata_file="/tmp/content/bilibili_CV123_metadata.json", content_file="/tmp/content/bilibili_CV123_content.md", title="Test Article", author="Test Author", duration=None # Articles don't have duration ) assert output.success is True assert output.duration is None assert output.content_file == "/tmp/content/bilibili_CV123_content.md" def test_schema_download_output_handles_zhihu_answer(self): """Test 3: DownloadOutput handles Zhihu answer content type""" output = DownloadOutput( success=True, metadata_file="/tmp/content/zhihu_123456_metadata.json", content_file="/tmp/content/zhihu_123456_content.md", title="Test Answer", author="Test Author" ) assert output.success is True assert "zhihu" in output.metadata_file def test_schema_download_output_handles_failure(self): """DownloadOutput should handle failure case""" output = DownloadOutput( success=False, error_message="Network error", metadata_file="", content_file="", title="", author="" ) 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)