refactor: comprehensive code quality, security, and release readiness improvements
Some checks failed
Lint & Test / test (push) Failing after 48s
Some checks failed
Lint & Test / test (push) Failing after 48s
Security: tighten CORS defaults, add webhook rate limiting, fix XSS in automations, guard WebSocket JSON.parse, validate ADB address input, seal debug exception leak, URL-encode WS tokens, CSS.escape in selectors. Code quality: add Pydantic models for brightness/power endpoints, fix thread safety and name uniqueness in DeviceStore, immutable update pattern, split 6 oversized files into 16 focused modules, enable TypeScript strictNullChecks (741→102 errors), type state variables, add dom-utils helper, migrate 3 modules from inline onclick to event delegation, ProcessorDependencies dataclass. Performance: async store saves, health endpoint log level, command palette debounce, optimized entity-events comparison, fix service worker precache list. Testing: expand from 45 to 293 passing tests — add store tests (141), route tests (25), core logic tests (42), E2E flow tests (33), organize into tests/api/, tests/storage/, tests/core/, tests/e2e/. DevOps: CI test pipeline, pre-commit config, Dockerfile multi-stage build with non-root user and health check, docker-compose improvements, version bump to 0.2.0. Docs: rewrite CLAUDE.md (202→56 lines), server/CLAUDE.md (212→76), create contexts/server-operations.md, fix .js→.ts references, fix env var prefix in README, rewrite INSTALLATION.md, add CONTRIBUTING.md and .env.example.
This commit is contained in:
@@ -1,13 +1,39 @@
|
||||
"""Pytest configuration and fixtures."""
|
||||
"""Pytest configuration and shared fixtures."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
from pathlib import Path
|
||||
|
||||
from wled_controller.config import Config, StorageConfig, ServerConfig, AuthConfig
|
||||
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,
|
||||
AlwaysCondition,
|
||||
WebhookCondition,
|
||||
)
|
||||
from wled_controller.storage.automation_store import AutomationStore
|
||||
from wled_controller.storage.value_source import StaticValueSource, ValueSource
|
||||
from wled_controller.storage.value_source_store import ValueSourceStore
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Directory / path fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_data_dir(tmp_path):
|
||||
"""Provide a temporary directory for test data."""
|
||||
return tmp_path / "data"
|
||||
d = tmp_path / "data"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -16,6 +42,198 @@ def test_config_dir(tmp_path):
|
||||
return tmp_path / "config"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_store_dir(tmp_path):
|
||||
"""Provide a temp directory for JSON store files, cleaned up after tests."""
|
||||
d = tmp_path / "stores"
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def test_config(tmp_path):
|
||||
"""A Config instance with temp directories for all store files."""
|
||||
data_dir = tmp_path / "data"
|
||||
data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
storage = StorageConfig(
|
||||
devices_file=str(data_dir / "devices.json"),
|
||||
templates_file=str(data_dir / "capture_templates.json"),
|
||||
postprocessing_templates_file=str(data_dir / "postprocessing_templates.json"),
|
||||
picture_sources_file=str(data_dir / "picture_sources.json"),
|
||||
output_targets_file=str(data_dir / "output_targets.json"),
|
||||
pattern_templates_file=str(data_dir / "pattern_templates.json"),
|
||||
color_strip_sources_file=str(data_dir / "color_strip_sources.json"),
|
||||
audio_sources_file=str(data_dir / "audio_sources.json"),
|
||||
audio_templates_file=str(data_dir / "audio_templates.json"),
|
||||
value_sources_file=str(data_dir / "value_sources.json"),
|
||||
automations_file=str(data_dir / "automations.json"),
|
||||
scene_presets_file=str(data_dir / "scene_presets.json"),
|
||||
color_strip_processing_templates_file=str(data_dir / "color_strip_processing_templates.json"),
|
||||
sync_clocks_file=str(data_dir / "sync_clocks.json"),
|
||||
)
|
||||
|
||||
return Config(
|
||||
server=ServerConfig(host="127.0.0.1", port=9999),
|
||||
auth=AuthConfig(api_keys={"test": "test-api-key-12345"}),
|
||||
storage=storage,
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Store fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def device_store(temp_store_dir):
|
||||
"""Provide a DeviceStore backed by a temp file."""
|
||||
return DeviceStore(temp_store_dir / "devices.json")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sync_clock_store(temp_store_dir):
|
||||
"""Provide a SyncClockStore backed by a temp file."""
|
||||
return SyncClockStore(str(temp_store_dir / "sync_clocks.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def output_target_store(temp_store_dir):
|
||||
"""Provide an OutputTargetStore backed by a temp file."""
|
||||
return OutputTargetStore(str(temp_store_dir / "output_targets.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def automation_store(temp_store_dir):
|
||||
"""Provide an AutomationStore backed by a temp file."""
|
||||
return AutomationStore(str(temp_store_dir / "automations.json"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def value_source_store(temp_store_dir):
|
||||
"""Provide a ValueSourceStore backed by a temp file."""
|
||||
return ValueSourceStore(str(temp_store_dir / "value_sources.json"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Sample entity factories
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_device():
|
||||
"""Provide a sample device configuration dict."""
|
||||
return {
|
||||
"id": "test_device_001",
|
||||
"name": "Test WLED Device",
|
||||
"url": "http://192.168.1.100",
|
||||
"led_count": 150,
|
||||
"enabled": True,
|
||||
"device_type": "wled",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_device():
|
||||
"""Factory fixture: call make_device(name=..., **overrides) to build a Device."""
|
||||
_counter = 0
|
||||
|
||||
def _factory(name=None, **kwargs):
|
||||
nonlocal _counter
|
||||
_counter += 1
|
||||
defaults = dict(
|
||||
device_id=f"device_test_{_counter:04d}",
|
||||
name=name or f"Device {_counter}",
|
||||
url=f"http://192.168.1.{_counter}",
|
||||
led_count=150,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Device(**defaults)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_sync_clock():
|
||||
"""Factory fixture: call make_sync_clock(name=..., **overrides)."""
|
||||
_counter = 0
|
||||
|
||||
def _factory(name=None, **kwargs):
|
||||
nonlocal _counter
|
||||
_counter += 1
|
||||
now = datetime.now(timezone.utc)
|
||||
defaults = dict(
|
||||
id=f"sc_test_{_counter:04d}",
|
||||
name=name or f"Clock {_counter}",
|
||||
speed=1.0,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return SyncClock(**defaults)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_automation():
|
||||
"""Factory fixture: call make_automation(name=..., **overrides)."""
|
||||
_counter = 0
|
||||
|
||||
def _factory(name=None, **kwargs):
|
||||
nonlocal _counter
|
||||
_counter += 1
|
||||
now = datetime.now(timezone.utc)
|
||||
defaults = dict(
|
||||
id=f"auto_test_{_counter:04d}",
|
||||
name=name or f"Automation {_counter}",
|
||||
enabled=True,
|
||||
condition_logic="or",
|
||||
conditions=[],
|
||||
scene_preset_id=None,
|
||||
deactivation_mode="none",
|
||||
deactivation_scene_preset_id=None,
|
||||
created_at=now,
|
||||
updated_at=now,
|
||||
)
|
||||
defaults.update(kwargs)
|
||||
return Automation(**defaults)
|
||||
|
||||
return _factory
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Authenticated test client
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def authenticated_client(test_config, monkeypatch):
|
||||
"""Provide a FastAPI TestClient with auth header pre-set.
|
||||
|
||||
Patches global config so the app uses temp storage paths.
|
||||
"""
|
||||
import wled_controller.config as config_mod
|
||||
monkeypatch.setattr(config_mod, "config", test_config)
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
from wled_controller.main import app
|
||||
|
||||
client = TestClient(app, raise_server_exceptions=False)
|
||||
client.headers["Authorization"] = "Bearer test-api-key-12345"
|
||||
return client
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Calibration sample (kept from original conftest)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_calibration():
|
||||
"""Provide a sample calibration configuration."""
|
||||
@@ -29,21 +247,3 @@ def sample_calibration():
|
||||
{"edge": "left", "led_start": 110, "led_count": 40, "reverse": True},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_device():
|
||||
"""Provide a sample device configuration."""
|
||||
return {
|
||||
"id": "test_device_001",
|
||||
"name": "Test WLED Device",
|
||||
"url": "http://192.168.1.100",
|
||||
"led_count": 150,
|
||||
"enabled": True,
|
||||
"settings": {
|
||||
"display_index": 0,
|
||||
"fps": 30,
|
||||
"border_width": 10,
|
||||
"brightness": 0.8,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user