Skip to content

Commit 50c9de3

Browse files
committed
Enhancing tests
1 parent fc798f6 commit 50c9de3

File tree

5 files changed

+142
-44
lines changed

5 files changed

+142
-44
lines changed

poetry.lock

Lines changed: 8 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ mashumaro = ">=3.10"
2626
orjson = ">=3.9.8"
2727
python = "^3.12"
2828
yarl = ">=1.6.0"
29-
aioresponses = "^0.7.6"
29+
aioresponses = "^0.7.7"
3030

3131
[tool.poetry.urls]
3232
"Bug Tracker" = "https://github.com/erwindouna/python-tado/issues"

src/tadoasync/tadoasync.py

Lines changed: 31 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -144,13 +144,10 @@ async def login_device_flow(self) -> DeviceActivationStatus:
144144
"scope": "offline_access",
145145
}
146146

147-
if self._session is None:
148-
self._session = ClientSession()
149-
self._close_session = True
150-
151147
try:
152148
async with asyncio.timeout(self._request_timeout):
153-
request = await self._session.post(url=DEVICE_AUTH_URL, data=data)
149+
session = self._ensure_session()
150+
request = await session.post(url=DEVICE_AUTH_URL, data=data)
154151
request.raise_for_status()
155152
except asyncio.TimeoutError as err:
156153
raise TadoConnectionError(
@@ -159,6 +156,15 @@ async def login_device_flow(self) -> DeviceActivationStatus:
159156
except ClientResponseError as err:
160157
await self.check_request_status(err, login=True)
161158

159+
content_type = request.headers.get("content-type")
160+
if content_type and "application/json" not in content_type:
161+
text = await request.text()
162+
raise TadoError(
163+
"Unexpected response from Tado. Content-Type: "
164+
f"{request.headers.get('content-type')}, "
165+
f"Response body: {text}"
166+
)
167+
162168
if request.status != 200:
163169
raise TadoError(f"Failed to start device activation flow: {request.status}")
164170

@@ -198,13 +204,10 @@ async def _check_device_activation(self) -> bool:
198204
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
199205
}
200206

201-
if self._session is None:
202-
self._session = ClientSession()
203-
self._close_session = True
204-
205207
try:
206208
async with asyncio.timeout(self._request_timeout):
207-
request = await self._session.post(url=TOKEN_URL, data=data)
209+
session = self._ensure_session()
210+
request = await session.post(url=TOKEN_URL, data=data)
208211
if request.status == 400:
209212
response = await request.json()
210213
if response.get("error") == "authorization_pending":
@@ -216,6 +219,15 @@ async def _check_device_activation(self) -> bool:
216219
"Timeout occurred while connecting to Tado."
217220
) from err
218221

222+
content_type = request.headers.get("content-type")
223+
if content_type and "application/json" not in content_type:
224+
text = await request.text()
225+
raise TadoError(
226+
"Unexpected response from Tado. Content-Type: "
227+
f"{request.headers.get('content-type')}, "
228+
f"Response body: {text}"
229+
)
230+
219231
if request.status == 200:
220232
response = await request.json()
221233
self._access_token = response["access_token"]
@@ -565,7 +577,8 @@ async def _request(
565577

566578
try:
567579
async with asyncio.timeout(self._request_timeout):
568-
request = await self._session.request( # type: ignore[union-attr]
580+
session = self._ensure_session()
581+
request = await session.request(
569582
method=method.value, url=str(url), headers=headers, json=data
570583
)
571584
request.raise_for_status()
@@ -745,6 +758,13 @@ async def close(self) -> None:
745758
if self._session and self._close_session:
746759
await self._session.close()
747760

761+
def _ensure_session(self) -> ClientSession:
762+
"""Return an active aiohttp ClientSession, creating one if needed."""
763+
if self._session is None or self._session.closed:
764+
self._session = ClientSession()
765+
self._close_session = True
766+
return self._session
767+
748768
async def __aenter__(self) -> Self:
749769
"""Async enter."""
750770
await self.async_init()

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ async def client() -> AsyncGenerator[Tado, None]:
2727
session=session,
2828
request_timeout=10,
2929
) as tado:
30-
await tado.login()
30+
await tado.device_activation()
3131
yield tado
3232

3333

@@ -40,7 +40,7 @@ def _tado_oauth(responses: aioresponses) -> None:
4040
payload={
4141
"device_code": "XXX_code_XXX",
4242
"expires_in": 300,
43-
"interval": 5,
43+
"interval": 0,
4444
"user_code": "7BQ5ZQ",
4545
"verification_uri": "https://login.tado.com/oauth2/device",
4646
"verification_uri_complete": "https://login.tado.com/oauth2/device?user_code=7BQ5ZQ",

tests/test_tado.py

Lines changed: 100 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import asyncio
44
import os
55
import time
6-
from datetime import datetime, timezone
6+
from datetime import datetime, timedelta, timezone
77
from typing import Any
88
from unittest.mock import AsyncMock, MagicMock, patch
99

@@ -21,7 +21,7 @@
2121
TadoError,
2222
TadoReadingError,
2323
)
24-
from tadoasync.tadoasync import DeviceActivationStatus
24+
from tadoasync.tadoasync import DEVICE_AUTH_URL, DeviceActivationStatus
2525

2626
from syrupy import SnapshotAssertion
2727
from tests import load_fixture
@@ -63,7 +63,8 @@ async def test_login_success(responses: aioresponses) -> None:
6363
)
6464
async with aiohttp.ClientSession() as session:
6565
tado = Tado(session=session)
66-
await tado.login()
66+
await tado.async_init()
67+
await tado.device_activation()
6768
assert tado._access_token == "test_access_token"
6869
assert tado._token_expiry is not None
6970
assert tado._token_expiry > time.time()
@@ -79,39 +80,94 @@ async def test_login_success_no_session(responses: aioresponses) -> None:
7980
)
8081
async with aiohttp.ClientSession():
8182
tado = Tado()
82-
await tado.login()
83+
await tado.async_init()
84+
await tado.device_activation()
8385
assert tado._access_token == "test_access_token"
8486
assert tado._token_expiry is not None
8587
assert tado._token_expiry > time.time()
8688
assert tado._refresh_token == "test_refresh_token"
8789

8890

89-
async def test_login_timeout(python_tado: Tado, responses: aioresponses) -> None:
90-
"""Test login timeout."""
91+
async def test_activation_timeout(responses: aioresponses) -> None:
92+
"""Test activation timeout."""
93+
responses.post(
94+
DEVICE_AUTH_URL,
95+
status=200,
96+
payload={
97+
"device_code": "XXX_code_XXX",
98+
"expires_in": 1,
99+
"interval": 0,
100+
"user_code": "7BQ5ZQ",
101+
"verification_uri": "https://login.tado.com/oauth2/device",
102+
"verification_uri_complete": "https://login.tado.com/oauth2/device?user_code=7BQ5ZQ",
103+
},
104+
)
91105
responses.post(
92106
TADO_TOKEN_URL,
107+
status=400,
108+
payload={"error": "authorization_pending"},
109+
)
110+
111+
async with aiohttp.ClientSession() as session:
112+
tado = Tado(session=session)
113+
await tado.async_init()
114+
tado._expires_at = datetime.now(timezone.utc) - timedelta(seconds=1)
115+
with pytest.raises(TadoError, match="User took too long"):
116+
await tado.device_activation()
117+
118+
119+
async def test_login_device_flow_timeout(responses: aioresponses) -> None:
120+
"""Test timeout during device auth flow."""
121+
responses._matches.clear()
122+
responses.post(
123+
DEVICE_AUTH_URL,
93124
exception=asyncio.TimeoutError(),
125+
repeat=True,
94126
)
95-
with pytest.raises(TadoConnectionError):
96-
await python_tado.login()
97127

128+
async with aiohttp.ClientSession() as session:
129+
tado = Tado(session=session)
130+
with pytest.raises(TadoConnectionError):
131+
await tado.async_init()
98132

99-
async def test_login_invalid_content_type(
100-
python_tado: Tado, responses: aioresponses
101-
) -> None:
133+
134+
async def test_login_invalid_content_type(responses: aioresponses) -> None:
102135
"""Test login invalid content type."""
136+
responses._matches.clear()
103137
responses.post(
104-
TADO_TOKEN_URL,
138+
DEVICE_AUTH_URL,
105139
status=200,
106140
headers={"content-type": "text/plain"},
107141
body="Unexpected response",
108142
)
109143

110-
with pytest.raises(TadoError):
111-
await python_tado.login()
144+
async with aiohttp.ClientSession() as session:
145+
tado = Tado(session=session)
146+
with pytest.raises(TadoError):
147+
await tado.async_init()
148+
149+
150+
async def test_login_invalid_status() -> None:
151+
"""Test login with non-200 status triggers."""
152+
mock_response = MagicMock(spec=ClientResponse)
153+
mock_response.status = 400
154+
mock_response.headers = {"content-type": "application/json"}
155+
mock_response.raise_for_status = MagicMock(return_value=None)
156+
mock_response.json = AsyncMock(return_value={"error": "bad_request"})
157+
mock_response.text = AsyncMock(return_value="Unexpected response")
158+
159+
async def mock_post(*args: Any, **kwargs: Any) -> ClientResponse: # noqa: ARG001 # pylint: disable=unused-argument
160+
return mock_response
161+
162+
async with aiohttp.ClientSession() as session:
163+
tado = Tado(session=session)
164+
with patch("aiohttp.ClientSession.post", new=mock_post), pytest.raises(
165+
TadoError
166+
):
167+
await tado.async_init()
112168

113169

114-
async def test_login_client_response_error(python_tado: Tado) -> None:
170+
async def test_login_client_response_error() -> None:
115171
"""Test login client response error."""
116172
mock_request_info = MagicMock(spec=RequestInfo)
117173
mock_response = MagicMock(spec=ClientResponse)
@@ -124,10 +180,12 @@ async def test_login_client_response_error(python_tado: Tado) -> None:
124180
async def mock_post(*args: Any, **kwargs: Any) -> ClientResponse: # noqa: ARG001 # pylint: disable=unused-argument
125181
return mock_response
126182

127-
with patch("aiohttp.ClientSession.post", new=mock_post), pytest.raises(
128-
TadoAuthenticationError
129-
):
130-
await python_tado.login()
183+
async with aiohttp.ClientSession() as session:
184+
tado = Tado(session=session)
185+
with patch("aiohttp.ClientSession.post", new=mock_post), pytest.raises(
186+
TadoAuthenticationError
187+
):
188+
await tado.async_init()
131189

132190

133191
async def test_refresh_auth_success(responses: aioresponses) -> None:
@@ -188,6 +246,29 @@ async def mock_post(*args: Any, **kwargs: Any) -> ClientResponse: # noqa: ARG00
188246
await python_tado._refresh_auth()
189247

190248

249+
async def test_device_flow_sets_verification_url(responses: aioresponses) -> None:
250+
"""Test device flow sets verification URL."""
251+
responses.post(
252+
DEVICE_AUTH_URL,
253+
status=200,
254+
payload={
255+
"device_code": "XXX",
256+
"expires_in": 600,
257+
"interval": 0,
258+
"user_code": "7BQ5ZQ",
259+
"verification_uri": "https://login.tado.com/oauth2/device",
260+
},
261+
)
262+
263+
async with aiohttp.ClientSession() as session:
264+
tado = Tado(session=session)
265+
await tado.login_device_flow()
266+
assert (
267+
tado.device_verification_url
268+
== "https://login.tado.com/oauth2/device?user_code=7BQ5ZQ"
269+
)
270+
271+
191272
async def test_get_me(
192273
python_tado: Tado, responses: aioresponses, snapshot: SnapshotAssertion
193274
) -> None:
@@ -210,10 +291,6 @@ async def test_get_devices(
210291
body=load_fixture("devices.json"),
211292
)
212293
assert await python_tado.get_devices() == snapshot
213-
assert (
214-
python_tado.device_verification_url
215-
== "https://login.tado.com/oauth2/device?user_code=7BQ5ZQ"
216-
)
217294

218295

219296
async def test_get_mobile_devices(

0 commit comments

Comments
 (0)