from __future__ import annotations from datetime import UTC, datetime, timedelta from typing import Any, cast import httpx import pytest from backup_tool.config import Settings from backup_tool.db.models import NotificationDelivery, NotificationEvent, Schedule from backup_tool.scheduler import SchedulerService from sqlalchemy import select PASSWORD = "correct-horse-battery-staple" async def setup_job(client: httpx.AsyncClient, settings: Settings) -> tuple[dict[str, str], str]: source_root = settings.local_source_roots[0] / "source" source_root.mkdir() headers = await login(client) repository = await client.post( "/api/v2/repositories", json={ "name": "repo", "relative_path": "repo", "compression": "none", "encryption": "none", }, headers=headers, ) source = await client.post( "/api/v2/sources", json={ "name": "source", "kind": "local", "public_config": {"root": str(source_root)}, }, headers=headers, ) job = await client.post( "/api/v2/jobs", json={ "name": "job", "source_id": source.json()["id"], "repository_id": repository.json()["id"], "requested_mode": "full", "exclusions": [], "retention": {}, "enabled": True, "allow_empty": True, }, headers=headers, ) assert job.status_code == 201 return headers, job.json()["id"] async def login(client: httpx.AsyncClient) -> dict[str, str]: response = await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD}) assert response.status_code == 201 return {"X-CSRF-Token": client.cookies["backup_tool_csrf"]} @pytest.mark.asyncio async def test_schedule_role_delivery_and_live_crud_sync( app_client: tuple[httpx.AsyncClient, Settings], ) -> None: client, settings = app_client headers, job_id = await setup_job(client, settings) subscription = await client.post( "/api/v2/notifications/subscriptions", json={ "channel": "email", "event_filters": ["schedule.occurrence_enqueued"], "destination": {"recipients": ["operator@example.test"]}, }, headers=headers, ) assert subscription.status_code == 201 created = await client.post( f"/api/v2/jobs/{job_id}/schedule", json={"cron": "* * * * *", "timezone": "UTC"}, headers=headers, ) assert created.status_code == 201 app = cast(Any, client._transport).app async with app.state.sessions() as db: schedule = await db.scalar(select(Schedule).where(Schedule.job_id == job_id)) assert schedule is not None schedule.next_nominal_at = datetime.now(UTC) - timedelta(seconds=1) await db.commit() service = SchedulerService(settings) try: assert await service.run_once() == 1 finally: await service.engine.dispose() async with app.state.sessions() as db: delivery = await db.scalar( select(NotificationDelivery.id) .join(NotificationEvent, NotificationDelivery.event_id == NotificationEvent.id) .where(NotificationEvent.type == "schedule.occurrence_enqueued") .limit(1) ) assert delivery is not None updated = await client.patch( f"/api/v2/jobs/{job_id}/schedule", json={"cron": "0 10 * * *", "timezone": "UTC", "enabled": False}, headers=headers, ) assert updated.status_code == 200 assert updated.json()["next_nominal_at"] is None # Deletion with historical executions is deliberately restricted; schedule # delete behavior is covered before occurrence enqueue in the catalog test.