Skip to content

Commit eb38faf

Browse files
committed
test: tighten id-guard tests — URL/body inspection, int/str, empty-string
Address pr-test-analyzer findings: - test_update_with_matching_inner_id_proceeds now asserts URL and body id both equal the resolved unique_id, so a refactor that mutates one without the other regresses - Add test_update_with_int_vs_str_id_equivalence_passes pinning the guard's deliberate str() coercion - Add test_update_with_empty_string_inner_id_is_rejected covering the empty-template-variable agent-slip pattern
1 parent 76d0bc9 commit eb38faf

1 file changed

Lines changed: 44 additions & 1 deletion

File tree

tests/src/unit/test_rest_client_automations.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,12 @@ async def test_update_with_mismatched_inner_id_is_rejected(self, mock_client):
183183

184184
@pytest.mark.asyncio
185185
async def test_update_with_matching_inner_id_proceeds(self, mock_client):
186-
"""identifier and config.id both resolve to the same unique_id → ok."""
186+
"""identifier and config.id both resolve to the same unique_id → ok.
187+
188+
Asserts URL and body id both equal the resolved unique_id so a future
189+
refactor that mutates one without the other (the exact failure mode
190+
this PR fixes) would regress.
191+
"""
187192
mock_client._resolve_automation_id = AsyncMock(return_value="AAA")
188193
mock_client._request = AsyncMock(return_value={"result": "ok"})
189194
mock_client._poll_for_automation_entity = AsyncMock(return_value=None)
@@ -196,6 +201,44 @@ async def test_update_with_matching_inner_id_proceeds(self, mock_client):
196201
assert result["unique_id"] == "AAA"
197202
assert result["operation"] == "updated"
198203
mock_client._request.assert_called_once()
204+
method, url = mock_client._request.call_args.args
205+
assert method == "POST"
206+
assert url == "/config/automation/config/AAA"
207+
assert mock_client._request.call_args.kwargs["json"]["id"] == "AAA"
208+
209+
@pytest.mark.asyncio
210+
async def test_update_with_int_vs_str_id_equivalence_passes(self, mock_client):
211+
"""``config['id']`` as int matching the resolved str id is treated as
212+
a match. HA accepts both shapes and stringifies on storage, so the
213+
guard's ``str(...)`` coercion is intentional — pin it so a future
214+
tightening to strict-type compare is a deliberate decision."""
215+
mock_client._resolve_automation_id = AsyncMock(return_value="1234")
216+
mock_client._request = AsyncMock(return_value={"result": "ok"})
217+
mock_client._poll_for_automation_entity = AsyncMock(return_value=None)
218+
219+
result = await mock_client.upsert_automation_config(
220+
{"id": 1234, "alias": "x", "trigger": [], "action": []},
221+
identifier="automation.foo",
222+
)
223+
224+
assert result["unique_id"] == "1234"
225+
mock_client._request.assert_called_once()
226+
227+
@pytest.mark.asyncio
228+
async def test_update_with_empty_string_inner_id_is_rejected(self, mock_client):
229+
"""``config['id']=''`` is a real agent-slip pattern (templating with an
230+
empty variable). It is not None, so the guard fires and rejects."""
231+
mock_client._resolve_automation_id = AsyncMock(return_value="AAA")
232+
mock_client._request = AsyncMock()
233+
234+
with pytest.raises(HomeAssistantAPIError) as exc_info:
235+
await mock_client.upsert_automation_config(
236+
{"id": "", "alias": "x", "trigger": [], "action": []},
237+
identifier="automation.foo",
238+
)
239+
240+
assert exc_info.value.status_code == 400
241+
mock_client._request.assert_not_called()
199242

200243
@pytest.mark.asyncio
201244
async def test_update_without_inner_id_proceeds(self, mock_client):

0 commit comments

Comments
 (0)