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",
|
"phash_threshold",
|
||||||
"download_rate_limit_seconds",
|
"download_rate_limit_seconds",
|
||||||
"download_validate_files",
|
"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,
|
"phash_threshold": row.phash_threshold,
|
||||||
"download_rate_limit_seconds": row.download_rate_limit_seconds,
|
"download_rate_limit_seconds": row.download_rate_limit_seconds,
|
||||||
"download_validate_files": row.download_validate_files,
|
"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"}
|
{"error": "download_validate_files must be a boolean"}
|
||||||
), 400
|
), 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:
|
async with get_session() as session:
|
||||||
row = (
|
row = (
|
||||||
await session.execute(select(ImportSettings).where(ImportSettings.id == 1))
|
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"]
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_system_stats_integrity_counts(client, db):
|
async def test_system_stats_integrity_counts(client, db):
|
||||||
from backend.app.models import ImageRecord
|
from backend.app.models import ImageRecord
|
||||||
|
|||||||
Reference in New Issue
Block a user