Files
localthings/tests/test_redact.py
T
Marc Billow 288ba02cc5 feat(diagnostics): capture /oic/p and /oic/d identity
Device-type detection currently parses board part numbers out of
/information/vs/0's modelNum. OCF has a standard field for exactly this
question -- /oic/d's `rt` -- and read_identity() already fetches the
resource, but kept only `n` and threw the rest away. No captured dump has
ever included it either: /device/0 batch responses don't carry /oic/d, and
diagnostics didn't report it, so there's no evidence on whether real
hardware populates it usefully.

Keep `rt` as DeviceIdentity.device_types, keep both raw payloads whole
(we don't yet know which of their fields identify a type), and surface
them in diagnostics so incoming issue reports answer the question.

Nothing routes on it yet.

/oic/d and /oic/p identify the unit with bare two-letter keys -- 'di' and
'pi' -- as sensitive as the serial number redact.py already covers but far
too short to match on: 'di' alone is a substring of 'condition', 'display'
and 'dispenser'. Add a whole-key match alongside the substring rules.
2026-07-29 01:56:52 +00:00

106 lines
3.8 KiB
Python

"""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': 'Family Hub'},
'/oic/p': {'pi': '12-34-56', 'mnmo': 'RF9000B'},
})
assert redacted['/oic/d']['di'] == REDACTED
assert redacted['/oic/p']['pi'] == REDACTED
# Non-identifying neighbours in the same payloads survive.
assert redacted['/oic/d']['n'] == 'Family Hub'
assert redacted['/oic/p']['mnmo'] == 'RF9000B'
def test_bare_key_redaction_does_not_leak_into_substring_matching():
"""'di'/'pi' are whole-key matches only -- plenty of ordinary appliance
fields contain those two letters and must survive untouched."""
redacted = redact_resources({
'/x': {
'condition': 'Normal', 'display': 'On', 'dispenser': 'Cubed',
'humidity': '45', 'spinSpeed': '1200',
},
})
assert redacted['/x'] == {
'condition': 'Normal', 'display': 'On', 'dispenser': 'Cubed',
'humidity': '45', 'spinSpeed': '1200',
}