# #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