Follow-up to the holistic review of the PIN-unification branch: - /system/status now reads the real arm state from the arm_state_log table via get_current_arm_state, instead of returning a hardcoded 'DISARMED' stub. Without this, polling after the new async 202 arm/disarm flow was a UX dead-end — clients never saw the state change they just requested. DB read failures degrade gracefully. - Operator guide: correct the claim that 'vigilar config set-pin' populates recovery_passphrase_hash. It doesn't. recovery_passphrase _hash has no CLI helper today; it must be set manually. - Tests: add a fail-closed regression for verify_pin on malformed stored hashes, and a companion test confirming the deprecation warning stays silent on a fully migrated config. All address specific review comments on the branch; no scope creep. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
187 lines
6.1 KiB
Python
187 lines
6.1 KiB
Python
"""Tests for config loading and validation."""
|
|
|
|
from vigilar.config import CameraConfig, VigilarConfig
|
|
from vigilar.config import PetsConfig, WildlifeThreatMap, WildlifeSizeHeuristics, PetActivityConfig
|
|
|
|
|
|
def test_default_config():
|
|
cfg = VigilarConfig()
|
|
assert cfg.system.name == "Vigilar Home Security"
|
|
assert cfg.web.port == 49735
|
|
assert cfg.mqtt.port == 1883
|
|
assert cfg.cameras == []
|
|
|
|
|
|
def test_camera_config_defaults():
|
|
cam = CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://localhost")
|
|
assert cam.idle_fps == 2
|
|
assert cam.motion_fps == 30
|
|
assert cam.pre_motion_buffer_s == 5
|
|
assert cam.post_motion_buffer_s == 30
|
|
assert cam.motion_sensitivity == 0.7
|
|
|
|
|
|
def test_duplicate_camera_ids_rejected():
|
|
import pytest
|
|
with pytest.raises(ValueError, match="Duplicate camera IDs"):
|
|
VigilarConfig(cameras=[
|
|
CameraConfig(id="cam1", display_name="A", rtsp_url="rtsp://a"),
|
|
CameraConfig(id="cam1", display_name="B", rtsp_url="rtsp://b"),
|
|
])
|
|
|
|
|
|
def test_camera_sensitivity_bounds():
|
|
import pytest
|
|
with pytest.raises(Exception):
|
|
CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://localhost", motion_sensitivity=1.5)
|
|
|
|
|
|
class TestPetsConfig:
|
|
def test_defaults(self):
|
|
cfg = PetsConfig()
|
|
assert cfg.enabled is False
|
|
assert cfg.model == "yolov8s"
|
|
assert cfg.confidence_threshold == 0.5
|
|
assert cfg.pet_id_threshold == 0.7
|
|
assert cfg.pet_id_low_confidence == 0.5
|
|
assert cfg.min_training_images == 20
|
|
assert cfg.crop_retention_days == 7
|
|
|
|
def test_custom_values(self):
|
|
cfg = PetsConfig(enabled=True, model="yolov8m", confidence_threshold=0.6)
|
|
assert cfg.enabled is True
|
|
assert cfg.model == "yolov8m"
|
|
assert cfg.confidence_threshold == 0.6
|
|
|
|
|
|
class TestWildlifeThreatMap:
|
|
def test_defaults(self):
|
|
tm = WildlifeThreatMap()
|
|
assert "bear" in tm.predator
|
|
assert "bird" in tm.passive
|
|
|
|
def test_custom_mapping(self):
|
|
tm = WildlifeThreatMap(predator=["bear", "wolf"], nuisance=["raccoon"])
|
|
assert "wolf" in tm.predator
|
|
assert "raccoon" in tm.nuisance
|
|
|
|
|
|
class TestWildlifeSizeHeuristics:
|
|
def test_defaults(self):
|
|
sh = WildlifeSizeHeuristics()
|
|
assert sh.small == 0.02
|
|
assert sh.medium == 0.08
|
|
assert sh.large == 0.15
|
|
|
|
|
|
class TestPetActivityConfig:
|
|
def test_defaults(self):
|
|
cfg = PetActivityConfig()
|
|
assert cfg.daily_digest is True
|
|
assert cfg.highlight_clips is True
|
|
assert cfg.zoomie_threshold == 0.8
|
|
|
|
|
|
def test_security_config_defaults():
|
|
from vigilar.config import SecurityConfig
|
|
sc = SecurityConfig()
|
|
assert sc.pin_hash == ""
|
|
assert sc.recovery_passphrase_hash == ""
|
|
|
|
|
|
def test_vigilar_config_has_security():
|
|
from vigilar.config import VigilarConfig
|
|
cfg = VigilarConfig()
|
|
assert cfg.security.pin_hash == ""
|
|
assert cfg.security.recovery_passphrase_hash == ""
|
|
|
|
|
|
def test_location_config_defaults():
|
|
from vigilar.config import LocationConfig
|
|
lc = LocationConfig()
|
|
assert lc.latitude == 0.0
|
|
assert lc.longitude == 0.0
|
|
|
|
|
|
def test_vigilar_config_has_location():
|
|
from vigilar.config import VigilarConfig
|
|
cfg = VigilarConfig()
|
|
assert cfg.location.latitude == 0.0
|
|
|
|
|
|
def test_highlights_config_defaults():
|
|
from vigilar.config import HighlightsConfig
|
|
assert HighlightsConfig().enabled is True
|
|
assert HighlightsConfig().generate_time == "06:00"
|
|
|
|
def test_kiosk_config_defaults():
|
|
from vigilar.config import KioskConfig
|
|
assert KioskConfig().ambient_enabled is True
|
|
assert KioskConfig().camera_rotation_s == 10
|
|
|
|
def test_recording_trigger_highlight():
|
|
from vigilar.constants import RecordingTrigger
|
|
assert RecordingTrigger.HIGHLIGHT == "HIGHLIGHT"
|
|
|
|
def test_recording_trigger_timelapse():
|
|
from vigilar.constants import RecordingTrigger
|
|
assert RecordingTrigger.TIMELAPSE == "TIMELAPSE"
|
|
|
|
|
|
class TestCameraConfigLocation:
|
|
def test_default_location_is_interior(self):
|
|
from vigilar.config import CameraConfig
|
|
cfg = CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://x")
|
|
assert cfg.location == "INTERIOR"
|
|
|
|
def test_exterior_location(self):
|
|
from vigilar.config import CameraConfig
|
|
cfg = CameraConfig(id="test", display_name="Test", rtsp_url="rtsp://x", location="EXTERIOR")
|
|
assert cfg.location == "EXTERIOR"
|
|
|
|
|
|
def test_deprecation_warning_for_arm_pin_hash(tmp_path, caplog):
|
|
"""Loading a config that still uses the legacy [system] arm_pin_hash
|
|
must log a clear warning pointing the user at `vigilar config set-pin`."""
|
|
import logging
|
|
cfg_path = tmp_path / "legacy.toml"
|
|
cfg_path.write_text(
|
|
'[system]\n'
|
|
'arm_pin_hash = "pbkdf2_sha256$abc$def"\n'
|
|
)
|
|
with caplog.at_level(logging.WARNING):
|
|
from vigilar.config import load_config
|
|
load_config(str(cfg_path))
|
|
|
|
messages = [r.message for r in caplog.records if r.levelno >= logging.WARNING]
|
|
assert any("arm_pin_hash" in m and "deprecated" in m.lower() for m in messages), (
|
|
f"expected deprecation warning mentioning arm_pin_hash, got: {messages}"
|
|
)
|
|
|
|
|
|
def test_no_deprecation_warning_when_security_pin_hash_set(tmp_path, caplog):
|
|
"""No warning should fire if [security] pin_hash is populated,
|
|
regardless of whether [system] arm_pin_hash is also still present.
|
|
The warning is specifically for un-migrated configs."""
|
|
import logging
|
|
cfg_path = tmp_path / "migrated.toml"
|
|
cfg_path.write_text(
|
|
'[system]\n'
|
|
'arm_pin_hash = "pbkdf2_sha256$legacy$value"\n'
|
|
'\n'
|
|
'[security]\n'
|
|
'pin_hash = "pbkdf2_sha256$current$value"\n'
|
|
)
|
|
with caplog.at_level(logging.WARNING):
|
|
from vigilar.config import load_config
|
|
load_config(str(cfg_path))
|
|
|
|
deprecation_messages = [
|
|
r.message for r in caplog.records
|
|
if r.levelno >= logging.WARNING and "arm_pin_hash" in r.message
|
|
]
|
|
assert deprecation_messages == [], (
|
|
f"deprecation warning should not fire on migrated configs, "
|
|
f"got: {deprecation_messages}"
|
|
)
|