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
118 lines
4.2 KiB
Python
118 lines
4.2 KiB
Python
"""Shared fixtures for end-to-end API tests.
|
|
|
|
Uses the real FastAPI app with a session-scoped TestClient.
|
|
All e2e tests run against an ISOLATED temporary database and assets
|
|
directory — never the production data.
|
|
"""
|
|
|
|
import shutil
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Isolate e2e tests from production data.
|
|
#
|
|
# We must set the config singleton BEFORE ledgrab.main is imported,
|
|
# because main.py reads get_config() at module level to create the DB and
|
|
# all stores. By forcing the singleton here we guarantee the app opens a
|
|
# throwaway SQLite file in a temp directory.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_e2e_tmp = Path(tempfile.mkdtemp(prefix="wled_e2e_"))
|
|
_test_db_path = str(_e2e_tmp / "test_ledgrab.db")
|
|
_test_assets_dir = str(_e2e_tmp / "test_assets")
|
|
|
|
import ledgrab.config as _config_mod # noqa: E402
|
|
|
|
# Build a Config that mirrors production settings but with isolated paths.
|
|
# Always inject a test API key so auth-enforcement tests have something
|
|
# concrete to authenticate against (and reject when omitted).
|
|
_original_config = _config_mod.Config.load()
|
|
_test_config = _original_config.model_copy(
|
|
update={
|
|
"storage": _config_mod.StorageConfig(database_file=_test_db_path),
|
|
"assets": _config_mod.AssetsConfig(
|
|
assets_dir=_test_assets_dir,
|
|
max_file_size_mb=_original_config.assets.max_file_size_mb,
|
|
),
|
|
"auth": _config_mod.AuthConfig(api_keys={"e2e": "e2e-test-api-key-12345"}),
|
|
},
|
|
)
|
|
# Install as the global singleton so all subsequent get_config() calls
|
|
# (including main.py module-level code) use isolated paths.
|
|
_config_mod.config = _test_config
|
|
|
|
API_KEY = next(iter(_test_config.auth.api_keys.values()), "")
|
|
AUTH_HEADERS = {"Authorization": f"Bearer {API_KEY}"}
|
|
|
|
|
|
@pytest.fixture(scope="session")
|
|
def _test_client():
|
|
"""Session-scoped TestClient to avoid lifespan re-entry issues.
|
|
|
|
The app's lifespan (MQTT, automation engine, health monitoring, etc.)
|
|
starts once for the entire e2e test session and shuts down after all
|
|
tests complete. The app uses the isolated test database set above.
|
|
"""
|
|
from fastapi.testclient import TestClient
|
|
from ledgrab.main import app
|
|
|
|
with TestClient(app, raise_server_exceptions=False) as c:
|
|
yield c
|
|
|
|
# Clean up temp directory after all e2e tests finish
|
|
shutil.rmtree(_e2e_tmp, ignore_errors=True)
|
|
|
|
|
|
@pytest.fixture
|
|
def client(_test_client):
|
|
"""Per-test client with auth headers and clean stores.
|
|
|
|
Clears all entity stores before each test so tests are independent.
|
|
"""
|
|
_clear_stores()
|
|
_test_client.headers["Authorization"] = f"Bearer {API_KEY}"
|
|
yield _test_client
|
|
# Clean up after test
|
|
_clear_stores()
|
|
|
|
|
|
def _clear_stores():
|
|
"""Remove all entities from all stores for test isolation."""
|
|
# Reset frozen-writes flags that restore flows set.
|
|
# Without this, subsequent tests can't persist data.
|
|
from ledgrab.storage.base_store import unfreeze_saves
|
|
|
|
unfreeze_saves()
|
|
import ledgrab.storage.database as _db_mod
|
|
|
|
_db_mod._writes_frozen = False
|
|
|
|
from ledgrab.api import dependencies as deps
|
|
|
|
store_clearers = [
|
|
(deps.get_device_store, "get_all_devices", "delete_device"),
|
|
(deps.get_output_target_store, "get_all_targets", "delete_target"),
|
|
(deps.get_color_strip_store, "get_all_sources", "delete_source"),
|
|
(deps.get_value_source_store, "get_all", "delete"),
|
|
(deps.get_sync_clock_store, "get_all", "delete"),
|
|
(deps.get_automation_store, "get_all", "delete"),
|
|
(deps.get_scene_preset_store, "get_all", "delete"),
|
|
(deps.get_asset_store, "get_all_assets", "delete_asset"),
|
|
]
|
|
for getter, list_method, delete_method in store_clearers:
|
|
try:
|
|
store = getter()
|
|
items = getattr(store, list_method)()
|
|
for item in items:
|
|
item_id = getattr(item, "id", getattr(item, "device_id", None))
|
|
if item_id:
|
|
try:
|
|
getattr(store, delete_method)(item_id)
|
|
except Exception:
|
|
pass
|
|
except RuntimeError:
|
|
pass # Store not initialized yet
|