"""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 wled_controller.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 wled_controller.config as _config_mod # noqa: E402 # Build a Config that mirrors production settings but with isolated paths. _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, ), }, ) # 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 wled_controller.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 wled_controller.storage.base_store import unfreeze_saves unfreeze_saves() import wled_controller.storage.database as _db_mod _db_mod._writes_frozen = False from wled_controller.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