"""Tests for registry.redact — the safety net for diagnostics downloads.""" import json from pathlib import Path from custom_components.localthings.registry.batch import parse_device0_batch from custom_components.localthings.registry.redact import REDACTED, redact_resources FIXTURES = Path(__file__).resolve().parent / "fixtures" def _load(name: str) -> dict: data = json.loads((FIXTURES / name).read_text()) return parse_device0_batch(data["device0"]) def test_redacts_known_sensitive_fields_in_dishwasher_dump(): resources = _load("dishwasher_device.json") redacted = redact_resources(resources) info = redacted["/information/vs/0"] assert info["x.com.samsung.da.serialNum"] == REDACTED assert info["x.com.samsung.da.otnDUID"] == REDACTED wireless = redacted["/wirelessinfo/vs/0"] assert wireless["macaddressWiFi"] == REDACTED assert wireless["macaddressBLE"] == REDACTED provisioning = redacted["/voice/provisioning/vs/0"] headers = provisioning["voice.provisioning.headers"] assert headers["login_id"] == REDACTED deviceinfo = provisioning["voice.provisioning.deviceinfo"] assert deviceinfo["voice.provisioning.deviceinfo.accesstoken"] == REDACTED assert deviceinfo["voice.provisioning.deviceinfo.deviceid"] == REDACTED assert deviceinfo["voice.provisioning.deviceinfo.userid"] == REDACTED def test_ordinary_state_fields_survive_untouched(): resources = _load("dishwasher_device.json") redacted = redact_resources(resources) op_state = redacted["/operational/state/vs/0"] assert op_state["x.com.samsung.da.state"] == "Run" assert op_state["x.com.samsung.da.progress"] == "Finish" power = redacted["/power/vs/0"] assert power["x.com.samsung.da.power"] == "On" dishwasher = redacted["/dishwasher/vs/0"] assert dishwasher["x.com.samsung.da.sanitize"] == "On" assert dishwasher["x.com.samsung.da.rinseLevel"] == "4" alarms = redacted["/alarms/vs/0"]["x.com.samsung.da.items"] assert alarms[0]["x.com.samsung.da.code"] == "SNSF_Reached" def test_redacts_known_sensitive_fields_in_refrigerator_dump(): resources = _load("refrigerator_device.json") redacted = redact_resources(resources) info = redacted["/information/vs/0"] assert info["x.com.samsung.da.serialNum"] == REDACTED wireless = redacted["/wirelessinfo/vs/0"] assert wireless["macaddressWiFi"] == REDACTED assert wireless["macaddressBLE"] == REDACTED def test_redact_resources_does_not_mutate_input(): resources = _load("dishwasher_device.json") original_serial = resources["/information/vs/0"]["x.com.samsung.da.serialNum"] redact_resources(resources) assert resources["/information/vs/0"]["x.com.samsung.da.serialNum"] == original_serial def test_redacts_bare_ocf_identity_keys(): """/oic/d and /oic/p identify the unit with two-letter keys ('di', 'pi') that the substring rules can't see.""" redacted = redact_resources( { "/oic/d": { "di": "ab-cd-ef", "n": "Marc's Fridge", "rt": ["oic.wk.d", "oic.d.refrigerator"], }, "/oic/p": {"pi": "12-34-56", "mnmo": "RF9000B"}, } ) assert redacted["/oic/d"]["di"] == REDACTED assert redacted["/oic/p"]["pi"] == REDACTED # 'n' is free text the owner sets from the SmartThings app, so it can # carry a person's name -- redacted too. `rt`, the device-type signal # we actually want out of /oic/d, is not. assert redacted["/oic/d"]["n"] == REDACTED assert redacted["/oic/d"]["rt"] == ["oic.wk.d", "oic.d.refrigerator"] assert redacted["/oic/p"]["mnmo"] == "RF9000B" def test_bare_key_redaction_does_not_leak_into_substring_matching(): """The bare keys are whole-key matches only -- plenty of ordinary appliance fields contain those letters and must survive untouched.""" redacted = redact_resources( { "/x": { "condition": "Normal", "display": "On", "dispenser": "Cubed", "humidity": "45", "spinSpeed": "1200", "name": "FilterProgress", }, } ) assert redacted["/x"] == { "condition": "Normal", "display": "On", "dispenser": "Cubed", "humidity": "45", "spinSpeed": "1200", "name": "FilterProgress", }