Files
localthings/tests/test_redact.py
T
Marc Billow daf7e3787f Add ruff (lint + format) and ty (type checking) to the project
Adds [tool.ruff] and [tool.ty] config to pyproject.toml with a curated
ruff rule set (E, F, W, I, UP, B, C4, SIM, RUF, ASYNC, LOG, G, PIE, RET,
PERF, N), pins ruff/ty in requirements-dev.txt, reformats the whole tree
with `ruff format`, and fixes the pre-existing lint and type-check debt
those tools surfaced so both run clean.

Production-code type fixes include: HA's ConfigFlowResult vs. the
generic FlowResult in config_flow.py, narrowing BoundEntity.desc to its
platform-specific subclass (SelectDesc/NumberDesc/SensorDesc/etc.) via
cast() instead of an unchecked annotation, converting HA device_class
strings to their proper enum types, a resolve_registry callback typed
as `object` instead of `DeviceRegistry | None`, and a couple of other
narrow correctness fixes (CA key type validation, an index-out-of-bounds
false positive from an empty-tuple fallback, a bool/dict argument swap).

Test-file fixes are mechanical: narrowing SamsungEntityDescription to
the correct subclass via isinstance()/cast() before accessing
subclass-only fields, and asserting Optional write_fn/unit_fn fields
are set before calling them.
2026-08-02 23:56:38 +00:00

126 lines
4.3 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",
}