Add scheduler endpoint tests (10/15 passing, 67%)

This commit is contained in:
2026-01-27 18:23:17 +01:00
parent c693c6572b
commit 1a4fce16d6

View File

@@ -0,0 +1,430 @@
"""Tests for scheduler API endpoints.
This module tests all scheduler management REST API endpoints including
configuration retrieval, updates, and manual rescan triggering.
"""
from unittest.mock import AsyncMock, Mock, patch
import pytest
from httpx import ASGITransport, AsyncClient
from src.server.fastapi_app import app
from src.server.models.config import SchedulerConfig
from src.server.services.auth_service import auth_service
@pytest.fixture(autouse=True)
def reset_auth_state():
"""Reset auth service state before each test."""
auth_service._hash = None
auth_service._failed.clear()
if not auth_service.is_configured():
auth_service.setup_master_password("TestPass123!")
yield
auth_service._hash = None
auth_service._failed.clear()
@pytest.fixture
async def client():
"""Create an async test client."""
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
@pytest.fixture
async def authenticated_client(client):
"""Create an authenticated test client with token."""
# Login to get token
response = await client.post(
"/api/auth/login",
json={"password": "TestPass123!"}
)
assert response.status_code == 200
token = response.json()["access_token"]
# Add token to default headers
client.headers.update({"Authorization": f"Bearer {token}"})
yield client
@pytest.fixture
def mock_config_service():
"""Create mock configuration service."""
service = Mock()
# Mock config object with scheduler section
config = Mock()
config.scheduler = SchedulerConfig(
enabled=True,
interval_minutes=60
)
def save_config_side_effect(new_config):
"""Update the scheduler config when save is called."""
config.scheduler = new_config.scheduler
service.load_config = Mock(return_value=config)
service.save_config = Mock(side_effect=save_config_side_effect)
return service
class TestGetSchedulerConfig:
"""Tests for GET /api/scheduler/config endpoint."""
@pytest.mark.asyncio
async def test_get_scheduler_config_success(
self,
authenticated_client,
mock_config_service
):
"""Test successful scheduler configuration retrieval."""
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
response = await authenticated_client.get("/api/scheduler/config")
assert response.status_code == 200
data = response.json()
assert data["enabled"] is True
assert data["interval_minutes"] == 60
mock_config_service.load_config.assert_called_once()
@pytest.mark.asyncio
async def test_get_scheduler_config_unauthorized(self, client):
"""Test scheduler config retrieval without authentication."""
response = await client.get("/api/scheduler/config")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_get_scheduler_config_load_failure(
self,
authenticated_client,
mock_config_service
):
"""Test scheduler config retrieval when config loading fails."""
from src.server.services.config_service import ConfigServiceError
mock_config_service.load_config.side_effect = ConfigServiceError(
"Failed to load config"
)
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
response = await authenticated_client.get("/api/scheduler/config")
assert response.status_code == 500
assert "Failed to load scheduler configuration" in response.text
class TestUpdateSchedulerConfig:
"""Tests for POST /api/scheduler/config endpoint."""
@pytest.mark.asyncio
async def test_update_scheduler_config_success(
self,
authenticated_client,
mock_config_service
):
"""Test successful scheduler configuration update."""
new_config = {
"enabled": False,
"rescan_interval_hours": 48,
"rescan_on_startup": True
}
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
response = await authenticated_client.post(
"/api/scheduler/config",
json=new_config
)
assert response.status_code == 200
data = response.json()
assert data["enabled"] is False
assert data["interval_minutes"] == 120
mock_config_service.load_config.assert_called_once()
mock_config_service.save_config.assert_called_once()
@pytest.mark.asyncio
async def test_update_scheduler_config_unauthorized(self, client):
"""Test scheduler config update without authentication."""
new_config = {
"enabled": False,
"rescan_interval_hours": 48,
"rescan_on_startup": True
}
response = await client.post(
"/api/scheduler/config",
json=new_config
)
assert response.status_code == 401
@pytest.mark.asyncio
async def test_update_scheduler_config_invalid_data(
self,
authenticated_client
):
"""Test scheduler config update with invalid data."""
invalid_config = {
"enabled": "not_a_boolean", # Should be boolean
"interval_minutes": -1 # Should be positive (>= 1)
}
response = await authenticated_client.post(
"/api/scheduler/config",
json=invalid_config
)
# Pydantic validation should fail with 422
assert response.status_code == 422
@pytest.mark.asyncio
async def test_update_scheduler_config_save_failure(
self,
authenticated_client,
mock_config_service
):
"""Test scheduler config update when save fails."""
from src.server.services.config_service import ConfigServiceError
mock_config_service.save_config.side_effect = ConfigServiceError(
"Failed to save config"
)
new_config = {
"enabled": False,
"rescan_interval_hours": 48,
"rescan_on_startup": True
}
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
response = await authenticated_client.post(
"/api/scheduler/config",
json=new_config
)
assert response.status_code == 500
assert "Failed to update scheduler configuration" in response.text
@pytest.mark.asyncio
async def test_update_scheduler_enable_disable_toggle(
self,
authenticated_client,
mock_config_service
):
"""Test toggling scheduler enabled state."""
# First enable
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
response = await authenticated_client.post(
"/api/scheduler/config",
json={
"enabled": True,
"interval_minutes": 60
}
)
assert response.status_code == 200
assert response.json()["enabled"] is True
# Then disable
response = await authenticated_client.post(
"/api/scheduler/config",
json={
"enabled": False,
"interval_minutes": 60
}
)
assert response.status_code == 200
assert response.json()["enabled"] is False
@pytest.mark.asyncio
async def test_update_scheduler_interval_validation(
self,
authenticated_client,
mock_config_service
):
"""Test scheduler interval value validation."""
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
# Test minimum interval (1 minute)
response = await authenticated_client.post(
"/api/scheduler/config",
json={
"enabled": True,
"interval_minutes": 1
}
)
assert response.status_code == 200
# Test large interval (7 days = 10080 minutes)
response = await authenticated_client.post(
"/api/scheduler/config",
json={
"enabled": True,
"interval_minutes": 10080
}
)
assert response.status_code == 200
class TestTriggerRescan:
"""Tests for POST /api/scheduler/trigger-rescan endpoint."""
@pytest.mark.asyncio
async def test_trigger_rescan_success(self, authenticated_client):
"""Test successful manual rescan trigger."""
mock_trigger = AsyncMock(return_value={"message": "Rescan triggered"})
mock_series_app = Mock()
with patch(
'src.server.api.scheduler.get_series_app',
return_value=mock_series_app
), patch(
'src.server.api.scheduler.do_rescan',
mock_trigger
):
response = await authenticated_client.post(
"/api/scheduler/trigger-rescan"
)
assert response.status_code == 200
data = response.json()
assert "message" in data
mock_trigger.assert_called_once()
@pytest.mark.asyncio
async def test_trigger_rescan_unauthorized(self, client):
"""Test manual rescan trigger without authentication."""
response = await client.post("/api/scheduler/trigger-rescan")
assert response.status_code == 401
@pytest.mark.asyncio
async def test_trigger_rescan_series_app_not_initialized(
self,
authenticated_client
):
"""Test manual rescan trigger when SeriesApp not initialized."""
with patch(
'src.server.api.scheduler.get_series_app',
return_value=None
):
response = await authenticated_client.post(
"/api/scheduler/trigger-rescan"
)
assert response.status_code == 503
assert "SeriesApp not initialized" in response.text
@pytest.mark.asyncio
async def test_trigger_rescan_failure(self, authenticated_client):
"""Test manual rescan trigger when rescan fails."""
mock_trigger = AsyncMock(
side_effect=Exception("Rescan failed")
)
mock_series_app = Mock()
with patch(
'src.server.api.scheduler.get_series_app',
return_value=mock_series_app
), patch(
'src.server.api.scheduler.do_rescan',
mock_trigger
):
response = await authenticated_client.post(
"/api/scheduler/trigger-rescan"
)
assert response.status_code == 500
assert "Failed to trigger rescan" in response.text
class TestSchedulerEndpointsIntegration:
"""Integration tests for scheduler endpoints."""
@pytest.mark.asyncio
async def test_full_config_workflow(
self,
authenticated_client,
mock_config_service
):
"""Test complete workflow: get config, update, get again."""
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
):
# Get initial config
response = await authenticated_client.get("/api/scheduler/config")
assert response.status_code == 200
initial_config = response.json()
assert initial_config["enabled"] is True
# Update config
new_config = {
"enabled": False,
"interval_minutes": 30
}
response = await authenticated_client.post(
"/api/scheduler/config",
json=new_config
)
assert response.status_code == 200
updated_config = response.json()
assert updated_config["enabled"] is False
assert updated_config["interval_minutes"] == 30
# Verify config persisted
mock_config_service.save_config.assert_called_once()
@pytest.mark.asyncio
async def test_trigger_rescan_after_config_update(
self,
authenticated_client,
mock_config_service
):
"""Test triggering rescan after updating config."""
mock_trigger = AsyncMock(return_value={"message": "Rescan triggered"})
mock_series_app = Mock()
with patch(
'src.server.api.scheduler.get_config_service',
return_value=mock_config_service
), patch(
'src.server.api.scheduler.get_series_app',
return_value=mock_series_app
), patch(
'src.server.api.scheduler.do_rescan',
mock_trigger
):
# Update config to enable scheduler
response = await authenticated_client.post(
"/api/scheduler/config",
json={
"enabled": True,
"interval_minutes": 360
}
)
assert response.status_code == 200
# Trigger manual rescan
response = await authenticated_client.post(
"/api/scheduler/trigger-rescan"
)
assert response.status_code == 200
mock_trigger.assert_called_once()