fc3d: settings API exposes scheduling knobs with bounds validation
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,9 @@ _EDITABLE_FIELDS = (
|
||||
"phash_threshold",
|
||||
"download_rate_limit_seconds",
|
||||
"download_validate_files",
|
||||
"download_schedule_default_seconds",
|
||||
"download_event_retention_days",
|
||||
"download_failure_warning_threshold",
|
||||
)
|
||||
|
||||
|
||||
@@ -42,6 +45,9 @@ async def get_import_settings():
|
||||
"phash_threshold": row.phash_threshold,
|
||||
"download_rate_limit_seconds": row.download_rate_limit_seconds,
|
||||
"download_validate_files": row.download_validate_files,
|
||||
"download_schedule_default_seconds": row.download_schedule_default_seconds,
|
||||
"download_event_retention_days": row.download_event_retention_days,
|
||||
"download_failure_warning_threshold": row.download_failure_warning_threshold,
|
||||
})
|
||||
|
||||
|
||||
@@ -73,6 +79,25 @@ async def update_import_settings():
|
||||
{"error": "download_validate_files must be a boolean"}
|
||||
), 400
|
||||
|
||||
# FC-3d scheduling knobs — bounds validation.
|
||||
def _bad_int(name: str, lo: int, hi: int):
|
||||
return jsonify(
|
||||
{"error": f"{name} must be an integer in [{lo}, {hi}]"}
|
||||
), 400
|
||||
|
||||
if "download_schedule_default_seconds" in body:
|
||||
v = body["download_schedule_default_seconds"]
|
||||
if not isinstance(v, int) or isinstance(v, bool) or v < 60 or v > 86400:
|
||||
return _bad_int("download_schedule_default_seconds", 60, 86400)
|
||||
if "download_event_retention_days" in body:
|
||||
v = body["download_event_retention_days"]
|
||||
if not isinstance(v, int) or isinstance(v, bool) or v < 1 or v > 3650:
|
||||
return _bad_int("download_event_retention_days", 1, 3650)
|
||||
if "download_failure_warning_threshold" in body:
|
||||
v = body["download_failure_warning_threshold"]
|
||||
if not isinstance(v, int) or isinstance(v, bool) or v < 1 or v > 100:
|
||||
return _bad_int("download_failure_warning_threshold", 1, 100)
|
||||
|
||||
async with get_session() as session:
|
||||
row = (
|
||||
await session.execute(select(ImportSettings).where(ImportSettings.id == 1))
|
||||
|
||||
@@ -82,6 +82,68 @@ async def test_system_stats_shape(client):
|
||||
assert status in body["tasks"]
|
||||
|
||||
|
||||
# --- FC-3d: scheduling knobs ----------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_includes_scheduling_defaults(client):
|
||||
resp = await client.get("/api/settings/import")
|
||||
body = await resp.get_json()
|
||||
assert body["download_schedule_default_seconds"] == 28800
|
||||
assert body["download_event_retention_days"] == 90
|
||||
assert body["download_failure_warning_threshold"] == 5
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_scheduling_defaults(client):
|
||||
resp = await client.patch("/api/settings/import", json={
|
||||
"download_schedule_default_seconds": 7200,
|
||||
"download_event_retention_days": 30,
|
||||
"download_failure_warning_threshold": 3,
|
||||
})
|
||||
assert resp.status_code == 200
|
||||
body = await resp.get_json()
|
||||
assert body["download_schedule_default_seconds"] == 7200
|
||||
assert body["download_event_retention_days"] == 30
|
||||
assert body["download_failure_warning_threshold"] == 3
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_rejects_below_min_interval(client):
|
||||
resp = await client.patch(
|
||||
"/api/settings/import",
|
||||
json={"download_schedule_default_seconds": 30},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_rejects_above_max_interval(client):
|
||||
resp = await client.patch(
|
||||
"/api/settings/import",
|
||||
json={"download_schedule_default_seconds": 90000},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_rejects_zero_retention_days(client):
|
||||
resp = await client.patch(
|
||||
"/api/settings/import",
|
||||
json={"download_event_retention_days": 0},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_patch_rejects_zero_failure_threshold(client):
|
||||
resp = await client.patch(
|
||||
"/api/settings/import",
|
||||
json={"download_failure_warning_threshold": 0},
|
||||
)
|
||||
assert resp.status_code == 400
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_system_stats_integrity_counts(client, db):
|
||||
from backend.app.models import ImageRecord
|
||||
|
||||
Reference in New Issue
Block a user