test(dashboard): cover constants, git helper, cron endpoint, files sandbox
This commit is contained in:
170
tests/test_dashboard_cron_endpoint.py
Normal file
170
tests/test_dashboard_cron_endpoint.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Tests for the /api/cron endpoint (echo-core flat schema, no UTC→local conversion)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[1]
|
||||
DASH = PROJECT_ROOT / "dashboard"
|
||||
|
||||
if str(DASH) not in sys.path:
|
||||
sys.path.insert(0, str(DASH))
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def cron_module():
|
||||
from handlers import cron as _c # type: ignore
|
||||
return _c
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def handler(cron_module):
|
||||
class _Stub(cron_module.CronHandlers):
|
||||
def __init__(self):
|
||||
self.captured = None
|
||||
self.captured_code = None
|
||||
|
||||
def send_json(self, data, code=200):
|
||||
self.captured = data
|
||||
self.captured_code = code
|
||||
|
||||
return _Stub()
|
||||
|
||||
|
||||
def _write_jobs(tmp_path: Path, jobs: list) -> Path:
|
||||
cron_dir = tmp_path / "cron"
|
||||
cron_dir.mkdir(parents=True, exist_ok=True)
|
||||
(cron_dir / "jobs.json").write_text(json.dumps(jobs), encoding="utf-8")
|
||||
return tmp_path
|
||||
|
||||
|
||||
def test_parse_cron_time_no_utc_conversion(cron_module):
|
||||
"""Echo-core cron strings are already Europe/Bucharest; no +3 shift."""
|
||||
# 10:00 Bucharest should stay 10:00 in the display.
|
||||
assert cron_module._parse_cron_time("0 10 * * *") == "10:00"
|
||||
assert cron_module._parse_cron_time("30 8 * * 1-5") == "08:30"
|
||||
|
||||
|
||||
def test_parse_cron_time_handles_hour_range(cron_module):
|
||||
"""A cron like `0 9-17 * * *` should display the starting hour."""
|
||||
assert cron_module._parse_cron_time("0 9-17 * * *") == "09:00"
|
||||
|
||||
|
||||
def test_parse_cron_time_falls_back_on_unexpected_expr(cron_module):
|
||||
assert cron_module._parse_cron_time("@hourly") == "@hourly"[:15]
|
||||
assert cron_module._parse_cron_time("bad") == "bad"
|
||||
|
||||
|
||||
def test_iso_to_epoch_ms_handles_empty(cron_module):
|
||||
assert cron_module._iso_to_epoch_ms("") == 0
|
||||
assert cron_module._iso_to_epoch_ms(None) == 0
|
||||
assert cron_module._iso_to_epoch_ms("not a date") == 0
|
||||
|
||||
|
||||
def test_iso_to_epoch_ms_parses_iso(cron_module):
|
||||
# Well-known epoch
|
||||
ms = cron_module._iso_to_epoch_ms("1970-01-01T00:00:00+00:00")
|
||||
assert ms == 0
|
||||
|
||||
|
||||
def test_missing_jobs_file_returns_empty(tmp_path, monkeypatch, handler):
|
||||
import constants # type: ignore
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
assert handler.captured["jobs"] == []
|
||||
assert "No jobs file" in handler.captured["error"]
|
||||
|
||||
|
||||
def test_non_list_jobs_file_is_rejected(tmp_path, monkeypatch, handler):
|
||||
import constants # type: ignore
|
||||
cron_dir = tmp_path / "cron"
|
||||
cron_dir.mkdir()
|
||||
(cron_dir / "jobs.json").write_text('{"not": "a list"}', encoding="utf-8")
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
assert handler.captured["jobs"] == []
|
||||
assert "shape" in handler.captured["error"].lower()
|
||||
|
||||
|
||||
def test_disabled_jobs_are_filtered_out(tmp_path, monkeypatch, handler):
|
||||
import constants # type: ignore
|
||||
_write_jobs(tmp_path, [
|
||||
{"name": "foo", "cron": "0 10 * * *", "enabled": True},
|
||||
{"name": "bar", "cron": "0 11 * * *", "enabled": False},
|
||||
])
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
names = [j["name"] for j in handler.captured["jobs"]]
|
||||
assert names == ["foo"]
|
||||
assert handler.captured["total"] == 1
|
||||
|
||||
|
||||
def test_frontend_shape_is_preserved(tmp_path, monkeypatch, handler):
|
||||
"""Output must carry: id, name, time, schedule, ranToday, lastStatus,
|
||||
lastRunAtMs, nextRunAtMs."""
|
||||
import constants # type: ignore
|
||||
_write_jobs(tmp_path, [
|
||||
{
|
||||
"name": "anaf-monitor",
|
||||
"cron": "0 10 * * 1-5",
|
||||
"enabled": True,
|
||||
"last_run": "2026-04-21T10:00:00+03:00",
|
||||
"next_run": "2026-04-22T10:00:00+03:00",
|
||||
"last_status": "ok",
|
||||
},
|
||||
])
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
|
||||
jobs = handler.captured["jobs"]
|
||||
assert len(jobs) == 1
|
||||
j = jobs[0]
|
||||
for k in ("id", "name", "time", "schedule", "ranToday",
|
||||
"lastStatus", "lastRunAtMs", "nextRunAtMs"):
|
||||
assert k in j, f"missing key {k!r} in response"
|
||||
# Echo-core has no separate id — fallback is name.
|
||||
assert j["id"] == "anaf-monitor"
|
||||
assert j["name"] == "anaf-monitor"
|
||||
assert j["time"] == "10:00"
|
||||
assert j["schedule"] == "0 10 * * 1-5"
|
||||
assert j["lastRunAtMs"] > 0
|
||||
assert j["nextRunAtMs"] is not None and j["nextRunAtMs"] > 0
|
||||
|
||||
|
||||
def test_ran_today_is_based_on_last_run_ms(tmp_path, monkeypatch, handler):
|
||||
"""If last_run is in the past (before today 00:00), ranToday is False and lastStatus is None."""
|
||||
import constants # type: ignore
|
||||
_write_jobs(tmp_path, [
|
||||
{
|
||||
"name": "ancient",
|
||||
"cron": "0 5 * * *",
|
||||
"enabled": True,
|
||||
"last_run": "2020-01-01T05:00:00+03:00",
|
||||
"next_run": "2026-04-22T05:00:00+03:00",
|
||||
"last_status": "ok",
|
||||
},
|
||||
])
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
|
||||
j = handler.captured["jobs"][0]
|
||||
assert j["ranToday"] is False
|
||||
# lastStatus is only surfaced when ranToday — else it's None.
|
||||
assert j["lastStatus"] is None
|
||||
|
||||
|
||||
def test_jobs_are_sorted_by_display_time(tmp_path, monkeypatch, handler):
|
||||
import constants # type: ignore
|
||||
_write_jobs(tmp_path, [
|
||||
{"name": "late", "cron": "0 22 * * *", "enabled": True},
|
||||
{"name": "early", "cron": "0 5 * * *", "enabled": True},
|
||||
{"name": "mid", "cron": "0 12 * * *", "enabled": True},
|
||||
])
|
||||
monkeypatch.setattr(constants, "BASE_DIR", tmp_path)
|
||||
handler.handle_cron_status()
|
||||
|
||||
names = [j["name"] for j in handler.captured["jobs"]]
|
||||
assert names == ["early", "mid", "late"]
|
||||
Reference in New Issue
Block a user