Two things a review of this branch turned up. /oic/d's `n` is free text the owner sets from the SmartThings app, so it may carry a person's name. Nothing in the /device/0 dump has ever exposed it -- it only became reachable when diagnostics started reporting /oic/d earlier in this branch, which would have started carrying it into public issue reports. Redact it. `rt`, the device-type signal the block exists for, is untouched, and no /device/0 resource uses a bare 'n' key, so nothing else changes. The unknown-device-type warning logged only modelNum. That line is what a user pastes into an issue, and modelNum alone can't identify a washer from a dryer -- both report the shared DA_WM_ laundry board, and detection reads the consumer-model code out of `description` for exactly that reason. Log both fields.
110 lines
4.1 KiB
Python
110 lines
4.1 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': "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',
|
|
}
|