123da1b5c4
Security: - Force API key auth for LAN (non-loopback) requests; remove shipped dev key - Block path-traversal in backup restore; require auth on backup endpoints - SSRF protection: DNS resolve + private/loopback/link-local IP rejection - AES-256-GCM encryption for HA tokens and MQTT passwords with auto-migration - WebSocket auth migrated from query-string to first-message protocol - Asset upload: extension allowlist, server-side mime, Content-Disposition - Update installer: SHA256 verification, tar/zip member validation - Tightened CORS (explicit methods/headers, no credentials) - ADB serial regex allowlist, webhook rate-limit key fix, log scrubbing Android: - Root-capture: ordered teardown, screenrecord respawn watchdog, child reaping - USB permission blocking API via CompletableDeferred - Python init crash guard with fatal-error screen - Moved root grant + QR generation off Main thread - Cached PyObject engine for per-frame bridge calls - Ordered ScreenCapture resource cleanup, allowBackup=false Python: - Replaced all asyncio.get_event_loop() with get_running_loop/to_thread - Split color_strip_sources.py (1683->5 files) and color_strip_stream.py (1324->7 files) into packages - Extracted FrameLimiter utility, migrated 9 stream loops - Provider base-class reuse, WLED state caching + URL normalization - Narrowed broad except-pass in WS routes, threading fixes in BaseStore Frontend: - XSS fix: escapeHtml on dynamic option labels, reconcile-based list renders - Typed DOM helpers, safe localStorage access, AbortController listener hygiene - openAuthedWs helper for first-message WS auth protocol - Migrated remaining plain <select>s to IconSelect/EntitySelect Design: - WCAG AA primary color on light theme (#2e7d32, 5.4:1 contrast) - Android TV 10-foot breakpoint (tv.css) - Consolidated z-index tokens, unified easing, card-running GPU hints
162 lines
5.3 KiB
Python
162 lines
5.3 KiB
Python
"""API tests for audio processing template endpoints."""
|
|
|
|
import pytest
|
|
|
|
from ledgrab.config import get_config
|
|
|
|
# Ensure audio filters registered
|
|
import ledgrab.core.audio.filters # noqa: F401
|
|
|
|
AUTH: dict = {}
|
|
|
|
|
|
@pytest.fixture(autouse=True, scope="module")
|
|
def _populate_auth():
|
|
"""Resolve the active API key once per module — the global config may
|
|
have been swapped by a session-scoped conftest after this module was
|
|
first imported, so we cannot capture at import time."""
|
|
key = next(iter(get_config().auth.api_keys.values()), "")
|
|
AUTH.clear()
|
|
if key:
|
|
AUTH["Authorization"] = f"Bearer {key}"
|
|
yield
|
|
AUTH.clear()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def client():
|
|
"""Provide a TestClient with lifespan (startup/shutdown) properly triggered."""
|
|
from fastapi.testclient import TestClient
|
|
from ledgrab.main import app
|
|
|
|
with TestClient(app) as c:
|
|
yield c
|
|
|
|
|
|
# Track created template IDs for cleanup
|
|
_created_ids: list[str] = []
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def cleanup_after_test(client):
|
|
"""Clean up created templates after each test."""
|
|
yield
|
|
for tid in list(_created_ids):
|
|
client.delete(f"/api/v1/audio-processing-templates/{tid}", headers=AUTH)
|
|
_created_ids.clear()
|
|
|
|
|
|
def _create(client, name: str, filters: list | None = None, **kwargs) -> dict:
|
|
"""Helper: create a template and track for cleanup."""
|
|
body = {"name": name, "filters": filters or [], **kwargs}
|
|
resp = client.post("/api/v1/audio-processing-templates", json=body, headers=AUTH)
|
|
if resp.status_code == 201:
|
|
_created_ids.append(resp.json()["id"])
|
|
return resp
|
|
|
|
|
|
class TestAudioProcessingTemplateAPI:
|
|
"""Test /api/v1/audio-processing-templates endpoints."""
|
|
|
|
def test_list(self, client):
|
|
resp = client.get("/api/v1/audio-processing-templates", headers=AUTH)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "templates" in data
|
|
assert "count" in data
|
|
|
|
def test_create(self, client):
|
|
resp = _create(
|
|
client,
|
|
"API Test Template",
|
|
filters=[{"filter_id": "gain", "options": {"factor": 2.0}}],
|
|
description="A test template",
|
|
tags=["test"],
|
|
)
|
|
assert resp.status_code == 201
|
|
data = resp.json()
|
|
assert data["name"] == "API Test Template"
|
|
assert data["id"].startswith("apt_")
|
|
assert len(data["filters"]) == 1
|
|
assert data["filters"][0]["filter_id"] == "gain"
|
|
assert data["description"] == "A test template"
|
|
assert data["tags"] == ["test"]
|
|
|
|
def test_create_invalid_filter_returns_400(self, client):
|
|
resp = client.post(
|
|
"/api/v1/audio-processing-templates",
|
|
json={
|
|
"name": "Bad Template",
|
|
"filters": [{"filter_id": "nonexistent", "options": {}}],
|
|
},
|
|
headers=AUTH,
|
|
)
|
|
assert resp.status_code == 400
|
|
|
|
def test_get_by_id(self, client):
|
|
create_resp = _create(client, "Fetchable Template")
|
|
tid = create_resp.json()["id"]
|
|
|
|
resp = client.get(f"/api/v1/audio-processing-templates/{tid}", headers=AUTH)
|
|
assert resp.status_code == 200
|
|
assert resp.json()["name"] == "Fetchable Template"
|
|
|
|
def test_get_nonexistent_returns_404(self, client):
|
|
resp = client.get("/api/v1/audio-processing-templates/apt_nonexistent", headers=AUTH)
|
|
assert resp.status_code == 404
|
|
|
|
def test_update(self, client):
|
|
create_resp = _create(client, "Original API Template")
|
|
tid = create_resp.json()["id"]
|
|
|
|
resp = client.put(
|
|
f"/api/v1/audio-processing-templates/{tid}",
|
|
json={
|
|
"name": "Updated API Template",
|
|
"filters": [{"filter_id": "inverter", "options": {}}],
|
|
},
|
|
headers=AUTH,
|
|
)
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert data["name"] == "Updated API Template"
|
|
assert len(data["filters"]) == 1
|
|
|
|
def test_update_nonexistent_returns_404(self, client):
|
|
resp = client.put(
|
|
"/api/v1/audio-processing-templates/apt_nonexistent",
|
|
json={"name": "X"},
|
|
headers=AUTH,
|
|
)
|
|
assert resp.status_code == 404
|
|
|
|
def test_delete(self, client):
|
|
create_resp = _create(client, "To Delete API")
|
|
tid = create_resp.json()["id"]
|
|
_created_ids.remove(tid)
|
|
|
|
resp = client.delete(f"/api/v1/audio-processing-templates/{tid}", headers=AUTH)
|
|
assert resp.status_code == 204
|
|
|
|
resp2 = client.get(f"/api/v1/audio-processing-templates/{tid}", headers=AUTH)
|
|
assert resp2.status_code == 404
|
|
|
|
def test_delete_nonexistent_returns_404(self, client):
|
|
resp = client.delete("/api/v1/audio-processing-templates/apt_nonexistent", headers=AUTH)
|
|
assert resp.status_code == 404
|
|
|
|
|
|
class TestFilterRegistryAPI:
|
|
"""Test /api/v1/audio-filters endpoint."""
|
|
|
|
def test_list_filters(self, client):
|
|
resp = client.get("/api/v1/audio-filters", headers=AUTH)
|
|
if resp.status_code == 404:
|
|
pytest.skip("Audio filters registry endpoint not implemented")
|
|
assert resp.status_code == 200
|
|
data = resp.json()
|
|
assert "filters" in data
|
|
ids = {f["filter_id"] for f in data["filters"]}
|
|
assert "gain" in ids
|
|
assert "inverter" in ids
|