1076 lines
43 KiB
Python
1076 lines
43 KiB
Python
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
|
|
assert task.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,
|
|
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
|
|
assert task.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,
|
|
enabled=True,
|
|
)
|
|
|
|
# Add task to tasks list so it can be found by _runner
|
|
tasks._tasks.append(test_task)
|
|
|
|
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,
|
|
"extras": {
|
|
"source_name": "Test Task",
|
|
"source_id": "task1",
|
|
"source_handler": "Tasks",
|
|
},
|
|
}
|
|
)
|
|
|
|
# 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 to trigger error
|
|
enabled=True,
|
|
)
|
|
|
|
# Add task to tasks list so it can be found by _runner
|
|
tasks._tasks.append(test_task)
|
|
|
|
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",
|
|
enabled=True,
|
|
)
|
|
|
|
# Add task to tasks list so it can be found by _runner
|
|
tasks._tasks.append(test_task)
|
|
|
|
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)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("app.library.Tasks.Config")
|
|
@patch("app.library.Tasks.EventBus")
|
|
@patch("app.library.Tasks.Scheduler")
|
|
@patch("app.library.Tasks.DownloadQueue")
|
|
async def test_tasks_runner_disabled_task(
|
|
self, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
|
):
|
|
"""Test that disabled tasks are skipped by the runner."""
|
|
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
|
|
|
|
tasks = Tasks(file=None, config=mock_config_instance)
|
|
|
|
test_task = Task(
|
|
id="task1",
|
|
name="Test Task",
|
|
url="https://example.com/video",
|
|
enabled=False, # Task is disabled
|
|
)
|
|
|
|
# Add task to tasks list so it can be found by _runner
|
|
tasks._tasks.append(test_task)
|
|
|
|
await tasks._runner(test_task)
|
|
|
|
# Verify download queue was NOT called (task was skipped)
|
|
mock_download_queue_instance.add.assert_not_called()
|
|
|
|
|
|
class TestHandleTaskInspect:
|
|
"""Test the HandleTask inspect method with static_only parameter."""
|
|
|
|
@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.Services")
|
|
async def test_inspect_with_static_only_true(
|
|
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
|
):
|
|
"""Test inspect with static_only=True returns immediately after can_handle check."""
|
|
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()
|
|
|
|
# Mock Services to simulate successful can_handle
|
|
mock_services_instance = Mock()
|
|
mock_services_instance.handle_sync = Mock(return_value=True)
|
|
mock_services_instance.handle_async = AsyncMock() # Should NOT be called with static_only=True
|
|
mock_services.get_instance.return_value = mock_services_instance
|
|
|
|
# Create a mock handler
|
|
mock_handler = Mock()
|
|
mock_handler.__name__ = "TestHandler"
|
|
mock_handler.can_handle = Mock(return_value=True)
|
|
mock_handler.extract = AsyncMock() # Should NOT be called with static_only=True
|
|
|
|
tasks = Tasks(file=None, config=mock_config_instance)
|
|
handler = tasks.get_handler()
|
|
handler._handlers = [mock_handler]
|
|
|
|
# Call inspect with static_only=True
|
|
result = await handler.inspect(
|
|
url="https://example.com/feed", preset="default", handler_name=None, static_only=True
|
|
)
|
|
|
|
# Verify result structure
|
|
assert hasattr(result, "items")
|
|
assert hasattr(result, "metadata")
|
|
assert result.items == []
|
|
assert result.metadata["matched"] is True
|
|
assert result.metadata["handler"] == "TestHandler"
|
|
|
|
# Verify handle_async (extract) was NOT called
|
|
mock_services_instance.handle_async.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.Services")
|
|
async def test_inspect_with_static_only_false(
|
|
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
|
):
|
|
"""Test inspect with static_only=False performs full extraction."""
|
|
from app.library.Tasks import TaskResult
|
|
|
|
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()
|
|
|
|
# Mock Services
|
|
mock_services_instance = Mock()
|
|
mock_services_instance.handle_sync = Mock(return_value=True)
|
|
|
|
# Mock successful extract result
|
|
mock_extract_result = TaskResult(
|
|
items=[{"url": "https://example.com/video1", "title": "Video 1"}],
|
|
metadata={"raw_count": 1},
|
|
)
|
|
mock_services_instance.handle_async = AsyncMock(return_value=mock_extract_result)
|
|
mock_services.get_instance.return_value = mock_services_instance
|
|
|
|
# Create a mock handler
|
|
mock_handler = Mock()
|
|
mock_handler.__name__ = "TestHandler"
|
|
mock_handler.can_handle = Mock(return_value=True)
|
|
mock_handler.extract = AsyncMock(return_value=mock_extract_result)
|
|
|
|
tasks = Tasks(file=None, config=mock_config_instance)
|
|
handler = tasks.get_handler()
|
|
handler._handlers = [mock_handler]
|
|
|
|
# Call inspect with static_only=False (default)
|
|
result = await handler.inspect(url="https://example.com/feed", preset="default", handler_name=None)
|
|
|
|
# Verify result structure includes extracted items
|
|
assert hasattr(result, "items")
|
|
assert hasattr(result, "metadata")
|
|
assert len(result.items) > 0
|
|
assert result.metadata["matched"] is True
|
|
assert result.metadata["handler"] == "TestHandler"
|
|
assert result.metadata["supported"] is True
|
|
|
|
# Verify handle_async (extract) WAS called
|
|
mock_services_instance.handle_async.assert_called_once()
|
|
|
|
@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.Services")
|
|
async def test_inspect_static_only_with_no_handler_match(
|
|
self, mock_services, mock_download_queue, mock_scheduler, mock_eventbus, mock_config
|
|
):
|
|
"""Test inspect with static_only=True when no handler matches."""
|
|
from app.library.Tasks import TaskFailure
|
|
|
|
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()
|
|
|
|
# Mock Services to simulate failed can_handle
|
|
mock_services_instance = Mock()
|
|
mock_services_instance.handle_sync = Mock(return_value=False)
|
|
mock_services.get_instance.return_value = mock_services_instance
|
|
|
|
# Create a mock handler that won't match
|
|
mock_handler = Mock()
|
|
mock_handler.__name__ = "TestHandler"
|
|
mock_handler.can_handle = Mock(return_value=False)
|
|
|
|
tasks = Tasks(file=None, config=mock_config_instance)
|
|
handler = tasks.get_handler()
|
|
handler._handlers = [mock_handler]
|
|
|
|
# Call inspect with static_only=True
|
|
result = await handler.inspect(
|
|
url="https://unsupported.com/feed", preset="default", handler_name=None, static_only=True
|
|
)
|
|
|
|
# Verify result is a TaskFailure
|
|
assert isinstance(result, TaskFailure)
|
|
assert result.metadata["matched"] is False
|
|
assert result.metadata["handler"] is None
|