Files
ledgrab/server/tests/test_home_assistant_host_validation.py
alexei.dolgolyov 17dd2e02ba fix: resolve comprehensive review findings (security, concurrency, perf, Android, UI)
Multi-dimension review of v0.8.2. Excludes the deliberately deferred
default_config.yaml weak-default-key item (C1).

Backend:
- calibration: create_default_calibration no longer exceeds led_count for small
  odd counts (bounded trim + regression test)
- game-integration: generic webhook now requires auth_token; constant-time
  compare_digest in all adapters; per-IP failed-auth rate limit on the ingest
  route; auth_token encrypted at rest via secret_box (migration-safe)
- playlist engine: serialize _state/_task under the lifecycle lock to close a
  delete-mid-play race (+ concurrency tests)
- main: stop the calibration session on shutdown (restore prior target)
- home_assistant: validate HA host via the LAN classifier on create/update
- perf: drop slow preview-WS clients instead of blocking the send loop; cache
  composite full-strip resize linspaces; effect_stream lava reuses scratch

Frontend:
- setup/auto-calibration wizard: guard _state after awaits (cancel-safe), await
  session teardown before output start, busy-gate skip-calibration, manual
  display input keeps focus, move focus on step change
- calibration: destroy EntitySelect on modal close
- color-strips test: dirty-flag-gated render + cached ctx/ImageData
- a11y/TV: focus-visible for new wizard/auto-cal/corner controls, aria-labels on
  the spatial corner/edge picker; theme-aware syntax tokens; dead/undefined CSS
  tokens removed; .modal-error styled; i18n titles (en/ru/zh)

Android:
- ApiKeyManager: EncryptedSharedPreferences with verified, data-safe legacy
  migration that never rotates an existing key
- CaptureService: validate MediaProjection token before promoting; satisfy the
  startForeground 5s contract on the bail path
- NotificationListener: connection-scoped executor with lazy fallback
- BLE: request BLUETOOTH_SCAN/CONNECT at runtime + guard handler-thread
  SecurityExceptions
- Root: cancellation-aware su grant probe

Adds 14 tests. Gate: ruff + tsc 5.9.3 + esbuild + pytest (2185 passed) +
compileDebugKotlin all green.
2026-06-09 16:35:08 +03:00

53 lines
1.8 KiB
Python

"""Tests for Home Assistant source host classification.
The HA source host is user-supplied and stored as ``host:port`` (e.g.
``192.168.1.100:8123``). Before the WebSocket runtime connects to it, the
route layer gates the host with the shared LAN classifier so an
(authenticated or default-anonymous) caller cannot weaponise it into a
public network-scan oracle — mirroring the LED device providers.
"""
import pytest
from ledgrab.api.routes.home_assistant import _validate_ha_host
@pytest.mark.parametrize(
"host",
[
"127.0.0.1:8123",
"10.0.0.5:8123",
"192.168.1.100:8123",
"172.16.0.10", # no port
"homeassistant.local:8123", # mDNS label
"hass", # bare hostname
"[fe80::1]:8123", # bracketed IPv6 link-local + port
"[fc00::1]:8123", # bracketed IPv6 ULA + port
"169.254.169.254:80", # link-local — allowed by the shared LAN policy
"", # empty passes (upstream schema requires non-empty)
None, # update path may pass None (host unchanged)
],
)
def test_validate_ha_host_accepts_lan(host) -> None:
"""Loopback / private / link-local / hostnames must not raise.
Link-local (169.254/16, fe80::/10) is part of the shared
``validate_lan_host`` LAN policy used by every LED device provider, so
HA reuses it unchanged rather than hand-rolling a stricter variant.
"""
_validate_ha_host(host)
@pytest.mark.parametrize(
"host",
[
"1.1.1.1:8123", # public IPv4 + port
"8.8.8.8", # public IPv4, no port
"[2606:4700:4700::1111]:8123", # public IPv6 + port
],
)
def test_validate_ha_host_rejects_public(host) -> None:
"""Genuinely-public IPs (the network-scan-oracle risk) are rejected."""
with pytest.raises(ValueError, match="LedGrab is LAN-only"):
_validate_ha_host(host)