836f733a2e
- Add get_api_key() and require_api_key() to auth.py - Add generic key-value settings storage to SettingsStore - Add test_api_key_auth to verify the implementation - Uses secrets.compare_digest for timing-safe comparison - Auto-generates API key on first use and stores in settings DB
156 lines
5.0 KiB
Python
156 lines
5.0 KiB
Python
import tempfile
|
|
from pathlib import Path
|
|
|
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
|
|
|
|
|
def test_backup_schema_created():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test_settings.sqlite"
|
|
store = SettingsStore(db_path)
|
|
store.init_schema()
|
|
with store.connect() as conn:
|
|
tables = conn.execute(
|
|
"SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'backup_%'"
|
|
).fetchall()
|
|
table_names = [t[0] for t in tables]
|
|
assert "backup_jobs" in table_names
|
|
assert "backup_runs" in table_names
|
|
assert "backup_alerts" in table_names
|
|
|
|
|
|
def test_backup_report_model():
|
|
from media_library_viewer_api.models.backups import BackupReportRequest
|
|
|
|
report = BackupReportRequest(
|
|
name="test-backup",
|
|
started_at="2026-05-11T02:00:00Z",
|
|
status="success",
|
|
ended_at="2026-05-11T02:15:00Z",
|
|
duration_ms=900000,
|
|
bytes_transferred=1024,
|
|
)
|
|
assert report.name == "test-backup"
|
|
assert report.status == "success"
|
|
|
|
|
|
def test_create_job_and_run():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test_settings.sqlite"
|
|
store = SettingsStore(db_path)
|
|
store.init_schema()
|
|
|
|
job = store.upsert_backup_job({
|
|
"name": "test-job",
|
|
"source": "server-a:/data",
|
|
"target": "server-b:/backups",
|
|
"schedule_interval_seconds": 86400,
|
|
})
|
|
assert job["name"] == "test-job"
|
|
|
|
run = store.create_backup_run({
|
|
"job_id": job["id"],
|
|
"started_at": 1715392800,
|
|
"status": "success",
|
|
"ended_at": 1715393700,
|
|
"duration_ms": 900000,
|
|
"bytes_transferred": 1024,
|
|
})
|
|
assert run["job_id"] == job["id"]
|
|
assert run["status"] == "success"
|
|
|
|
|
|
def test_api_key_auth():
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test_settings.sqlite"
|
|
store = SettingsStore(db_path)
|
|
store.init_schema()
|
|
|
|
from fastapi import HTTPException
|
|
|
|
from media_library_viewer_api.auth import get_api_key, require_api_key
|
|
from media_library_viewer_api.services import settings_store
|
|
|
|
# Monkey-patch the global store for this test
|
|
original_store = settings_store._store
|
|
settings_store._store = store
|
|
|
|
# Reset the cached API key
|
|
import media_library_viewer_api.auth as auth_module
|
|
auth_module._API_KEY = None
|
|
|
|
try:
|
|
# Should generate and return an API key
|
|
api_key = get_api_key()
|
|
assert api_key
|
|
assert len(api_key) > 0
|
|
|
|
# Should raise 401 without key
|
|
try:
|
|
require_api_key("")
|
|
assert False, "Should have raised"
|
|
except HTTPException as e:
|
|
assert e.status_code == 401
|
|
|
|
# Should raise 401 with wrong key
|
|
try:
|
|
require_api_key("Bearer wrong-key")
|
|
assert False, "Should have raised"
|
|
except HTTPException as e:
|
|
assert e.status_code == 401
|
|
|
|
# Should succeed with correct key
|
|
result = require_api_key(f"Bearer {api_key}")
|
|
assert result == f"Bearer {api_key}"
|
|
finally:
|
|
settings_store._store = original_store
|
|
auth_module._API_KEY = None
|
|
|
|
|
|
def test_alert_failed_status():
|
|
from media_library_viewer_api.services.backup_alert_engine import generate_alerts_for_run
|
|
|
|
run = {
|
|
"id": "run-1",
|
|
"job_id": "job-1",
|
|
"status": "failure",
|
|
"bytes_transferred": 0,
|
|
"duration_ms": 1000,
|
|
"started_at": 1715392800,
|
|
}
|
|
alerts = generate_alerts_for_run(run, [], None)
|
|
assert len(alerts) == 1
|
|
assert alerts[0]["alert_type"] == "failed_status"
|
|
assert alerts[0]["severity"] == "critical"
|
|
|
|
|
|
def test_backup_poller_detects_missed_schedule():
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
from media_library_viewer_api.services.backup_poller import BackupAlertPoller
|
|
from media_library_viewer_api.services.settings_store import SettingsStore
|
|
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
db_path = Path(tmpdir) / "test_settings.sqlite"
|
|
store = SettingsStore(db_path)
|
|
store.init_schema()
|
|
|
|
import time
|
|
# Create job with created_at in the past so it's already overdue
|
|
past_time = int(time.time()) - 200
|
|
job = store.upsert_backup_job({
|
|
"name": "old-job",
|
|
"schedule_interval_seconds": 60,
|
|
})
|
|
# Override created_at to be in the past so missed schedule fires
|
|
with store.connect() as conn:
|
|
conn.execute("UPDATE backup_jobs SET created_at = ? WHERE id = ?", (past_time, job["id"]))
|
|
|
|
poller = BackupAlertPoller()
|
|
poller._run_cycle(store)
|
|
|
|
alerts = store.list_backup_alerts(job_id=job["id"])
|
|
assert len(alerts) == 1
|
|
assert alerts[0]["alert_type"] == "missed_schedule"
|