Files
ledgrab/server/tests/api/routes/test_output_target_response_registry.py
T
alexei.dolgolyov 2f15fbb752 refactor(output-targets): registry + coverage assertion for response builders
``_target_to_response`` in ``api/routes/output_targets.py`` used to be
an isinstance ladder over the three OutputTarget subclasses with a
silent fallback that fabricated a ``LedOutputTargetResponse`` for
unknown types (audit finding H3). The fallback masked exactly the
kind of bug we hit on the CSS side in Phase 1.1: a new target subclass
slipped past the ladder and got mis-shaped on the wire.

Replace the ladder with a ``_TARGET_RESPONSE_BUILDERS`` dict keyed by
the concrete subclass plus an import-time
``_assert_target_response_coverage()`` that requires the registry to
exactly match ``{WledOutputTarget, HALightOutputTarget,
Z2MLightOutputTarget}``. ``_target_to_response`` now raises
``RuntimeError`` instead of silently fabricating a LED response for an
unknown subclass — coverage is asserted at import so this branch is
unreachable in normal operation.

Tests: 5 new regression tests cover bijection between expected classes
and registered builders, callable shape, the rogue-target-raises
contract, and missing/extra entry rejection in the assertion. 24
existing output-target tests stay green; ruff clean.
2026-05-23 00:03:01 +03:00

70 lines
2.3 KiB
Python

"""Tests for the output-target response-builder registry.
Locks in the safety net that replaced the previous silent
``LedOutputTargetResponse(defaults)`` fallback in
``output_targets._target_to_response``.
"""
from __future__ import annotations
import pytest
from ledgrab.api.routes import output_targets
from ledgrab.api.routes.output_targets import (
_assert_target_response_coverage,
_target_to_response,
_TARGET_RESPONSE_BUILDERS,
)
from ledgrab.storage.ha_light_output_target import HALightOutputTarget
from ledgrab.storage.wled_output_target import WledOutputTarget
from ledgrab.storage.z2m_light_output_target import Z2MLightOutputTarget
EXPECTED_TARGET_CLASSES = {WledOutputTarget, HALightOutputTarget, Z2MLightOutputTarget}
def test_registry_covers_every_known_target_subclass():
assert set(_TARGET_RESPONSE_BUILDERS.keys()) == EXPECTED_TARGET_CLASSES
def test_every_registered_builder_is_callable():
for cls, builder in _TARGET_RESPONSE_BUILDERS.items():
assert callable(builder), f"builder for {cls.__name__} is not callable"
def test_unregistered_target_class_raises_in_target_to_response():
"""Reaching ``_target_to_response`` with an unmapped class raises loudly
instead of silently returning a malformed LedOutputTargetResponse.
"""
class _RogueTarget:
id = "tgt_rogue"
name = "rogue"
with pytest.raises(RuntimeError, match="No response builder"):
_target_to_response(_RogueTarget())
def test_coverage_assertion_raises_on_drift(monkeypatch):
"""Removing a builder from the registry makes the import-time check fail."""
pruned = {
cls: builder
for cls, builder in _TARGET_RESPONSE_BUILDERS.items()
if cls is not WledOutputTarget
}
monkeypatch.setattr(output_targets, "_TARGET_RESPONSE_BUILDERS", pruned)
with pytest.raises(RuntimeError, match="WledOutputTarget"):
_assert_target_response_coverage()
def test_coverage_assertion_raises_on_extra_entry(monkeypatch):
"""An entry keyed by an unknown class is also caught."""
class _RogueTargetClass:
pass
extended = {**_TARGET_RESPONSE_BUILDERS, _RogueTargetClass: lambda t: None}
monkeypatch.setattr(output_targets, "_TARGET_RESPONSE_BUILDERS", extended)
with pytest.raises(RuntimeError, match="_RogueTargetClass"):
_assert_target_response_coverage()