import tempfile from pathlib import Path from unittest.mock import AsyncMock, Mock, patch import pytest from app.library.Tasks import Task, Tasks class TestTask: """Test the Task dataclass.""" def test_task_creation_with_required_fields(self): """Test creating a task with only required fields.""" task = Task(id="test_id", name="test_task", url="https://example.com/video") assert task.id == "test_id" assert task.name == "test_task" assert task.url == "https://example.com/video" assert task.folder == "" assert task.preset == "" assert task.timer == "" assert task.template == "" assert task.cli == "" assert task.auto_start is True assert task.handler_enabled is True def test_task_creation_with_all_fields(self): """Test creating a task with all fields specified.""" task = Task( id="test_id", name="test_task", url="https://example.com/video", folder="test_folder", preset="test_preset", timer="0 */6 * * *", template="%(title)s.%(ext)s", cli="--extract-flat", auto_start=False, handler_enabled=False, ) assert task.id == "test_id" assert task.name == "test_task" assert task.url == "https://example.com/video" assert task.folder == "test_folder" assert task.preset == "test_preset" assert task.timer == "0 */6 * * *" assert task.template == "%(title)s.%(ext)s" assert task.cli == "--extract-flat" assert task.auto_start is False assert task.handler_enabled is False def test_task_serialize(self): """Test task serialization to dictionary.""" task = Task( id="test_id", name="test_task", url="https://example.com/video", preset="test_preset", ) serialized = task.serialize() assert isinstance(serialized, dict) assert serialized["id"] == "test_id" assert serialized["name"] == "test_task" assert serialized["url"] == "https://example.com/video" assert serialized["preset"] == "test_preset" assert serialized["folder"] == "" assert serialized["auto_start"] is True @patch("app.library.Tasks.Encoder") def test_task_json(self, mock_encoder): """Test task JSON serialization.""" mock_encoder_instance = Mock() mock_encoder_instance.encode.return_value = '{"id": "test_id"}' mock_encoder.return_value = mock_encoder_instance task = Task(id="test_id", name="test_task", url="https://example.com/video") json_str = task.json() assert json_str == '{"id": "test_id"}' mock_encoder_instance.encode.assert_called_once() def test_task_get_method(self): """Test task get method for accessing fields.""" task = Task( id="test_id", name="test_task", url="https://example.com/video", preset="test_preset", ) assert task.get("id") == "test_id" assert task.get("name") == "test_task" assert task.get("preset") == "test_preset" assert task.get("nonexistent") is None assert task.get("nonexistent", "default_value") == "default_value" @patch("app.library.Tasks.YTDLPOpts") def test_task_get_ytdlp_opts_without_preset_or_cli(self, mock_ytdlp_opts): """Test getting YTDLPOpts without preset or CLI options.""" mock_opts_instance = Mock() mock_ytdlp_opts.get_instance.return_value = mock_opts_instance task = Task(id="test_id", name="test_task", url="https://example.com/video") result = task.get_ytdlp_opts() assert result == mock_opts_instance mock_ytdlp_opts.get_instance.assert_called_once() @patch("app.library.Tasks.YTDLPOpts") def test_task_get_ytdlp_opts_with_preset_and_cli(self, mock_ytdlp_opts): """Test getting YTDLPOpts with preset and CLI options.""" mock_opts_instance = Mock() mock_preset_opts = Mock() mock_final_opts = Mock() mock_opts_instance.preset.return_value = mock_preset_opts mock_preset_opts.add_cli.return_value = mock_final_opts mock_ytdlp_opts.get_instance.return_value = mock_opts_instance task = Task( id="test_id", name="test_task", url="https://example.com/video", preset="test_preset", cli="--extract-flat", ) result = task.get_ytdlp_opts() assert result == mock_final_opts mock_opts_instance.preset.assert_called_once_with(name="test_preset") mock_preset_opts.add_cli.assert_called_once_with("--extract-flat", from_user=True) @patch("app.library.Tasks.archive_add") @patch("app.library.Tasks.extract_info") def test_task_mark_success(self, mock_extract_info, mock_archive_add): """Test successful task marking.""" # Mock extract_info to return playlist data mock_extract_info.return_value = { "_type": "playlist", "entries": [ { "_type": "video", "id": "video1", "ie_key": "Youtube", }, { "_type": "video", "id": "video2", "ie_key": "Youtube", }, ], } mock_archive_add.return_value = True with patch.object(Task, "get_ytdlp_opts") as mock_get_opts: mock_get_opts.return_value.get_all.return_value = {"download_archive": "/tmp/archive.txt"} task = Task(id="test_id", name="test_task", url="https://example.com/playlist") success, message = task.mark() assert success is True assert "marked as downloaded" in message mock_archive_add.assert_called_once() # Check that archive_add was called with the correct items call_args = mock_archive_add.call_args[0] archive_file = call_args[0] items = call_args[1] assert str(archive_file) == "/tmp/archive.txt" assert "youtube video1" in items assert "youtube video2" in items def test_task_mark_no_url(self): """Test task marking with no URL.""" task = Task(id="test_id", name="test_task", url="") success, message = task.mark() assert success is False assert "No URL found" in message def test_task_mark_no_archive_file(self): """Test task marking with no archive file configured.""" with patch.object(Task, "get_ytdlp_opts") as mock_get_opts: mock_get_opts.return_value.get_all.return_value = {} task = Task(id="test_id", name="test_task", url="https://example.com/video") success, message = task.mark() assert success is False assert "No archive file found" in message @patch("app.library.Tasks.archive_delete") @patch("app.library.Tasks.extract_info") def test_task_unmark_success(self, mock_extract_info, mock_archive_delete): """Test successful task unmarking.""" # Mock extract_info to return playlist data mock_extract_info.return_value = { "_type": "playlist", "entries": [ { "_type": "video", "id": "video1", "ie_key": "Youtube", }, ], } mock_archive_delete.return_value = True with patch.object(Task, "get_ytdlp_opts") as mock_get_opts: mock_get_opts.return_value.get_all.return_value = {"download_archive": "/tmp/archive.txt"} task = Task(id="test_id", name="test_task", url="https://example.com/playlist") success, message = task.unmark() assert success is True assert "Removed" in message assert "items from archive file" in message mock_archive_delete.assert_called_once() @patch("app.library.Tasks.extract_info") def test_task_mark_logic_invalid_extract_info(self, mock_extract_info): """Test _mark_logic with invalid extract_info response.""" mock_extract_info.return_value = None with patch.object(Task, "get_ytdlp_opts") as mock_get_opts: mock_get_opts.return_value.get_all.return_value = {"download_archive": "/tmp/archive.txt"} task = Task(id="test_id", name="test_task", url="https://example.com/video") result = task._mark_logic() assert isinstance(result, tuple) success, message = result assert success is False assert "Failed to extract information" in message @patch("app.library.Tasks.extract_info") def test_task_mark_logic_not_playlist(self, mock_extract_info): """Test _mark_logic with non-playlist type.""" mock_extract_info.return_value = { "_type": "video", "id": "video1", } with patch.object(Task, "get_ytdlp_opts") as mock_get_opts: mock_get_opts.return_value.get_all.return_value = {"download_archive": "/tmp/archive.txt"} task = Task(id="test_id", name="test_task", url="https://example.com/video") result = task._mark_logic() assert isinstance(result, tuple) success, message = result assert success is False assert "Expected a playlist type" in message class TestTasks: """Test the Tasks singleton manager.""" def setup_method(self): """Set up test environment before each test.""" # Reset singleton instance to ensure clean state Tasks._reset_singleton() def teardown_method(self): """Clean up after each test.""" # Reset singleton instance to prevent test pollution Tasks._reset_singleton() @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_singleton(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test that Tasks follows singleton pattern.""" mock_config.get_instance.return_value = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() instance1 = Tasks.get_instance() instance2 = Tasks.get_instance() assert instance1 is instance2 assert isinstance(instance1, Tasks) @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_initialization(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test Tasks initialization with proper dependencies.""" mock_config_instance = Mock( debug=True, default_preset="test_preset", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus_instance = Mock() mock_eventbus.get_instance.return_value = mock_eventbus_instance mock_scheduler_instance = Mock() mock_scheduler.get_instance.return_value = mock_scheduler_instance mock_download_queue_instance = Mock() mock_download_queue.get_instance.return_value = mock_download_queue_instance with tempfile.TemporaryDirectory() as temp_dir: tasks_file = Path(temp_dir) / "tasks.json" tasks = Tasks(file=tasks_file, config=mock_config_instance) # Check initialization assert tasks._debug is True assert tasks._default_preset == "test_preset" assert tasks._file == tasks_file assert tasks._tasks == [] assert tasks._scheduler == mock_scheduler_instance assert tasks._notify == mock_eventbus_instance assert tasks._downloadQueue == mock_download_queue_instance @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_get_all_empty(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test getting all tasks when no tasks exist.""" mock_config.get_instance.return_value = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() tasks = Tasks.get_instance() result = tasks.get_all() assert result == [] assert isinstance(result, list) @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_get_by_id_not_found(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test getting task by ID when task doesn't exist.""" mock_config.get_instance.return_value = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() tasks = Tasks.get_instance() result = tasks.get("nonexistent_id") assert result is None @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_load_empty_file(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test loading tasks from empty or non-existent file.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: tasks_file = Path(temp_dir) / "nonexistent.json" tasks = Tasks(file=tasks_file, config=mock_config_instance) result = tasks.load() assert result == tasks # Should return self assert tasks.get_all() == [] @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_load_valid_file(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test loading tasks from valid JSON file.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler_instance = Mock() mock_scheduler.get_instance.return_value = mock_scheduler_instance mock_download_queue.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: tasks_file = Path(temp_dir) / "tasks.json" # Create valid JSON file with task data task_data = [ { "id": "task1", "name": "Test Task 1", "url": "https://example.com/video1", "preset": "default", "timer": "0 6 * * *", "folder": "", "template": "", "cli": "", "auto_start": True, "handler_enabled": True, } ] tasks_file.write_text("[\n" + "\n".join(f" {line}" for line in str(task_data).split("\n")) + "\n]") # Mock json.loads to return our test data with patch("json.loads", return_value=task_data): tasks = Tasks(file=tasks_file, config=mock_config_instance) # Mock the scheduler.add method mock_scheduler_instance.add = Mock() result = tasks.load() assert result == tasks loaded_tasks = tasks.get_all() assert len(loaded_tasks) == 1 assert loaded_tasks[0].id == "task1" assert loaded_tasks[0].name == "Test Task 1" assert loaded_tasks[0].url == "https://example.com/video1" @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_load_invalid_json(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test loading tasks from file with invalid JSON.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: tasks_file = Path(temp_dir) / "tasks.json" tasks_file.write_text("invalid json {") tasks = Tasks(file=tasks_file, config=mock_config_instance) result = tasks.load() assert result == tasks assert tasks.get_all() == [] # Should be empty due to invalid JSON @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_save_valid_tasks(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test saving valid tasks to file.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() with tempfile.TemporaryDirectory() as temp_dir: tasks_file = Path(temp_dir) / "tasks.json" tasks = Tasks(file=tasks_file, config=mock_config_instance) task_data = [ { "id": "task1", "name": "Test Task", "url": "https://example.com/video", "preset": "default", "folder": "", "template": "", "cli": "", "timer": "", "auto_start": True, "handler_enabled": True, } ] result = tasks.save(task_data) assert result == tasks assert tasks_file.exists() # Verify file content was written assert tasks_file.stat().st_size > 0 @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_get_by_id_found(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test getting task by ID when task exists.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() tasks = Tasks(file=None, config=mock_config_instance) # Manually add a task to the internal list test_task = Task(id="test_id", name="Test Task", url="https://example.com/video") tasks._tasks.append(test_task) result = tasks.get("test_id") assert result is not None assert result.id == "test_id" assert result.name == "Test Task" @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_get_all_with_tasks(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test getting all tasks when tasks exist.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() tasks = Tasks(file=None, config=mock_config_instance) # Manually add tasks to the internal list task1 = Task(id="task1", name="Task 1", url="https://example.com/video1") task2 = Task(id="task2", name="Task 2", url="https://example.com/video2") tasks._tasks.extend([task1, task2]) result = tasks.get_all() assert len(result) == 2 assert result[0].id == "task1" assert result[1].id == "task2" @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_clear_with_scheduler_cleanup(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test clearing tasks with scheduler cleanup.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler_instance = Mock() mock_scheduler_instance.has.return_value = True mock_scheduler_instance.remove = Mock() mock_scheduler.get_instance.return_value = mock_scheduler_instance mock_download_queue.get_instance.return_value = Mock() tasks = Tasks(file=None, config=mock_config_instance) # Add a task with timer to the internal list task_with_timer = Task(id="task1", name="Task 1", url="https://example.com/video1", timer="0 6 * * *") tasks._tasks.append(task_with_timer) result = tasks.clear() assert result == tasks assert len(tasks.get_all()) == 0 mock_scheduler_instance.has.assert_called_with("task1") mock_scheduler_instance.remove.assert_called_with("task1") @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_clear_no_tasks(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test clearing when no tasks exist.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus.get_instance.return_value = Mock() mock_scheduler_instance = Mock() mock_scheduler.get_instance.return_value = mock_scheduler_instance mock_download_queue.get_instance.return_value = Mock() tasks = Tasks(file=None, config=mock_config_instance) result = tasks.clear() assert result == tasks assert len(tasks.get_all()) == 0 # Should not call scheduler methods when no tasks exist mock_scheduler_instance.has.assert_not_called() mock_scheduler_instance.remove.assert_not_called() @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") def test_tasks_validate_valid_task_dict(self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config): """Test task validation with valid task dictionary.""" mock_config.get_instance.return_value = Mock() mock_eventbus.get_instance.return_value = Mock() mock_scheduler.get_instance.return_value = Mock() mock_download_queue.get_instance.return_value = Mock() valid_task = { "name": "Test Task", "url": "https://example.com/video", "timer": "0 6 * * *", "cli": "--write-info-json", } with patch("app.library.Tasks.validate_url") as mock_validate_url: mock_validate_url.return_value = True result = Tasks.validate(valid_task) assert result is True mock_validate_url.assert_called_once_with("https://example.com/video", allow_internal=True) def test_tasks_validate_invalid_task_type(self): """Test task validation with invalid task type.""" invalid_task = "not a dict or Task" with pytest.raises(ValueError, match="Invalid task type"): Tasks.validate(invalid_task) def test_tasks_validate_missing_name(self): """Test task validation with missing name.""" invalid_task = { "url": "https://example.com/video", } with pytest.raises(ValueError, match="No name found"): Tasks.validate(invalid_task) def test_tasks_validate_missing_url(self): """Test task validation with missing URL.""" invalid_task = { "name": "Test Task", } with pytest.raises(ValueError, match="No URL found"): Tasks.validate(invalid_task) def test_tasks_validate_invalid_url(self): """Test task validation with invalid URL.""" invalid_task = { "name": "Test Task", "url": "invalid-url", } with patch("app.library.Tasks.validate_url") as mock_validate_url: mock_validate_url.side_effect = ValueError("Invalid URL") with pytest.raises(ValueError, match="Invalid URL format"): Tasks.validate(invalid_task) def test_tasks_validate_invalid_timer(self): """Test task validation with invalid timer format.""" invalid_task = { "name": "Test Task", "url": "https://example.com/video", "timer": "invalid timer", } with patch("app.library.Tasks.validate_url") as mock_validate_url: mock_validate_url.return_value = True # Import error will be raised by cronsim for invalid format with patch("cronsim.CronSim") as mock_cronsim: mock_cronsim.side_effect = Exception("Invalid cron format") with pytest.raises(ValueError, match="Invalid timer format"): Tasks.validate(invalid_task) def test_tasks_validate_invalid_cli(self): """Test task validation with invalid CLI options.""" invalid_task = { "name": "Test Task", "url": "https://example.com/video", "cli": "invalid --cli options", } with patch("app.library.Tasks.validate_url") as mock_validate_url: mock_validate_url.return_value = True with patch("app.library.Utils.arg_converter") as mock_arg_converter: mock_arg_converter.side_effect = Exception("Invalid CLI args") with pytest.raises(ValueError, match="Invalid command options for yt-dlp"): Tasks.validate(invalid_task) @pytest.mark.asyncio @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") @patch("app.library.Tasks.Item") @patch("app.library.Tasks.datetime") @patch("app.library.Tasks.time") async def test_tasks_runner_success( self, mock_time, mock_datetime, mock_item, mock_download_queue, mock_scheduler, mock_eventbus, mock_config ): """Test successful task runner execution.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus_instance = Mock() mock_eventbus.get_instance.return_value = mock_eventbus_instance mock_scheduler.get_instance.return_value = Mock() # Mock download queue mock_download_queue_instance = Mock() mock_download_queue_instance.add = AsyncMock(return_value={"success": True, "id": "download123"}) mock_download_queue.get_instance.return_value = mock_download_queue_instance # Mock time and datetime mock_time.time.side_effect = [1000.0, 1005.5] # start and end times mock_datetime_instance = Mock() mock_datetime_instance.isoformat.return_value = "2024-01-01T12:00:00Z" mock_datetime.now.return_value = mock_datetime_instance # Mock Item.format mock_item.format.return_value = {"formatted": "item"} tasks = Tasks(file=None, config=mock_config_instance) test_task = Task( id="task1", name="Test Task", url="https://example.com/video", preset="test_preset", folder="test_folder", template="test_template", cli="--write-info-json", auto_start=True, ) await tasks._runner(test_task) # Verify download queue was called mock_download_queue_instance.add.assert_called_once() # Verify Item.format was called with correct parameters mock_item.format.assert_called_once_with( { "url": "https://example.com/video", "preset": "test_preset", "folder": "test_folder", "template": "test_template", "cli": "--write-info-json", "auto_start": True, } ) # Verify events were emitted assert mock_eventbus_instance.emit.call_count == 2 @pytest.mark.asyncio @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") @patch("app.library.Tasks.datetime") async def test_tasks_runner_no_url( self, mock_datetime, mock_download_queue, mock_scheduler, mock_eventbus, mock_config ): """Test task runner with no URL.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus_instance = Mock() mock_eventbus.get_instance.return_value = mock_eventbus_instance mock_scheduler.get_instance.return_value = Mock() mock_download_queue_instance = Mock() mock_download_queue.get_instance.return_value = mock_download_queue_instance mock_datetime_instance = Mock() mock_datetime_instance.isoformat.return_value = "2024-01-01T12:00:00Z" mock_datetime.now.return_value = mock_datetime_instance tasks = Tasks(file=None, config=mock_config_instance) test_task = Task( id="task1", name="Test Task", url="", # Empty URL ) await tasks._runner(test_task) # Verify download queue was NOT called mock_download_queue_instance.add.assert_not_called() @pytest.mark.asyncio @patch("app.library.Tasks.Config") @patch("app.library.Tasks.EventBus") @patch("app.library.Tasks.Scheduler") @patch("app.library.Tasks.DownloadQueue") @patch("app.library.Tasks.Item") @patch("app.library.Tasks.datetime") async def test_tasks_runner_download_queue_failure( self, mock_datetime, mock_item, mock_download_queue, mock_scheduler, mock_eventbus, mock_config ): """Test task runner when download queue add fails.""" mock_config_instance = Mock( debug=False, default_preset="default", config_path="/tmp", tasks_handler_timer="15 */1 * * *" ) mock_config.get_instance.return_value = mock_config_instance mock_eventbus_instance = Mock() mock_eventbus.get_instance.return_value = mock_eventbus_instance mock_scheduler.get_instance.return_value = Mock() # Mock download queue to raise exception mock_download_queue_instance = Mock() mock_download_queue_instance.add = AsyncMock(side_effect=Exception("Queue error")) mock_download_queue.get_instance.return_value = mock_download_queue_instance mock_datetime_instance = Mock() mock_datetime_instance.isoformat.return_value = "2024-01-01T12:00:00Z" mock_datetime.now.return_value = mock_datetime_instance mock_item.format.return_value = {"formatted": "item"} tasks = Tasks(file=None, config=mock_config_instance) test_task = Task( id="task1", name="Test Task", url="https://example.com/video", ) await tasks._runner(test_task) # Verify error event was emitted (check last call) calls = mock_eventbus_instance.emit.call_args_list assert len(calls) > 0 # Verify the last call was an error event last_call = calls[-1] assert "Task failed" in str(last_call)