Skip to content

Commit ae8780c

Browse files
committed
Review comments
1 parent 3d3be90 commit ae8780c

File tree

4 files changed

+39
-134
lines changed

4 files changed

+39
-134
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [0.4.1] - 2025-08-17
6+
7+
### Changed
8+
9+
- Further refactoring of the code (HA compatibility)
10+
511
## [0.4.0] - 2025-08-16
612

713
### Added

airos/airos8.py

Lines changed: 10 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,6 @@ def __init__(
6969

7070
self._use_json_for_login_post = False
7171

72-
self._common_headers = {
73-
"Accept": "application/json, text/javascript, */*; q=0.01",
74-
"Sec-Fetch-Site": "same-origin",
75-
"Accept-Language": "en-US,nl;q=0.9",
76-
"Accept-Encoding": "gzip, deflate, br",
77-
"Sec-Fetch-Mode": "cors",
78-
"Origin": self.base_url,
79-
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.5 Safari/605.1.15",
80-
"Referer": self.base_url + "/",
81-
"Connection": "keep-alive",
82-
"Sec-Fetch-Dest": "empty",
83-
"X-Requested-With": "XMLHttpRequest",
84-
}
85-
8672
self._auth_cookie: str | None = None
8773
self._csrf_id: str | None = None
8874
self.connected: bool = False
@@ -171,15 +157,14 @@ def _store_auth_data(self, response: aiohttp.ClientResponse) -> None:
171157
"""Parse the response from a successful login and store auth data."""
172158
self._csrf_id = response.headers.get("X-CSRF-ID")
173159

174-
set_cookie_header = response.headers.get("Set-Cookie")
175-
if set_cookie_header:
176-
cookie = SimpleCookie()
177-
cookie.load(set_cookie_header)
178-
179-
for key, morsel in cookie.items():
180-
if key.startswith("AIROS_"):
181-
self._auth_cookie = morsel.key[6:] + "=" + morsel.value
182-
break
160+
# Parse all Set-Cookie headers to ensure we don't miss AIROS_* cookie
161+
cookie = SimpleCookie()
162+
for set_cookie in response.headers.getall("Set-Cookie", []):
163+
cookie.load(set_cookie)
164+
for key, morsel in cookie.items():
165+
if key.startswith("AIROS_"):
166+
self._auth_cookie = morsel.key[6:] + "=" + morsel.value
167+
break
183168

184169
async def _request_json(
185170
self,
@@ -216,7 +201,7 @@ async def _request_json(
216201
) as response:
217202
response.raise_for_status()
218203
response_text = await response.text()
219-
_LOGGER.info("Successfully fetched JSON from %s", url)
204+
_LOGGER.debug("Successfully fetched JSON from %s", url)
220205

221206
# If this is the login request, we need to store the new auth data
222207
if url == self._login_url:
@@ -234,14 +219,11 @@ async def _request_json(
234219
except (TimeoutError, aiohttp.ClientError) as err:
235220
_LOGGER.exception("Error during API call to %s: %s", url, err)
236221
raise AirOSDeviceConnectionError from err
237-
except aiohttp.ClientError as err:
238-
_LOGGER.error("Aiohttp client error for %s: %s", url, err)
239-
raise AirOSDeviceConnectionError from err
240222
except json.JSONDecodeError as err:
241223
_LOGGER.error("Failed to decode JSON from %s", url)
242224
raise AirOSDataMissingError from err
243225
except asyncio.CancelledError:
244-
_LOGGER.info("Request to %s was cancelled", url)
226+
_LOGGER.warning("Request to %s was cancelled", url)
245227
raise
246228

247229
async def login(self) -> None:

tests/test_airos8.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Additional tests for airos8 module."""
22

3+
from http.cookies import SimpleCookie
34
import json
45
from unittest.mock import AsyncMock, MagicMock, patch
56

@@ -11,8 +12,8 @@
1112
import aiohttp
1213
from mashumaro.exceptions import MissingField
1314

14-
# pylint: disable=pointless-string-statement
15-
'''
15+
16+
@pytest.mark.skip(reason="broken, needs investigation")
1617
@pytest.mark.asyncio
1718
async def test_login_no_csrf_token(airos_device: AirOS) -> None:
1819
"""Test login response without a CSRF token header."""
@@ -26,11 +27,11 @@ async def test_login_no_csrf_token(airos_device: AirOS) -> None:
2627
mock_login_response.cookies = cookie # Use the SimpleCookie object
2728
mock_login_response.headers = {} # Simulate missing X-CSRF-ID
2829

29-
with patch.object(airos_device.session, "request", return_value=mock_login_response):
30+
with patch.object(
31+
airos_device.session, "request", return_value=mock_login_response
32+
):
3033
# We expect a return of None as the CSRF token is missing
31-
result = await airos_device.login()
32-
assert result is False
33-
'''
34+
await airos_device.login()
3435

3536

3637
@pytest.mark.asyncio
@@ -149,8 +150,7 @@ async def test_stakick_no_mac_address(airos_device: AirOS) -> None:
149150
await airos_device.stakick(None)
150151

151152

152-
# pylint: disable=pointless-string-statement
153-
'''
153+
@pytest.mark.skip(reason="broken, needs investigation")
154154
@pytest.mark.asyncio
155155
async def test_stakick_non_200_response(airos_device: AirOS) -> None:
156156
"""Test stakick() with a non-successful HTTP response."""
@@ -160,9 +160,10 @@ async def test_stakick_non_200_response(airos_device: AirOS) -> None:
160160
mock_stakick_response.text = AsyncMock(return_value="Error")
161161
mock_stakick_response.status = 500
162162

163-
with patch.object(airos_device.session, "request", return_value=mock_stakick_response):
163+
with patch.object(
164+
airos_device.session, "request", return_value=mock_stakick_response
165+
):
164166
assert not await airos_device.stakick("01:23:45:67:89:aB")
165-
'''
166167

167168

168169
@pytest.mark.asyncio
@@ -185,8 +186,7 @@ async def test_provmode_when_not_connected(airos_device: AirOS) -> None:
185186
await airos_device.provmode(active=True)
186187

187188

188-
# pylint: disable=pointless-string-statement
189-
'''
189+
@pytest.mark.skip(reason="broken, needs investigation")
190190
@pytest.mark.asyncio
191191
async def test_provmode_activate_success(airos_device: AirOS) -> None:
192192
"""Test successful activation of provisioning mode."""
@@ -197,12 +197,13 @@ async def test_provmode_activate_success(airos_device: AirOS) -> None:
197197
mock_provmode_response.text = AsyncMock()
198198
mock_provmode_response.text.return_value = ""
199199

200-
with (
201-
patch.object(airos_device.session, "request", return_value=mock_provmode_response)
200+
with patch.object(
201+
airos_device.session, "request", return_value=mock_provmode_response
202202
):
203203
assert await airos_device.provmode(active=True)
204204

205205

206+
@pytest.mark.skip(reason="broken, needs investigation")
206207
@pytest.mark.asyncio
207208
async def test_provmode_deactivate_success(airos_device: AirOS) -> None:
208209
"""Test successful deactivation of provisioning mode."""
@@ -219,6 +220,7 @@ async def test_provmode_deactivate_success(airos_device: AirOS) -> None:
219220
assert await airos_device.provmode(active=False)
220221

221222

223+
@pytest.mark.skip(reason="broken, needs investigation")
222224
@pytest.mark.asyncio
223225
async def test_provmode_non_200_response(airos_device: AirOS) -> None:
224226
"""Test provmode() with a non-successful HTTP response."""
@@ -232,7 +234,6 @@ async def test_provmode_non_200_response(airos_device: AirOS) -> None:
232234
airos_device.session, "request", return_value=mock_provmode_response
233235
):
234236
assert not await airos_device.provmode(active=True)
235-
'''
236237

237238

238239
@pytest.mark.asyncio

tests/test_stations.py

Lines changed: 7 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import json
55
import os
66
from typing import Any
7-
from unittest.mock import AsyncMock, patch
7+
from unittest.mock import AsyncMock, MagicMock, patch
88

99
from airos.airos8 import AirOS
10-
from airos.data import AirOS8Data as AirOSData
10+
from airos.data import AirOS8Data as AirOSData, Wireless
11+
from airos.exceptions import AirOSDeviceConnectionError, AirOSKeyDataMissingError
1112
import pytest
1213

1314
import aiofiles
15+
from mashumaro import MissingField
1416

1517

1618
async def _read_fixture(fixture: str = "loco5ac_ap-ptp") -> Any:
@@ -26,8 +28,7 @@ async def _read_fixture(fixture: str = "loco5ac_ap-ptp") -> Any:
2628
pytest.fail(f"Invalid JSON in fixture file {path}: {e}")
2729

2830

29-
# pylint: disable=pointless-string-statement
30-
'''
31+
@pytest.mark.skip(reason="broken, needs investigation")
3132
@patch("airos.airos8._LOGGER")
3233
@pytest.mark.asyncio
3334
async def test_status_logs_redacted_data_on_invalid_value(
@@ -91,11 +92,9 @@ async def test_status_logs_redacted_data_on_invalid_value(
9192
assert "status" in logged_data["interfaces"][2]
9293
assert "ipaddr" in logged_data["interfaces"][2]["status"]
9394
assert logged_data["interfaces"][2]["status"]["ipaddr"] == "127.0.0.3"
94-
'''
9595

9696

97-
# pylint: disable=pointless-string-statement
98-
'''
97+
@pytest.mark.skip(reason="broken, needs investigation")
9998
@patch("airos.airos8._LOGGER")
10099
@pytest.mark.asyncio
101100
async def test_status_logs_exception_on_missing_field(
@@ -139,7 +138,6 @@ async def test_status_logs_exception_on_missing_field(
139138
assert log_args[0] == "API call to %s failed with status %d: %s"
140139
assert log_args[2] == 500
141140
assert log_args[3] == "Error"
142-
'''
143141

144142

145143
@pytest.mark.parametrize(
@@ -157,8 +155,6 @@ async def test_status_logs_exception_on_missing_field(
157155
async def test_ap_object(
158156
airos_device: AirOS, base_url: str, mode: str, fixture: str
159157
) -> None:
160-
"""Test device operation."""
161-
162158
"""Test device operation using the new _request_json method."""
163159
fixture_data = await _read_fixture(fixture)
164160

@@ -190,8 +186,7 @@ async def test_ap_object(
190186
cookie["AIROS_TOKEN"] = "abc123"
191187

192188

193-
# pylint: disable=pointless-string-statement
194-
'''
189+
@pytest.mark.skip(reason="broken, needs investigation")
195190
@pytest.mark.asyncio
196191
async def test_reconnect(airos_device: AirOS, base_url: str) -> None:
197192
"""Test reconnect client."""
@@ -209,82 +204,3 @@ async def test_reconnect(airos_device: AirOS, base_url: str) -> None:
209204
patch.object(airos_device, "connected", True),
210205
):
211206
assert await airos_device.stakick("01:23:45:67:89:aB")
212-
'''
213-
214-
215-
# pylint: disable=pointless-string-statement
216-
'''
217-
@pytest.mark.asyncio
218-
async def test_ap_corners(
219-
airos_device: AirOS, base_url: str, mode: str = "ap-ptp"
220-
) -> None:
221-
"""Test device operation."""
222-
cookie = SimpleCookie()
223-
cookie["session_id"] = "test-cookie"
224-
cookie["AIROS_TOKEN"] = "abc123"
225-
226-
mock_login_response = MagicMock()
227-
mock_login_response.__aenter__.return_value = mock_login_response
228-
mock_login_response.text = AsyncMock(return_value="{}")
229-
mock_login_response.status = 200
230-
mock_login_response.cookies = cookie
231-
mock_login_response.headers = {"X-CSRF-ID": "test-csrf-token"}
232-
233-
# Test case 1: Successful login
234-
with (
235-
patch.object(airos_device.session, "post", return_value=mock_login_response),
236-
patch.object(airos_device, "_use_json_for_login_post", return_value=True),
237-
):
238-
assert await airos_device.login()
239-
240-
# Test case 2: Login fails with missing cookies (expects an exception)
241-
mock_login_response.cookies = {}
242-
with (
243-
patch.object(airos_device.session, "post", return_value=mock_login_response),
244-
patch.object(airos_device, "_use_json_for_login_post", return_value=True),
245-
pytest.raises(AirOSConnectionSetupError),
246-
):
247-
# Only call the function; no return value to assert.
248-
await airos_device.login()
249-
250-
# Test case 3: Login successful, returns None due to missing headers
251-
mock_login_response.cookies = cookie
252-
mock_login_response.headers = {}
253-
with (
254-
patch.object(airos_device.session, "post", return_value=mock_login_response),
255-
patch.object(airos_device, "_use_json_for_login_post", return_value=True),
256-
):
257-
result = await airos_device.login()
258-
assert result is False
259-
260-
# Test case 4: Login fails with bad data from the API (expects an exception)
261-
mock_login_response.headers = {"X-CSRF-ID": "test-csrf-token"}
262-
mock_login_response.text = AsyncMock(return_value="abc123")
263-
with (
264-
patch.object(airos_device.session, "post", return_value=mock_login_response),
265-
patch.object(airos_device, "_use_json_for_login_post", return_value=True),
266-
pytest.raises(AirOSDataMissingError),
267-
):
268-
# Only call the function; no return value to assert.
269-
await airos_device.login()
270-
271-
# Test case 5: Login fails due to HTTP status code (expects an exception)
272-
mock_login_response.text = AsyncMock(return_value="{}")
273-
mock_login_response.status = 400
274-
with (
275-
patch.object(airos_device.session, "post", return_value=mock_login_response),
276-
patch.object(airos_device, "_use_json_for_login_post", return_value=True),
277-
pytest.raises(AirOSConnectionAuthenticationError),
278-
):
279-
# Only call the function; no return value to assert.
280-
await airos_device.login()
281-
282-
# Test case 6: Login fails due to client-level connection error (expects an exception)
283-
mock_login_response.status = 200
284-
with (
285-
patch.object(airos_device.session, "post", side_effect=aiohttp.ClientError),
286-
pytest.raises(AirOSDeviceConnectionError),
287-
):
288-
# Only call the function; no return value to assert.
289-
await airos_device.login()
290-
'''

0 commit comments

Comments
 (0)