mirror of
https://github.com/rommapp/romm.git
synced 2026-07-01 08:16:21 +00:00
381 lines
14 KiB
Python
381 lines
14 KiB
Python
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi.testclient import TestClient
|
|
from main import app
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
with TestClient(app) as client:
|
|
yield client
|
|
|
|
|
|
class MockTask:
|
|
"""Mock task class for testing"""
|
|
|
|
def __init__(
|
|
self,
|
|
name="test_task",
|
|
manual_run=True,
|
|
title="Test Task",
|
|
description="Test task description",
|
|
enabled=True,
|
|
cron_string="0 0 * * *",
|
|
):
|
|
self.manual_run = manual_run
|
|
self.title = title
|
|
self.description = description
|
|
self.enabled = enabled
|
|
self.cron_string = cron_string
|
|
self.run = AsyncMock()
|
|
|
|
|
|
class TestTasksEndpoints:
|
|
"""Test class for tasks endpoints"""
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_list_tasks_success(self, mock_get_tasks, client, access_token):
|
|
"""Test successful task listing"""
|
|
# Mock the available tasks
|
|
mock_get_tasks.return_value = {
|
|
"test_manual_task": MockTask(name="test_manual_task", manual_run=True),
|
|
"test_scheduled_task": MockTask(
|
|
name="test_scheduled_task", manual_run=False
|
|
),
|
|
}
|
|
|
|
# Mock the filesystem structure
|
|
with patch("pathlib.Path.exists") as mock_exists:
|
|
mock_exists.side_effect = lambda: True # All task files exist
|
|
|
|
response = client.get(
|
|
"/api/tasks", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Should have manual, scheduled, and watcher sections
|
|
assert "manual" in data
|
|
assert "scheduled" in data
|
|
assert "watcher" in data
|
|
|
|
# Check watcher task is always present
|
|
assert len(data["watcher"]) == 1
|
|
assert data["watcher"][0]["name"] == "filesystem_watcher"
|
|
assert data["watcher"][0]["title"] == "Rescan on filesystem change"
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_list_tasks_empty(self, mock_get_tasks, client, access_token):
|
|
"""Test task listing when no tasks are available"""
|
|
mock_get_tasks.return_value = {}
|
|
|
|
response = client.get(
|
|
"/api/tasks", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
# Should still have watcher task
|
|
assert "watcher" in data
|
|
assert len(data["watcher"]) == 1
|
|
|
|
def test_list_tasks_unauthorized(self, client):
|
|
"""Test task listing without authentication"""
|
|
response = client.get("/api/tasks")
|
|
assert response.status_code == 403
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_all_tasks_success(self, mock_get_tasks, client, access_token):
|
|
"""Test successful execution of all runnable tasks"""
|
|
mock_task1 = MockTask(name="task1", manual_run=True)
|
|
mock_task2 = MockTask(name="task2", manual_run=True)
|
|
mock_task3 = MockTask(name="task3", manual_run=False) # Not runnable
|
|
|
|
mock_get_tasks.return_value = {
|
|
"task1": mock_task1,
|
|
"task2": mock_task2,
|
|
"task3": mock_task3,
|
|
}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "2 triggerable tasks ran successfully" in data["msg"]
|
|
assert "task1, task2" in data["msg"]
|
|
|
|
# Verify only runnable tasks were called
|
|
mock_task1.run.assert_called_once()
|
|
mock_task2.run.assert_called_once()
|
|
mock_task3.run.assert_not_called()
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_all_tasks_some_fail(self, mock_get_tasks, client, access_token):
|
|
"""Test when some tasks fail during execution"""
|
|
mock_task1 = MockTask(name="task1", manual_run=True)
|
|
mock_task2 = MockTask(name="task2", manual_run=True)
|
|
mock_task2.run.side_effect = Exception("Task 2 failed")
|
|
|
|
mock_get_tasks.return_value = {
|
|
"task1": mock_task1,
|
|
"task2": mock_task2,
|
|
}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
|
|
assert "Some tasks failed" in data["msg"]
|
|
assert "task1" in data["msg"] # Successful
|
|
assert "task2: Task 2 failed" in data["msg"] # Failed
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_all_tasks_no_tasks(self, mock_get_tasks, client, access_token):
|
|
"""Test when no tasks are available"""
|
|
mock_get_tasks.return_value = {}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["msg"] == "No tasks available to run"
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_all_tasks_no_runnable_tasks(
|
|
self, mock_get_tasks, client, access_token
|
|
):
|
|
"""Test when no tasks are manually runnable"""
|
|
mock_task = MockTask(name="task1", manual_run=False)
|
|
mock_get_tasks.return_value = {"task1": mock_task}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run", headers={"Authorization": f"Bearer {access_token}"}
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["msg"] == "No runnable tasks available to run"
|
|
|
|
def test_run_all_tasks_unauthorized(self, client):
|
|
"""Test running all tasks without authentication"""
|
|
response = client.post("/api/tasks/run")
|
|
assert response.status_code == 403
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_single_task_success(self, mock_get_tasks, client, access_token):
|
|
"""Test successful execution of a single task"""
|
|
mock_task = MockTask(name="test_task", manual_run=True)
|
|
mock_get_tasks.return_value = {"test_task": mock_task}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run/test_task",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
data = response.json()
|
|
assert data["msg"] == "Task 'test_task' ran successfully!"
|
|
mock_task.run.assert_called_once()
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_single_task_not_found(self, mock_get_tasks, client, access_token):
|
|
"""Test running a task that doesn't exist"""
|
|
mock_get_tasks.return_value = {"other_task": MockTask()}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run/nonexistent_task",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
assert response.status_code == 404
|
|
data = response.json()
|
|
assert "Task 'nonexistent_task' not found" in data["detail"]
|
|
assert "other_task" in data["detail"]
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_single_task_not_runnable(self, mock_get_tasks, client, access_token):
|
|
"""Test running a task that is not manually runnable"""
|
|
mock_task = MockTask(name="scheduled_task", manual_run=False)
|
|
mock_get_tasks.return_value = {"scheduled_task": mock_task}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run/scheduled_task",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
assert response.status_code == 400
|
|
data = response.json()
|
|
assert "Task 'scheduled_task' is not triggerable manually" in data["detail"]
|
|
|
|
@patch("endpoints.tasks._get_available_tasks")
|
|
def test_run_single_task_execution_fails(
|
|
self, mock_get_tasks, client, access_token
|
|
):
|
|
"""Test when a single task execution fails"""
|
|
mock_task = MockTask(name="failing_task", manual_run=True)
|
|
mock_task.run.side_effect = Exception("Task execution failed")
|
|
mock_get_tasks.return_value = {"failing_task": mock_task}
|
|
|
|
response = client.post(
|
|
"/api/tasks/run/failing_task",
|
|
headers={"Authorization": f"Bearer {access_token}"},
|
|
)
|
|
|
|
assert response.status_code == 500
|
|
data = response.json()
|
|
assert "Task 'failing_task' failed: Task execution failed" in data["detail"]
|
|
|
|
def test_run_single_task_unauthorized(self, client):
|
|
"""Test running a single task without authentication"""
|
|
response = client.post("/api/tasks/run/test_task")
|
|
assert response.status_code == 403
|
|
|
|
|
|
class TestGetAvailableTasks:
|
|
"""Test class for the _get_available_tasks function"""
|
|
|
|
@patch("importlib.import_module")
|
|
@patch("pathlib.Path.glob")
|
|
def test_get_available_tasks_success(self, mock_glob, mock_import):
|
|
"""Test successful task discovery"""
|
|
from endpoints.tasks import _get_available_tasks
|
|
|
|
# Mock file discovery for both scheduled and manual task types
|
|
mock_file1 = MagicMock()
|
|
mock_file1.stem = "test_task"
|
|
mock_file2 = MagicMock()
|
|
mock_file2.stem = "another_task"
|
|
mock_glob.return_value = [mock_file1, mock_file2]
|
|
|
|
# Mock modules
|
|
mock_module1 = MagicMock()
|
|
mock_task_instance = MockTask()
|
|
mock_module1.test_task_task = mock_task_instance
|
|
|
|
mock_module2 = MagicMock()
|
|
mock_task_instance2 = MockTask()
|
|
mock_module2.another_task_task = mock_task_instance2
|
|
|
|
# Mock dir() calls to return the expected attributes
|
|
with patch("builtins.dir") as mock_dir:
|
|
mock_dir.side_effect = [
|
|
["test_task_task", "other_var"], # For mock_module1
|
|
["another_task_task"], # For mock_module2
|
|
] * 2 # Multiply by 2 because it runs for both 'scheduled' and 'manual' task types
|
|
|
|
mock_import.side_effect = [mock_module1, mock_module2] * 2
|
|
|
|
tasks = _get_available_tasks()
|
|
|
|
# Should find 2 tasks (one per module)
|
|
assert len(tasks) == 2
|
|
assert "test" in tasks # Key is task name without _task suffix
|
|
assert "another" in tasks # Key is task name without _task suffix
|
|
assert tasks["test"] == mock_task_instance
|
|
assert tasks["another"] == mock_task_instance2
|
|
|
|
@patch("importlib.import_module")
|
|
@patch("pathlib.Path.glob")
|
|
def test_get_available_tasks_import_error(self, mock_glob, mock_import):
|
|
"""Test task discovery with import errors"""
|
|
from endpoints.tasks import _get_available_tasks
|
|
|
|
# Mock file discovery
|
|
mock_file = MagicMock()
|
|
mock_file.stem = "broken_task"
|
|
mock_glob.return_value = [mock_file]
|
|
|
|
# Mock import error for both task types
|
|
mock_import.side_effect = [ImportError("Module not found")] * 2
|
|
|
|
tasks = _get_available_tasks()
|
|
|
|
# Should return empty dict when imports fail
|
|
assert len(tasks) == 0
|
|
|
|
@patch("importlib.import_module")
|
|
@patch("pathlib.Path.glob")
|
|
def test_get_available_tasks_no_valid_tasks(self, mock_glob, mock_import):
|
|
"""Test task discovery when modules don't have valid tasks"""
|
|
from endpoints.tasks import _get_available_tasks
|
|
|
|
# Mock file discovery
|
|
mock_file = MagicMock()
|
|
mock_file.stem = "invalid_task"
|
|
mock_glob.return_value = [mock_file]
|
|
|
|
# Mock module without valid task attributes
|
|
mock_module = MagicMock()
|
|
|
|
with patch("builtins.dir") as mock_dir:
|
|
mock_dir.return_value = ["some_var", "another_var"] # No _task suffix
|
|
mock_import.side_effect = [mock_module] * 2 # For both task types
|
|
|
|
tasks = _get_available_tasks()
|
|
|
|
# Should return empty dict
|
|
assert len(tasks) == 0
|
|
|
|
@patch("importlib.import_module")
|
|
@patch("pathlib.Path.glob")
|
|
def test_get_available_tasks_invalid_task_object(self, mock_glob, mock_import):
|
|
"""Test task discovery when task object doesn't have run method"""
|
|
from endpoints.tasks import _get_available_tasks
|
|
|
|
# Mock file discovery
|
|
mock_file = MagicMock()
|
|
mock_file.stem = "invalid_task"
|
|
mock_glob.return_value = [mock_file]
|
|
|
|
# Mock module with invalid task object
|
|
mock_module = MagicMock()
|
|
invalid_task = MagicMock()
|
|
del invalid_task.run # Remove run method
|
|
mock_module.invalid_task_task = invalid_task
|
|
|
|
with patch("builtins.dir") as mock_dir:
|
|
mock_dir.return_value = ["invalid_task_task"]
|
|
mock_import.side_effect = [mock_module] * 2 # For both task types
|
|
|
|
tasks = _get_available_tasks()
|
|
|
|
# Should return empty dict
|
|
assert len(tasks) == 0
|
|
|
|
@patch("importlib.import_module")
|
|
@patch("pathlib.Path.glob")
|
|
def test_get_available_tasks_non_callable_run(self, mock_glob, mock_import):
|
|
"""Test task discovery when run attribute is not callable"""
|
|
from endpoints.tasks import _get_available_tasks
|
|
|
|
# Mock file discovery
|
|
mock_file = MagicMock()
|
|
mock_file.stem = "invalid_task"
|
|
mock_glob.return_value = [mock_file]
|
|
|
|
# Mock module with non-callable run attribute
|
|
mock_module = MagicMock()
|
|
invalid_task = MagicMock()
|
|
invalid_task.run = "not callable" # Not callable
|
|
mock_module.invalid_task_task = invalid_task
|
|
|
|
with patch("builtins.dir") as mock_dir:
|
|
mock_dir.return_value = ["invalid_task_task"]
|
|
mock_import.side_effect = [mock_module] * 2 # For both task types
|
|
|
|
tasks = _get_available_tasks()
|
|
|
|
# Should return empty dict
|
|
assert len(tasks) == 0
|