"""CameraStore — the writable camera list. No camera or models needed.""" import json import pytest from behavision.cameras import CameraStore, protect, unprotect from behavision.config import CameraConfig SECRET = "p@ss:w0rd/with@specials" def cam(cid="cam1", **kw): base = dict(id=cid, host="192.168.0.138", path="/ch0_0.264", username="admin", password=SECRET) base.update(kw) return CameraConfig(**base) def test_add_get_list_delete(tmp_path): s = CameraStore(tmp_path / "cameras.json") s.add(cam()) assert [c.id for c in s.list()] == ["cam1"] assert s.get("cam1").password == SECRET assert s.delete("cam1") and s.get("cam1") is None assert not s.delete("cam1") def test_duplicate_id_rejected(tmp_path): s = CameraStore(tmp_path / "cameras.json") s.add(cam()) with pytest.raises(ValueError, match="already exists"): s.add(cam()) def test_camera_with_no_source_is_rejected_at_add(tmp_path): """Better to fail on Save than to silently create a camera that can never connect.""" s = CameraStore(tmp_path / "cameras.json") with pytest.raises(ValueError, match="set url, host or webcam"): s.add(CameraConfig(id="bad")) def test_survives_a_restart(tmp_path): path = tmp_path / "cameras.json" CameraStore(path).add(cam()) reloaded = CameraStore(path) assert reloaded.get("cam1").password == SECRET assert reloaded.get("cam1").host == "192.168.0.138" def test_password_is_not_stored_in_the_clear_verbatim(tmp_path): """On Windows DPAPI encrypts it; elsewhere it is tagged plaintext. Either way the stored form must be tagged so the format can evolve.""" path = tmp_path / "cameras.json" CameraStore(path).add(cam()) stored = json.loads(path.read_text())["cameras"][0]["password"] assert stored.startswith(("dpapi:", "plain:")) assert unprotect(stored) == SECRET def test_protect_roundtrip_including_empty(): assert unprotect(protect(SECRET)) == SECRET assert protect("") == "" assert unprotect("") == "" def test_update_cannot_rename_the_id(tmp_path): """id is the engine's key for the running worker; a rename would orphan it.""" s = CameraStore(tmp_path / "cameras.json") s.add(cam()) updated = s.update("cam1", {"id": "renamed", "port": 8554}) assert updated.id == "cam1" and updated.port == 8554 assert s.get("renamed") is None def test_update_unknown_camera_returns_none(tmp_path): assert CameraStore(tmp_path / "cameras.json").update("nope", {}) is None def test_seed_runs_once_only(tmp_path): """A camera deleted in the UI must not reappear from YAML on restart.""" path = tmp_path / "cameras.json" s = CameraStore(path) assert s.seed([cam("yaml1")]) is True assert s.delete("yaml1") assert CameraStore(path).seed([cam("yaml1")]) is False assert CameraStore(path).list() == [] def test_unreadable_file_does_not_destroy_it(tmp_path): path = tmp_path / "cameras.json" path.write_text("{ not json") s = CameraStore(path) assert s.list() == [] assert path.read_text() == "{ not json" # left for a human to look at def test_malformed_entry_is_skipped_not_fatal(tmp_path): path = tmp_path / "cameras.json" path.write_text(json.dumps({"cameras": [ {"id": "good", "host": "10.0.0.1"}, {"host": "10.0.0.2"}, # no id ]})) assert [c.id for c in CameraStore(path).list()] == ["good"] def test_safe_url_masks_the_password(tmp_path): assert SECRET not in cam().safe_url() assert "*****" in cam().safe_url()