# #region TranslateSchedulerTests [C:2] [TYPE Module] # @SEMANTICS: tests, translate, scheduler # @BRIEF Tests for TranslationScheduler CRUD and APScheduler integration. # @LAYER Tests # @RELATION BINDS_TO -> [TranslationScheduler] # # @TEST_CONTRACT: ScheduleFlow -> # { # invariants: [ # "Create schedule returns TranslationSchedule with valid fields", # "Update schedule modifies cron/timezone/active", # "Delete schedule removes the row", # "Enable/disable toggles is_active", # "get_next_executions returns ISO datetime strings", # "List active schedules returns only active rows" # ] # } # @TEST_FIXTURE: valid_job -> created via TranslateJobService # @TEST_FIXTURE: schedule_data -> valid cron expression # @TEST_EDGE: schedule_not_found -> ValueError on get/delete non-existent # @TEST_EDGE: next_executions_invalid_cron -> empty list # @TEST_INVARIANT: schedule_crud_flow -> VERIFIED_BY: [valid_job, schedule_data] from pathlib import Path import sys sys.path.insert(0, str(Path(__file__).parent.parent / "src")) import pytest from unittest.mock import MagicMock from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from src.core.database import Base from src.plugins.translate.scheduler import TranslationScheduler from src.plugins.translate.service import TranslateJobService from src.schemas.translate import TranslateJobCreate # In-memory SQLite SQLALCHEMY_DATABASE_URL = "sqlite:///:memory:" engine = create_engine(SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}) TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) Base.metadata.create_all(bind=engine) @pytest.fixture def db_session(): connection = engine.connect() transaction = connection.begin() session = TestingSessionLocal(bind=connection) yield session session.close() transaction.rollback() connection.close() # #region test_create_schedule [C:2] [TYPE Function] # @BRIEF Verify schedule creation with valid params. async def test_create_schedule(db_session): """Test creating a schedule for a job.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] svc = TranslateJobService(db_session, config_mgr, "test_user") payload = TranslateJobCreate(name="Sched Job", source_dialect="pg", target_dialect="ch") job = await svc.create_job(payload) scheduler = TranslationScheduler(db_session, config_mgr, "test_user") schedule = scheduler.create_schedule( job_id=job.id, cron_expression="0 2 * * *", timezone="UTC", ) assert schedule.job_id == job.id assert schedule.cron_expression == "0 2 * * *" assert schedule.timezone == "UTC" assert schedule.is_active is True assert schedule.id is not None # #endregion test_create_schedule # #region test_update_schedule [C:2] [TYPE Function] # @BRIEF Verify schedule update. async def test_update_schedule(db_session): """Test updating a schedule.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] svc = TranslateJobService(db_session, config_mgr, "test_user") job = await svc.create_job(TranslateJobCreate(name="Job", source_dialect="pg", target_dialect="ch")) scheduler = TranslationScheduler(db_session, config_mgr, "test_user") scheduler.create_schedule(job.id, "0 2 * * *") updated = scheduler.update_schedule(job.id, cron_expression="30 3 * * *", timezone_str="US/Eastern", is_active=False) assert updated.cron_expression == "30 3 * * *" assert updated.timezone == "US/Eastern" assert updated.is_active is False # #endregion test_update_schedule # #region test_delete_schedule [C:2] [TYPE Function] # @BRIEF Verify schedule deletion. async def test_delete_schedule(db_session): """Test deleting a schedule.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] svc = TranslateJobService(db_session, config_mgr, "test_user") job = await svc.create_job(TranslateJobCreate(name="Job", source_dialect="pg", target_dialect="ch")) scheduler = TranslationScheduler(db_session, config_mgr, "test_user") scheduler.create_schedule(job.id, "0 2 * * *") scheduler.delete_schedule(job.id) with pytest.raises(ValueError, match="No schedule found"): scheduler.get_schedule(job.id) # #endregion test_delete_schedule # #region test_enable_disable_schedule [C:2] [TYPE Function] # @BRIEF Verify enable/disable toggle. async def test_enable_disable_schedule(db_session): """Test enabling and disabling a schedule.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] svc = TranslateJobService(db_session, config_mgr, "test_user") job = await svc.create_job(TranslateJobCreate(name="Job", source_dialect="pg", target_dialect="ch")) scheduler = TranslationScheduler(db_session, config_mgr, "test_user") scheduler.create_schedule(job.id, "0 2 * * *") sched = scheduler.set_schedule_active(job.id, False) assert sched.is_active is False sched = scheduler.set_schedule_active(job.id, True) assert sched.is_active is True # #endregion test_enable_disable_schedule # #region test_get_schedule_not_found [C:2] [TYPE Function] # @BRIEF Verify ValueError on getting non-existent schedule. def test_get_schedule_not_found(db_session): """Test that getting a non-existent schedule raises ValueError.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] scheduler = TranslationScheduler(db_session, config_mgr, "test_user") with pytest.raises(ValueError, match="No schedule found"): scheduler.get_schedule("non-existent") # #endregion test_get_schedule_not_found # #region test_list_active_schedules [C:2] [TYPE Function] # @BRIEF Verify listing only active schedules. async def test_list_active_schedules(db_session): """Test listing active schedules.""" config_mgr = MagicMock() config_mgr.get_environments.return_value = [] svc = TranslateJobService(db_session, config_mgr, "test_user") job1 = await svc.create_job(TranslateJobCreate(name="Job1", source_dialect="pg", target_dialect="ch")) scheduler = TranslationScheduler(db_session, config_mgr, "test_user") scheduler.create_schedule(job1.id, "0 2 * * *") scheduler.set_schedule_active(job1.id, True) active = TranslationScheduler.list_active_schedules(db_session) assert len(active) >= 1 assert active[0].job_id == job1.id # #endregion test_list_active_schedules # #region test_get_next_executions [C:2] [TYPE Function] # @BRIEF Verify next execution time computation. def test_get_next_executions(): """Test computing next execution times.""" times = TranslationScheduler.get_next_executions("0 2 * * *", "UTC", n=3) assert len(times) == 3 for t in times: assert "T" in t # ISO format check # #endregion test_get_next_executions # #region test_get_next_executions_invalid [C:2] [TYPE Function] # @BRIEF Verify invalid cron returns empty list. def test_get_next_executions_invalid(): """Test invalid cron returns empty list.""" times = TranslationScheduler.get_next_executions("invalid cron", "UTC", n=3) assert times == [] # #endregion test_get_next_executions_invalid # #endregion TranslateSchedulerTests