import pytest from edge.config import (frame_interval_from_env, load_config, queue_db_path, require_upload_config) def test_defaults_when_env_empty(): cfg = load_config({}) assert cfg.device_id == "pi-unknown" assert cfg.fall.prone_confirm_s == 3.0 assert cfg.fall.no_motion_s == 10.0 assert cfg.camera_id == "cam-01" assert cfg.cafe24_url == "" assert cfg.secret == "" assert cfg.ghost.reappear_s == 10.0 assert cfg.zone.debounce_s == 0.75 assert cfg.engine.cooldown_s == 30.0 def test_env_overrides(): env = { "DEVICE_ID": "pi-w1", "CAMERA_ID": "cam-9", "CAFE24_URL": "https://x/api/events", "DEVICE_SECRET": "s", "FALL_PRONE_S": "2.0", "FALL_NO_MOTION_S": "8", "IMMOBILE_S": "90", "COOLDOWN_S": "45", "GHOST_REAPPEAR_S": "7", "ZONE_DEBOUNCE_S": "1.5", } cfg = load_config(env) assert cfg.device_id == "pi-w1" assert cfg.camera_id == "cam-9" assert cfg.fall.prone_confirm_s == 2.0 assert cfg.fall.no_motion_s == 8.0 assert cfg.immobility.immobile_s == 90.0 assert cfg.engine.cooldown_s == 45.0 assert cfg.engine.camera_id == "cam-9" assert cfg.ghost.reappear_s == 7.0 assert cfg.zone.debounce_s == 1.5 def test_require_upload_config_rejects_blank_url_and_secret(): # A blank CAFE24_URL or DEVICE_SECRET must fail LOUD at startup. Left silent, the daemon # runs green while every upload fails (bad URL -> requests error, caught) and the offline # queue grows until the SD card is full. with pytest.raises(ValueError, match="CAFE24_URL"): require_upload_config(load_config({"DEVICE_SECRET": "s"})) with pytest.raises(ValueError, match="DEVICE_SECRET"): require_upload_config(load_config({"CAFE24_URL": "https://x/move_detect"})) # Whitespace-only is still blank. with pytest.raises(ValueError): require_upload_config(load_config({"CAFE24_URL": " ", "DEVICE_SECRET": " "})) def test_require_upload_config_passes_when_both_present(): # A real per-device secret is a long HMAC-derived hex string, on an https URL. cfg = load_config({"CAFE24_URL": "https://x/move_detect", "DEVICE_SECRET": "0123456789abcdef0123"}) require_upload_config(cfg) # must not raise def test_require_upload_config_rejects_placeholder_and_http(): # Non-empty ship-time placeholders pass a blank check but are never a real value; a unit left # at one uploads nothing while looking healthy. http would leak worker snapshots in plaintext. with pytest.raises(ValueError, match="DEVICE_SECRET"): require_upload_config(load_config({"CAFE24_URL": "https://x/move_detect", "DEVICE_SECRET": "CHANGEME"})) with pytest.raises(ValueError, match="CAFE24_URL"): require_upload_config(load_config({"CAFE24_URL": "https://your-domain.example/move_detect", "DEVICE_SECRET": "0123456789abcdef0123"})) with pytest.raises(ValueError, match="https"): require_upload_config(load_config({"CAFE24_URL": "http://x/move_detect", "DEVICE_SECRET": "0123456789abcdef0123"})) with pytest.raises(ValueError, match="too short"): require_upload_config(load_config({"CAFE24_URL": "https://x/move_detect", "DEVICE_SECRET": "short"})) def test_load_config_rejects_non_finite_or_nonpositive_motion_thr(): # A 0 / NaN / inf MOTION_THR would make every track read as "still" -> constant no_motion. for bad in ("0", "-1", "nan", "inf"): with pytest.raises(ValueError, match="MOTION_THR"): load_config({"MOTION_THR": bad}) # A sane value flows through to both detectors. cfg = load_config({"MOTION_THR": "0.3"}) assert cfg.fall.motion_thr == 0.3 and cfg.immobility.motion_thr == 0.3 def test_frame_interval_defaults_and_validates(): assert frame_interval_from_env({}) == pytest.approx(1 / 12) assert frame_interval_from_env({"TARGET_FPS": "24"}) == pytest.approx(1 / 24) # TARGET_FPS=0 -> ZeroDivisionError today -> infinite systemd crash loop; must be rejected. with pytest.raises(ValueError): frame_interval_from_env({"TARGET_FPS": "0"}) with pytest.raises(ValueError): frame_interval_from_env({"TARGET_FPS": "12fps"}) with pytest.raises(ValueError): frame_interval_from_env({"TARGET_FPS": "-5"}) def test_queue_db_path_defaults_absolute_under_state_dir(): # A relative 'queue.db' is unwritable under the hardened unit (ProtectSystem=strict, # WorkingDirectory read-only) -> crash loop. Default to the writable StateDirectory. assert queue_db_path({}) == "/var/lib/move-detect/queue.db" assert queue_db_path({"QUEUE_DB": "/custom/q.db"}) == "/custom/q.db"