diff --git a/server/tests/api/test_audio_processing_templates_api.py b/server/tests/api/test_audio_processing_templates_api.py index d2ddd77..cd7b76a 100644 --- a/server/tests/api/test_audio_processing_templates_api.py +++ b/server/tests/api/test_audio_processing_templates_api.py @@ -1,9 +1,7 @@ """API tests for audio processing template endpoints.""" import pytest -from fastapi.testclient import TestClient -from wled_controller.main import app from wled_controller.config import get_config # Ensure audio filters registered @@ -17,6 +15,9 @@ AUTH = {"Authorization": f"Bearer {_api_key}"} if _api_key else {} @pytest.fixture(scope="module") def client(): """Provide a TestClient with lifespan (startup/shutdown) properly triggered.""" + from fastapi.testclient import TestClient + from wled_controller.main import app + with TestClient(app) as c: yield c diff --git a/server/tests/conftest.py b/server/tests/conftest.py index 12d246c..f7e295e 100644 --- a/server/tests/conftest.py +++ b/server/tests/conftest.py @@ -1,20 +1,52 @@ -"""Pytest configuration and shared fixtures.""" +"""Pytest configuration and shared fixtures. +IMPORTANT: This conftest patches the global config singleton BEFORE any test +module can import ``wled_controller.main``. ``main.py`` reads ``get_config()`` +at module level to open the database — if the singleton is not patched first, +the REAL production database (``data/ledgrab.db``) is opened and tests +read/write/delete production data. +""" + +import tempfile from datetime import datetime, timezone +from pathlib import Path import pytest -from wled_controller.config import Config, StorageConfig, ServerConfig, AuthConfig -from wled_controller.storage.database import Database -from wled_controller.storage.device_store import Device, DeviceStore -from wled_controller.storage.sync_clock import SyncClock -from wled_controller.storage.sync_clock_store import SyncClockStore -from wled_controller.storage.output_target_store import OutputTargetStore -from wled_controller.storage.automation import ( - Automation, +# --------------------------------------------------------------------------- +# ISOLATE ALL TESTS FROM PRODUCTION DATA — must happen before any test module +# imports ``wled_controller.main``. +# --------------------------------------------------------------------------- + +import wled_controller.config as _config_mod # noqa: E402 + +_test_tmp = Path(tempfile.mkdtemp(prefix="wled_test_")) +_test_db_path = str(_test_tmp / "test_ledgrab.db") +_test_assets_dir = str(_test_tmp / "test_assets") + +_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, + ), + }, ) -from wled_controller.storage.automation_store import AutomationStore -from wled_controller.storage.value_source_store import ValueSourceStore +_config_mod.config = _test_config + +# --------------------------------------------------------------------------- + +from wled_controller.config import Config, StorageConfig, ServerConfig, AuthConfig # noqa: E402 +from wled_controller.storage.database import Database # noqa: E402 +from wled_controller.storage.device_store import Device, DeviceStore # noqa: E402 +from wled_controller.storage.sync_clock import SyncClock # noqa: E402 +from wled_controller.storage.sync_clock_store import SyncClockStore # noqa: E402 +from wled_controller.storage.output_target_store import OutputTargetStore # noqa: E402 +from wled_controller.storage.automation import Automation # noqa: E402 +from wled_controller.storage.automation_store import AutomationStore # noqa: E402 +from wled_controller.storage.value_source_store import ValueSourceStore # noqa: E402 # --------------------------------------------------------------------------- @@ -242,3 +274,17 @@ def sample_calibration(): {"edge": "left", "led_start": 110, "led_count": 40, "reverse": True}, ], } + + +# --------------------------------------------------------------------------- +# Session cleanup — remove temporary test directory +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="session", autouse=True) +def _cleanup_test_tmp(): + """Remove the temporary test directory after all tests complete.""" + import shutil + + yield + shutil.rmtree(_test_tmp, ignore_errors=True) diff --git a/server/tests/test_api.py b/server/tests/test_api.py index bc0e564..30fabf5 100644 --- a/server/tests/test_api.py +++ b/server/tests/test_api.py @@ -4,31 +4,39 @@ import os import sys import pytest -from fastapi.testclient import TestClient -from wled_controller.main import app from wled_controller import __version__ from wled_controller.config import get_config -_has_display = bool(os.environ.get("DISPLAY") or sys.platform == "win32" or sys.platform == "darwin") +_has_display = bool( + os.environ.get("DISPLAY") or sys.platform == "win32" or sys.platform == "darwin" +) requires_display = pytest.mark.skipif(not _has_display, reason="No display available (headless CI)") -client = TestClient(app) - # Build auth header from the first configured API key _config = get_config() _api_key = next(iter(_config.auth.api_keys.values()), "") AUTH_HEADERS = {"Authorization": f"Bearer {_api_key}"} if _api_key else {} -def test_root_endpoint(): +@pytest.fixture(scope="module") +def client(): + """Provide a TestClient backed by the isolated test database.""" + from fastapi.testclient import TestClient + from wled_controller.main import app + + with TestClient(app, raise_server_exceptions=False) as c: + yield c + + +def test_root_endpoint(client): """Test root endpoint returns the HTML dashboard.""" response = client.get("/") assert response.status_code == 200 assert "text/html" in response.headers["content-type"] -def test_health_check(): +def test_health_check(client): """Test health check endpoint.""" response = client.get("/health") assert response.status_code == 200 @@ -38,7 +46,7 @@ def test_health_check(): assert "timestamp" in data -def test_version_endpoint(): +def test_version_endpoint(client): """Test version endpoint.""" response = client.get("/api/v1/version") assert response.status_code == 200 @@ -49,7 +57,7 @@ def test_version_endpoint(): @requires_display -def test_get_displays(): +def test_get_displays(client): """Test get displays endpoint (requires auth and a real display).""" response = client.get("/api/v1/config/displays", headers=AUTH_HEADERS) assert response.status_code == 200 @@ -69,7 +77,7 @@ def test_get_displays(): assert "is_primary" in display -def test_openapi_docs(): +def test_openapi_docs(client): """Test OpenAPI documentation is available.""" response = client.get("/openapi.json") assert response.status_code == 200 @@ -77,7 +85,7 @@ def test_openapi_docs(): assert data["info"]["version"] == __version__ -def test_swagger_ui(): +def test_swagger_ui(client): """Test Swagger UI is available.""" response = client.get("/docs") assert response.status_code == 200