Files
ss-tools/backend/tests/api/test_translate_schedule_routes.py

390 lines
14 KiB
Python

# #region Test.Api.TranslateScheduleRoutes [C:3] [TYPE Module] [SEMANTICS test,translate,schedule,api]
# @BRIEF Tests for _schedule_routes.py — schedule CRUD, enable/disable, next executions.
# @RELATION BINDS_TO -> [TranslateScheduleRoutesModule]
# @TEST_EDGE: schedule_not_found -> 404
# @TEST_EDGE: schedule_set_value_error -> 400
# @TEST_EDGE: schedule_enable_not_found -> 404
# @TEST_EDGE: schedule_disable_not_found -> 404
# @TEST_EDGE: schedule_delete_not_found -> 404
import os
os.environ.setdefault("DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("AUTH_DATABASE_URL", "sqlite:///:memory:")
os.environ.setdefault("SECRET_KEY", "test-secret-key-for-tests")
import sys
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
_src = str(Path(__file__).resolve().parent.parent.parent / "src")
if _src not in sys.path:
sys.path.insert(0, _src)
def _make_client(overrides: dict | None = None) -> TestClient:
from src.api.routes.translate._schedule_routes import router
from src.dependencies import (
get_config_manager, get_current_user, get_db,
has_permission, require_feature,
)
from src.schemas.auth import User, RoleSchema
app = FastAPI()
app.include_router(router)
mock_user = User(
id="user-1", username="admin", email="admin@x.com",
auth_source="LOCAL",
created_at=__import__("datetime").datetime.now(),
roles=[RoleSchema(id="r1", name="Admin", description="", permissions=[])],
)
defaults = {
get_current_user: lambda: mock_user,
get_config_manager: lambda: MagicMock(),
get_db: lambda: MagicMock(),
has_permission: lambda *a, **kw: lambda: None,
require_feature: lambda *a, **kw: lambda: None,
}
if overrides:
defaults.update(overrides)
for dep, mock_fn in defaults.items():
app.dependency_overrides[dep] = mock_fn
return TestClient(app)
def _mock_schedule(
id="sched-1", job_id="job-1",
cron_expression="0 2 * * *", timezone="UTC",
is_active=True, execution_mode="full",
last_run_at=None, next_run_at=None,
created_by="admin",
created_at=None, updated_at=None,
):
from datetime import datetime, UTC
s = MagicMock()
s.id = id
s.job_id = job_id
s.cron_expression = cron_expression
s.timezone = timezone
s.is_active = is_active
s.execution_mode = execution_mode
s.last_run_at = last_run_at or datetime(2026, 1, 1, 2, 0, 0, tzinfo=UTC)
s.next_run_at = next_run_at or datetime(2026, 1, 2, 2, 0, 0, tzinfo=UTC)
s.created_by = created_by
s.created_at = created_at or datetime(2026, 1, 1, 0, 0, 0, tzinfo=UTC)
s.updated_at = updated_at
return s
# =============================================================================
# Get Schedule
# =============================================================================
class TestGetSchedule:
"""GET /api/translate/jobs/{job_id}/schedule"""
def test_get_schedule_success(self):
mock_scheduler = MagicMock()
mock_scheduler.get_schedule.return_value = _mock_schedule()
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.get("/api/translate/jobs/job-1/schedule")
assert resp.status_code == 200
data = resp.json()
assert data["id"] == "sched-1"
assert data["cron_expression"] == "0 2 * * *"
assert data["is_active"] is True
def test_get_schedule_not_found_404(self):
mock_scheduler = MagicMock()
mock_scheduler.get_schedule.side_effect = ValueError("Schedule not found")
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.get("/api/translate/jobs/job-missing/schedule")
assert resp.status_code == 404
# =============================================================================
# Set Schedule
# =============================================================================
class TestSetSchedule:
"""PUT /api/translate/jobs/{job_id}/schedule"""
def test_set_schedule_create(self):
mock_scheduler = MagicMock()
mock_scheduler.create_schedule.return_value = _mock_schedule()
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = None
mock_sched_svc = MagicMock()
from src.dependencies import get_db
with (
patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
),
patch(
"src.dependencies.get_scheduler_service",
return_value=mock_sched_svc,
),
):
client = _make_client({get_db: lambda: mock_db})
resp = client.put("/api/translate/jobs/job-1/schedule", json={
"cron_expression": "0 2 * * *",
"timezone": "UTC",
"is_active": True,
"execution_mode": "full",
})
assert resp.status_code == 200
data = resp.json()
assert data["cron_expression"] == "0 2 * * *"
mock_scheduler.create_schedule.assert_called_once()
mock_sched_svc.add_translation_job.assert_called_once()
def test_set_schedule_update(self):
mock_scheduler = MagicMock()
mock_scheduler.update_schedule.return_value = _mock_schedule()
mock_existing = MagicMock()
mock_existing.id = "sched-1"
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = mock_existing
mock_sched_svc = MagicMock()
from src.dependencies import get_db
with (
patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
),
patch(
"src.dependencies.get_scheduler_service",
return_value=mock_sched_svc,
),
):
client = _make_client({get_db: lambda: mock_db})
resp = client.put("/api/translate/jobs/job-1/schedule", json={
"cron_expression": "0 3 * * *",
"timezone": "Europe/Moscow",
"is_active": False,
"execution_mode": "new_key_only",
})
assert resp.status_code == 200
mock_scheduler.update_schedule.assert_called_once()
mock_sched_svc.add_translation_job.assert_called_once()
def test_set_schedule_value_error_400(self):
mock_scheduler = MagicMock()
mock_scheduler.create_schedule.side_effect = ValueError("Invalid cron")
mock_db = MagicMock()
mock_db.query.return_value.filter.return_value.first.return_value = None
from src.dependencies import get_db
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client({get_db: lambda: mock_db})
resp = client.put("/api/translate/jobs/job-1/schedule", json={
"cron_expression": "bad cron",
"timezone": "UTC",
})
assert resp.status_code == 400
def test_set_schedule_422_missing_cron(self):
client = _make_client()
resp = client.put("/api/translate/jobs/job-1/schedule", json={
"timezone": "UTC",
})
assert resp.status_code == 422
assert "cron_expression" in resp.text
# =============================================================================
# Enable Schedule
# =============================================================================
class TestEnableSchedule:
"""POST /api/translate/jobs/{job_id}/schedule/enable"""
def test_enable_success(self):
mock_scheduler = MagicMock()
mock_scheduler.set_schedule_active.return_value = _mock_schedule(is_active=True)
mock_sched_svc = MagicMock()
with (
patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
),
patch(
"src.dependencies.get_scheduler_service",
return_value=mock_sched_svc,
),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-1/schedule/enable")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "enabled"
assert data["job_id"] == "job-1"
def test_enable_not_found_404(self):
mock_scheduler = MagicMock()
mock_scheduler.set_schedule_active.side_effect = ValueError("Schedule not found")
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-missing/schedule/enable")
assert resp.status_code == 404
# =============================================================================
# Disable Schedule
# =============================================================================
class TestDisableSchedule:
"""POST /api/translate/jobs/{job_id}/schedule/disable"""
def test_disable_success(self):
mock_scheduler = MagicMock()
mock_scheduler.set_schedule_active.return_value = _mock_schedule(is_active=False)
mock_sched_svc = MagicMock()
with (
patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
),
patch(
"src.dependencies.get_scheduler_service",
return_value=mock_sched_svc,
),
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-1/schedule/disable")
assert resp.status_code == 200
data = resp.json()
assert data["status"] == "disabled"
def test_disable_not_found_404(self):
mock_scheduler = MagicMock()
mock_scheduler.set_schedule_active.side_effect = ValueError("Schedule not found")
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.post("/api/translate/jobs/job-missing/schedule/disable")
assert resp.status_code == 404
# =============================================================================
# Delete Schedule
# =============================================================================
class TestDeleteSchedule:
"""DELETE /api/translate/jobs/{job_id}/schedule"""
def test_delete_success(self):
mock_scheduler = MagicMock()
mock_scheduler.delete_schedule.return_value = None
mock_sched_svc = MagicMock()
with (
patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
),
patch(
"src.dependencies.get_scheduler_service",
return_value=mock_sched_svc,
),
):
client = _make_client()
resp = client.delete("/api/translate/jobs/job-1/schedule")
assert resp.status_code == 204
def test_delete_not_found_404(self):
mock_scheduler = MagicMock()
mock_scheduler.delete_schedule.side_effect = ValueError("Schedule not found")
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.delete("/api/translate/jobs/job-missing/schedule")
assert resp.status_code == 404
# =============================================================================
# Get Next Executions
# =============================================================================
class TestGetNextExecutions:
"""GET /api/translate/jobs/{job_id}/schedule/next-executions"""
def test_next_executions_success(self):
sched = _mock_schedule(cron_expression="0 2 * * *", timezone="UTC")
mock_scheduler = MagicMock()
mock_scheduler.get_schedule.return_value = sched
# TranslationScheduler is replaced by a MagicMock class.
# `get_next_executions` is a static method called on the class itself, not on instance.
# Set it on the mock class so TranslationScheduler.get_next_executions() works.
mock_sched_class = MagicMock(return_value=mock_scheduler)
mock_sched_class.get_next_executions.return_value = [
"2026-01-02T02:00:00Z", "2026-01-03T02:00:00Z",
]
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
mock_sched_class,
):
client = _make_client()
resp = client.get("/api/translate/jobs/job-1/schedule/next-executions?n=2")
assert resp.status_code == 200
data = resp.json()
assert data["cron_expression"] == "0 2 * * *"
assert len(data["next_executions"]) == 2
def test_next_executions_not_found_404(self):
mock_scheduler = MagicMock()
mock_scheduler.get_schedule.side_effect = ValueError("Schedule not found")
with patch(
"src.api.routes.translate._schedule_routes.TranslationScheduler",
return_value=mock_scheduler,
):
client = _make_client()
resp = client.get("/api/translate/jobs/job-missing/schedule/next-executions")
assert resp.status_code == 404
# #endregion Test.Api.TranslateScheduleRoutes