|
15 | 15 | _looks_like_mac, |
16 | 16 | async_get_config_entry_diagnostics, |
17 | 17 | ) |
| 18 | +from custom_components.govee.models import GoveeDeviceState |
18 | 19 |
|
19 | 20 | # Govee device-id MAC pattern: 6-8 colon-separated hex octets |
20 | 21 | _MAC_RE = re.compile(r"\b[0-9A-Fa-f]{2}(?::[0-9A-Fa-f]{2}){5,7}\b") |
@@ -113,20 +114,43 @@ async def test_no_mac_in_diagnostics_output(self) -> None: |
113 | 114 | device_group.is_group = True |
114 | 115 | device_group.capabilities = [] |
115 | 116 |
|
116 | | - state_mock = MagicMock() |
117 | | - state_mock.online = True |
118 | | - state_mock.power_state = True |
119 | | - state_mock.brightness = 80 |
120 | | - state_mock.color = None |
121 | | - state_mock.color_temp_kelvin = 4000 |
122 | | - state_mock.source = "cloud_api" |
| 117 | + # Real state object so the full asdict dump path runs (incl. the |
| 118 | + # device_id MAC field, which must be redacted). |
| 119 | + state = GoveeDeviceState.create_empty(mac_id) |
| 120 | + state.sensor_temperature = 23.4 |
| 121 | + state.sensor_humidity = 48.0 |
| 122 | + |
| 123 | + # Realistic raw API/MQTT captures whose "device"/"deviceName" carry the |
| 124 | + # MAC + user name — must be redacted out of the dump. |
| 125 | + raw_state_payload = { |
| 126 | + "device": mac_id, |
| 127 | + "sku": "H6601", |
| 128 | + "capabilities": [ |
| 129 | + { |
| 130 | + "type": "devices.capabilities.property", |
| 131 | + "instance": "sensorTemperature", |
| 132 | + "state": {"value": 23.4}, |
| 133 | + } |
| 134 | + ], |
| 135 | + } |
| 136 | + api_client = MagicMock() |
| 137 | + api_client.last_raw_state = {mac_id: raw_state_payload} |
| 138 | + api_client.last_raw_devices = [ |
| 139 | + {"device": mac_id, "sku": "H6601", "deviceName": "Living Room Lamp"} |
| 140 | + ] |
| 141 | + |
| 142 | + mqtt_client = MagicMock() |
| 143 | + mqtt_client.available = True |
| 144 | + mqtt_client.connected = True |
| 145 | + mqtt_client.last_messages = {mac_id: {"onOff": 1, "sensorTemperature": 2340}} |
123 | 146 |
|
124 | 147 | coordinator = MagicMock() |
125 | 148 | coordinator.devices = {mac_id: device_mac, group_id: device_group} |
126 | | - coordinator.get_state = lambda _did: state_mock |
| 149 | + coordinator.get_state = lambda did: state if did == mac_id else None |
127 | 150 | coordinator.mqtt_connected = True |
128 | 151 | coordinator.is_ble_available = lambda _did: False |
129 | | - coordinator.mqtt_client = None |
| 152 | + coordinator.mqtt_client = mqtt_client |
| 153 | + coordinator.api_client = api_client |
130 | 154 | coordinator.api_rate_limit_remaining = 100 |
131 | 155 | coordinator.api_rate_limit_total = 100 |
132 | 156 | coordinator.api_rate_limit_reset = 0 |
@@ -158,3 +182,72 @@ async def test_no_mac_in_diagnostics_output(self) -> None: |
158 | 182 | # API key and email must also be redacted. |
159 | 183 | assert "secret" not in rendered |
160 | 184 | assert "user@example.com" not in rendered |
| 185 | + |
| 186 | + @pytest.mark.asyncio |
| 187 | + async def test_raw_captures_present_and_redacted(self) -> None: |
| 188 | + """Raw API/MQTT payloads are included for debugging but redacted. |
| 189 | +
|
| 190 | + The full parsed state (incl. sensor readings) and the verbatim |
| 191 | + /device/state + MQTT payloads must appear, while the MAC inside their |
| 192 | + "device" field is redacted. |
| 193 | + """ |
| 194 | + mac_id = "03:9C:DC:06:75:4B:10:7C" |
| 195 | + |
| 196 | + device = MagicMock() |
| 197 | + device.sku = "H5075" |
| 198 | + device.name = "Office Thermo" |
| 199 | + device.device_type = "devices.types.thermometer" |
| 200 | + device.is_group = False |
| 201 | + device.capabilities = [] |
| 202 | + |
| 203 | + state = GoveeDeviceState.create_empty(mac_id) |
| 204 | + state.sensor_temperature = 23.4 |
| 205 | + state.sensor_humidity = 48.0 |
| 206 | + |
| 207 | + api_client = MagicMock() |
| 208 | + api_client.last_raw_state = { |
| 209 | + mac_id: {"device": mac_id, "sku": "H5075", "capabilities": []} |
| 210 | + } |
| 211 | + api_client.last_raw_devices = [{"device": mac_id, "sku": "H5075"}] |
| 212 | + |
| 213 | + mqtt_client = MagicMock() |
| 214 | + mqtt_client.available = True |
| 215 | + mqtt_client.connected = True |
| 216 | + mqtt_client.last_messages = {mac_id: {"onOff": 1}} |
| 217 | + |
| 218 | + coordinator = MagicMock() |
| 219 | + coordinator.devices = {mac_id: device} |
| 220 | + coordinator.get_state = lambda _did: state |
| 221 | + coordinator.mqtt_connected = True |
| 222 | + coordinator.is_ble_available = lambda _did: False |
| 223 | + coordinator.mqtt_client = mqtt_client |
| 224 | + coordinator.api_client = api_client |
| 225 | + coordinator.api_rate_limit_remaining = 100 |
| 226 | + coordinator.api_rate_limit_total = 100 |
| 227 | + coordinator.api_rate_limit_reset = 0 |
| 228 | + coordinator.scene_cache_count = 0 |
| 229 | + |
| 230 | + entry = MagicMock() |
| 231 | + entry.entry_id = "e" |
| 232 | + entry.version = 1 |
| 233 | + entry.data = {} |
| 234 | + entry.options = {} |
| 235 | + entry.runtime_data = coordinator |
| 236 | + |
| 237 | + out = await async_get_config_entry_diagnostics(MagicMock(), entry) |
| 238 | + |
| 239 | + dev = next(iter(out["devices"].values())) |
| 240 | + # Full parsed state carries the sensor readings (the #83 debug signal). |
| 241 | + assert dev["state"]["sensor_temperature"] == 23.4 |
| 242 | + assert dev["state"]["sensor_humidity"] == 48.0 |
| 243 | + # Raw captures are attached per-device + the device-list at top level. |
| 244 | + assert dev["raw_api_state"] is not None |
| 245 | + assert dev["last_mqtt_message"] == {"onOff": 1} |
| 246 | + assert out["raw_api_devices"] is not None |
| 247 | + assert out["mqtt"]["tracked_devices"] == 1 |
| 248 | + |
| 249 | + # But the MAC in the raw payloads' "device" field is redacted, and the |
| 250 | + # parsed-state device_id MAC is gone too. |
| 251 | + rendered = json.dumps(out, default=str) |
| 252 | + assert mac_id not in rendered |
| 253 | + assert _MAC_RE.search(rendered) is None |
0 commit comments