Files
ledgrab/server/tests/e2e/conftest.py
T
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

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