"""SP16 — Admin scheduler API endpoint tests. The endpoints under /api/admin/scheduler/* are thin wrappers around :class:`cyclone.scheduler.Scheduler`. These tests exercise them via the FastAPI TestClient to confirm wiring (auth-free admin endpoints work, response shapes match, idempotency holds). """ from __future__ import annotations import asyncio import json from pathlib import Path import pytest @pytest.fixture def _stub_scheduler_env(tmp_path, monkeypatch): """Set up: a stub-mode SFTP block, scheduler configured. Yields (staging_dir, scheduler_singleton). We deliberately do NOT enable SQLCipher encryption in this fixture — the scheduler doesn't care about encryption, and patching ``db_crypto.get_secret`` here would cause the lifespan handler to rebuild the engine with SQLCipher on a plain-SQLite test file (which raises "file is not a database"). The encryption-at-rest tests live in ``test_db_crypto.py``. """ from cyclone import db from cyclone import scheduler as sched_mod from cyclone.providers import SftpBlock monkeypatch.setenv("CYCLONE_DB_URL", f"sqlite:///{tmp_path}/test.db") db._reset_for_tests() staging = tmp_path / "staging" inbound = staging / "ToHPE" inbound.mkdir(parents=True) sftp_block = SftpBlock( host="mft.example.com", port=22, username="test", paths={"outbound": "/FromHPE", "inbound": "/ToHPE"}, stub=True, staging_dir=str(staging), poll_seconds=60, auth={"method": "keychain", "secret_ref": "test.password"}, ) sched_mod.reset_scheduler_for_tests() sched = sched_mod.configure_scheduler(sftp_block, sftp_block_name="t") yield staging, sched sched_mod.reset_scheduler_for_tests() db._reset_for_tests() def _drop_file(staging: Path, name: str, body: bytes) -> Path: p = staging / "ToHPE" / name p.write_bytes(body) return p def test_scheduler_status_starts_not_running(_stub_scheduler_env): from fastapi.testclient import TestClient from cyclone.api import app _, sched = _stub_scheduler_env with TestClient(app) as client: r = client.get("/api/admin/scheduler/status") assert r.status_code == 200 body = r.json() assert body["running"] is False assert body["poll_interval_seconds"] == 60 assert body["sftp_block_name"] == "t" def test_scheduler_start_then_status_then_stop(_stub_scheduler_env): from fastapi.testclient import TestClient from cyclone.api import app with TestClient(app) as client: r1 = client.post("/api/admin/scheduler/start") assert r1.status_code == 200 assert r1.json()["status"]["running"] is True r2 = client.get("/api/admin/scheduler/status") assert r2.json()["running"] is True r3 = client.post("/api/admin/scheduler/stop") assert r3.status_code == 200 assert r3.json()["status"]["running"] is False def test_scheduler_tick_processes_one_file(_stub_scheduler_env): from fastapi.testclient import TestClient from cyclone.api import app staging, _ = _stub_scheduler_env _drop_file( staging, "TP11525703-837P_M019048402-20260618130000000-1of1_TA1.x12", (Path(__file__).parent / "fixtures" / "minimal_ta1.txt").read_bytes(), ) with TestClient(app) as client: r = client.post("/api/admin/scheduler/tick") assert r.status_code == 200 body = r.json() assert body["ok"] is True assert body["tick"]["files_seen"] == 1 assert body["tick"]["files_processed"] == 1 def test_scheduler_processed_files_lists_history(_stub_scheduler_env): from fastapi.testclient import TestClient from cyclone.api import app staging, _ = _stub_scheduler_env _drop_file( staging, "TP11525703-837P_M019048402-20260618130000000-1of1_TA1.x12", (Path(__file__).parent / "fixtures" / "minimal_ta1.txt").read_bytes(), ) with TestClient(app) as client: client.post("/api/admin/scheduler/tick") r = client.get("/api/admin/scheduler/processed-files") assert r.status_code == 200 body = r.json() assert body["count"] == 1 f = body["files"][0] assert f["status"] == "ok" assert f["parser_used"] == "parse_ta1" assert "TP11525703" in f["name"] def test_scheduler_processed_files_filters_by_status(_stub_scheduler_env): from fastapi.testclient import TestClient from cyclone.api import app staging, _ = _stub_scheduler_env # Drop a file with a type Cyclone doesn't parse — gets recorded as # "skipped". _drop_file( staging, "TP11525703-837P_M019048402-20260618130000000-1of1_270.x12", b"some bytes", ) with TestClient(app) as client: client.post("/api/admin/scheduler/tick") r_all = client.get("/api/admin/scheduler/processed-files") r_skipped = client.get( "/api/admin/scheduler/processed-files?status=skipped", ) r_ok = client.get( "/api/admin/scheduler/processed-files?status=ok", ) assert r_all.json()["count"] == 1 assert r_skipped.json()["count"] == 1 assert r_ok.json()["count"] == 0