"""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}" )