Skip to content

Commit 12daeb5

Browse files
committed
Add unit tests for client params, error classes, and models
1 parent 4b13021 commit 12daeb5

3 files changed

Lines changed: 236 additions & 0 deletions

File tree

tests/test_client.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import os
2+
from datetime import datetime
23

34
import aiohttp_client_cache.session
45
import requests_cache
@@ -7,6 +8,7 @@
78
from homeassistant_api import AsyncWebsocketClient
89
from homeassistant_api import Client
910
from homeassistant_api import WebsocketClient
11+
from homeassistant_api.baseclient import BaseClient
1012

1113

1214
def test_custom_cached_session() -> None:
@@ -62,3 +64,47 @@ async def test_async_websocket_client_ping() -> None:
6264
os.environ["HOMEASSISTANTAPI_TOKEN"],
6365
) as client:
6466
assert (await client.ping_latency()) > 0
67+
68+
69+
# --- BaseClient: prepare_get_entity_histories_params with naive timestamps ---
70+
71+
72+
def test_prepare_entity_histories_naive_timestamps() -> None:
73+
"""Tests that naive (tzinfo=None) timestamps are converted to local timezone."""
74+
naive_start = datetime(2024, 1, 1, 12, 0, 0) # noqa: DTZ001
75+
naive_end = datetime(2024, 6, 1, 12, 0, 0) # noqa: DTZ001
76+
params, url = BaseClient.prepare_get_entity_histories_params(
77+
start_timestamp=naive_start,
78+
end_timestamp=naive_end,
79+
)
80+
# Naive timestamps should get a timezone attached
81+
assert "+" in url or "-" in url.split("T")[-1], (
82+
"start_timestamp should have timezone offset"
83+
)
84+
assert "+" in params["end_time"] or "-" in params["end_time"].split("T")[-1], (
85+
"end_time should have timezone offset"
86+
)
87+
88+
89+
# --- BaseClient: prepare_get_logbook_entry_params ---
90+
91+
92+
def test_prepare_logbook_entry_no_start_timestamp() -> None:
93+
"""Tests logbook params without a start_timestamp return base 'logbook' path."""
94+
params, url = BaseClient.prepare_get_logbook_entry_params(
95+
filter_entities=["light.kitchen", "light.bedroom"],
96+
end_timestamp=datetime(2024, 6, 1, 12, 0, 0), # noqa: DTZ001
97+
)
98+
assert url == "logbook"
99+
assert "light.kitchen,light.bedroom" in params["entity"]
100+
assert "end_time" in params
101+
102+
103+
def test_prepare_logbook_entry_string_timestamps() -> None:
104+
"""Tests logbook params with string timestamps pass through unchanged."""
105+
params, url = BaseClient.prepare_get_logbook_entry_params(
106+
start_timestamp="2024-01-01T00:00:00",
107+
end_timestamp="2024-06-01T00:00:00",
108+
)
109+
assert "2024-01-01T00:00:00" in url
110+
assert params["end_time"] == "2024-06-01T00:00:00"

tests/test_errors.py

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
from http import HTTPMethod
77

88
import aiohttp
9+
import aiohttp_client_cache.session
910
import pytest
1011
import requests
1112
from multidict import CIMultiDict
1213
from multidict import CIMultiDictProxy
14+
from requests_cache import CachedSession
1315

1416
from homeassistant_api import AsyncClient
1517
from homeassistant_api import AsyncWebsocketClient
@@ -28,6 +30,7 @@
2830
from homeassistant_api.errors import ResponseError
2931
from homeassistant_api.errors import UnauthorizedError
3032
from homeassistant_api.errors import UnexpectedStatusCodeError
33+
from homeassistant_api.models.states import State
3134
from homeassistant_api.models.websocket import Error
3235
from homeassistant_api.processing import async_process_response
3336
from homeassistant_api.processing import process_response
@@ -287,3 +290,114 @@ def test_error_model_without_optional_fields() -> None:
287290
assert error.translation_key is None
288291
assert error.translation_placeholders is None
289292
assert error.translation_domain is None
293+
294+
295+
# --- Processing: async processor not found ---
296+
297+
298+
async def test_async_exception_processor_not_found_error() -> None:
299+
"""Tests that async_process_response raises ProcessorNotFoundError for unknown MIME types."""
300+
with pytest.raises(ProcessorNotFoundError, match="this_type/does-not-exist"):
301+
await async_process_response(
302+
make_async_response(200, "", {"Content-Type": "this_type/does-not-exist"}),
303+
)
304+
305+
306+
async def test_async_exception_bad_request() -> None:
307+
"""Tests that async_process_response raises RequestError for 400 responses."""
308+
with pytest.raises(RequestError):
309+
await async_process_response(
310+
make_async_response(400, "bad request data", {}),
311+
)
312+
313+
314+
async def test_async_exception_internal_server_error() -> None:
315+
"""Tests that async_process_response raises InternalServerError for 500 responses."""
316+
with pytest.raises(InternalServerError):
317+
await async_process_response(make_async_response(500, "server broke", {}))
318+
319+
320+
async def test_async_exception_unexpected_status_code() -> None:
321+
"""Tests that async_process_response raises UnexpectedStatusCodeError for unknown status."""
322+
with pytest.raises(UnexpectedStatusCodeError):
323+
await async_process_response(make_async_response(0, "", {}))
324+
325+
326+
# --- WebSocket: NotImplementedError stubs ---
327+
328+
329+
def test_websocket_set_state_not_supported(websocket_client: WebsocketClient) -> None:
330+
"""Tests that WebsocketClient.set_state raises NotImplementedError."""
331+
state = State(state="test", entity_id="sun.sun")
332+
with pytest.raises(
333+
NotImplementedError,
334+
match="not supported over the WebSocket API",
335+
):
336+
websocket_client.set_state(state)
337+
338+
339+
def test_websocket_get_entity_histories_not_supported(
340+
websocket_client: WebsocketClient,
341+
) -> None:
342+
"""Tests that WebsocketClient.get_entity_histories raises NotImplementedError."""
343+
with pytest.raises(
344+
NotImplementedError,
345+
match="not supported over the WebSocket API",
346+
):
347+
list(websocket_client.get_entity_histories())
348+
349+
350+
async def test_async_websocket_set_state_not_supported(
351+
async_websocket_client: AsyncWebsocketClient,
352+
) -> None:
353+
"""Tests that AsyncWebsocketClient.set_state raises NotImplementedError."""
354+
state = State(state="test", entity_id="sun.sun")
355+
with pytest.raises(
356+
NotImplementedError,
357+
match="not supported over the WebSocket API",
358+
):
359+
await async_websocket_client.set_state(state)
360+
361+
362+
async def test_async_websocket_get_entity_histories_not_supported(
363+
async_websocket_client: AsyncWebsocketClient,
364+
) -> None:
365+
"""Tests that AsyncWebsocketClient.get_entity_histories raises NotImplementedError."""
366+
with pytest.raises(
367+
NotImplementedError,
368+
match="not supported over the WebSocket API",
369+
):
370+
async for _ in async_websocket_client.get_entity_histories():
371+
pass
372+
373+
374+
# --- Client: no-cache session ---
375+
376+
377+
def test_client_no_cache_session() -> None:
378+
"""Tests that Client can be created without a cache session."""
379+
token = os.environ["HOMEASSISTANTAPI_TOKEN"]
380+
client = Client(HA_URL, token, use_cache=False)
381+
assert isinstance(client._session, requests.Session)
382+
assert not isinstance(client._session, CachedSession)
383+
384+
385+
def test_client_default_cache_session() -> None:
386+
"""Tests that Client creates a CachedSession when use_cache=True."""
387+
token = os.environ["HOMEASSISTANTAPI_TOKEN"]
388+
client = Client(HA_URL, token, use_cache=True)
389+
assert isinstance(client._session, CachedSession)
390+
391+
392+
async def test_async_client_no_cache_session() -> None:
393+
"""Tests that AsyncClient can be created without a cache session."""
394+
token = os.environ["HOMEASSISTANTAPI_TOKEN"]
395+
client = AsyncClient(HA_URL, token, use_cache=False)
396+
assert isinstance(client._session, aiohttp.ClientSession)
397+
398+
399+
async def test_async_client_default_cache_session() -> None:
400+
"""Tests that AsyncClient creates a CachedSession when use_cache=True."""
401+
token = os.environ["HOMEASSISTANTAPI_TOKEN"]
402+
client = AsyncClient(HA_URL, token, use_cache=True)
403+
assert isinstance(client._session, aiohttp_client_cache.session.CachedSession)

tests/test_models.py

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@
99
from homeassistant_api import AsyncClient
1010
from homeassistant_api import Client
1111
from homeassistant_api import Domain
12+
from homeassistant_api.models.domains import BaseDomain
13+
from homeassistant_api.models.entity import BaseGroup
1214
from homeassistant_api.models.events import Event
15+
from homeassistant_api.models.history import History
1316
from homeassistant_api.models.states import State
1417

1518

@@ -142,3 +145,76 @@ async def test_async_entity_get_history_none(async_cached_client: AsyncClient) -
142145
end_timestamp=datetime(2020, 1, 1, tzinfo=UTC),
143146
)
144147
assert history is None
148+
149+
150+
# --- BaseGroup: __getattr__ for nonexistent key ---
151+
152+
153+
def test_base_group_getattr_nonexistent() -> None:
154+
"""Tests that BaseGroup.__getattr__ raises AttributeError for unknown attributes."""
155+
group = BaseGroup(group_id="test")
156+
assert group.get_entity("nonexistent") is None
157+
with pytest.raises(AttributeError):
158+
_ = group.nonexistent_entity
159+
160+
161+
def test_base_group_add_entity_not_implemented() -> None:
162+
"""Tests that BaseGroup._add_entity raises NotImplementedError."""
163+
group = BaseGroup(group_id="test")
164+
with pytest.raises(NotImplementedError):
165+
group._add_entity("slug", State(state="on", entity_id="test.slug"))
166+
167+
168+
# --- BaseDomain: _add_service not implemented ---
169+
170+
171+
def test_base_domain_add_service_not_implemented() -> None:
172+
"""Tests that BaseDomain._add_service raises NotImplementedError."""
173+
domain = BaseDomain(domain_id="test")
174+
with pytest.raises(NotImplementedError):
175+
domain._add_service("svc")
176+
177+
178+
def test_base_domain_from_json_invalid_services_type(cached_client: Client) -> None:
179+
"""Tests that Domain.from_json_with_client raises TypeError when services is not a dict."""
180+
with pytest.raises(TypeError, match="Expected dict for services"):
181+
Domain.from_json_with_client(
182+
{"domain": "test", "services": "not_a_dict"},
183+
cached_client,
184+
)
185+
186+
187+
# --- History: repr and entity_id ---
188+
189+
190+
def test_history_repr() -> None:
191+
"""Tests that History has a meaningful repr with entity_id."""
192+
states = (
193+
State(state="on", entity_id="light.kitchen"),
194+
State(state="off", entity_id="light.kitchen"),
195+
)
196+
history = History(states=states)
197+
assert history.entity_id == "light.kitchen"
198+
assert "light.kitchen" in repr(history)
199+
200+
201+
def test_history_entity_id_from_states() -> None:
202+
"""Tests that History.entity_id is derived from the states' entity_ids."""
203+
states = (
204+
State(state="on", entity_id="light.kitchen"),
205+
State(state="off", entity_id="light.kitchen"),
206+
)
207+
history = History(states=states)
208+
assert history.entity_id == "light.kitchen"
209+
210+
211+
# --- Domain: service access via attribute ---
212+
213+
214+
def test_domain_service_attribute_access(cached_client: Client) -> None:
215+
"""Tests that Domain services are accessible as attributes."""
216+
notify = cached_client.get_domain("notify")
217+
assert notify is not None
218+
svc = notify.get_service("persistent_notification")
219+
assert svc is not None
220+
assert notify.persistent_notification == svc

0 commit comments

Comments
 (0)