Files
ledgrab/server/tests/api/test_audio_processing_templates_api.py
alexei.dolgolyov 123da1b5c4
Build Android APK / build-android (push) Failing after 1m45s
Lint & Test / test (push) Successful in 4m54s
fix: comprehensive security, stability, and code quality audit
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
2026-04-16 04:56:04 +03:00

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