Skip to content

Commit 9e03d84

Browse files
committed
Add tests and correct changelog
1 parent e29e1fc commit 9e03d84

File tree

3 files changed

+236
-2
lines changed

3 files changed

+236
-2
lines changed

CHANGELOG.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,17 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8-
## [0.2.2] - 2025-08-02
8+
## [0.2.3] - 2025-08-02
99

1010
### Changed
1111

1212
- Fixed callback function to async
13+
- Added changelog
14+
15+
## [0.2.2] - 2025-08-02
16+
17+
### Changed
18+
1319
- Added a method to control provisioning mode for AirOS devices.
1420
- Introduced a high-level asynchronous device discovery function for AirOS devices.
1521
- Standardized class, exception, and log naming from "Airos" to "AirOS" across the codebase.

airos/airos8.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -240,9 +240,10 @@ async def status(self) -> AirOSData:
240240
self._status_cgi_url,
241241
headers=authenticated_get_headers,
242242
) as response:
243+
response_text = await response.text()
244+
243245
if response.status == 200:
244246
try:
245-
response_text = await response.text()
246247
response_json = json.loads(response_text)
247248
try:
248249
adjusted_json = self.derived_data(response_json)
@@ -260,6 +261,7 @@ async def status(self) -> AirOSData:
260261
else:
261262
log = f"Authenticated status.cgi failed: {response.status}. Response: {response_text}"
262263
_LOGGER.error(log)
264+
raise AirOSDeviceConnectionError from None
263265
except (
264266
aiohttp.ClientError,
265267
aiohttp.client_exceptions.ConnectionTimeoutError,

tests/test_airos8.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
"""Additional tests for airos8 module."""
2+
3+
from http.cookies import SimpleCookie
4+
import json
5+
from unittest.mock import AsyncMock, MagicMock, patch
6+
7+
import airos.exceptions
8+
import pytest
9+
10+
import aiohttp
11+
12+
13+
# --- Tests for Login and Connection Errors ---
14+
@pytest.mark.asyncio
15+
async def test_login_no_csrf_token(airos_device):
16+
"""Test login response without a CSRF token header."""
17+
cookie = SimpleCookie()
18+
cookie["AIROS_TOKEN"] = "abc"
19+
20+
mock_login_response = MagicMock()
21+
mock_login_response.__aenter__.return_value = mock_login_response
22+
mock_login_response.text = AsyncMock(return_value="{}")
23+
mock_login_response.status = 200
24+
mock_login_response.cookies = cookie # Use the SimpleCookie object
25+
mock_login_response.headers = {} # Simulate missing X-CSRF-ID
26+
27+
with patch.object(airos_device.session, "post", return_value=mock_login_response):
28+
# We expect a return of None as the CSRF token is missing
29+
result = await airos_device.login()
30+
assert result is None
31+
32+
33+
@pytest.mark.asyncio
34+
async def test_login_connection_error(airos_device):
35+
"""Test aiohttp ClientError during login attempt."""
36+
with (
37+
patch.object(airos_device.session, "post", side_effect=aiohttp.ClientError),
38+
pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
39+
):
40+
await airos_device.login()
41+
42+
43+
# --- Tests for status() and derived_data() logic ---
44+
@pytest.mark.asyncio
45+
async def test_status_when_not_connected(airos_device):
46+
"""Test calling status() before a successful login."""
47+
airos_device.connected = False # Ensure connected state is false
48+
with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
49+
await airos_device.status()
50+
51+
52+
@pytest.mark.asyncio
53+
async def test_status_non_200_response(airos_device):
54+
"""Test status() with a non-successful HTTP response."""
55+
airos_device.connected = True
56+
mock_status_response = MagicMock()
57+
mock_status_response.__aenter__.return_value = mock_status_response
58+
mock_status_response.text = AsyncMock(return_value="Error")
59+
mock_status_response.status = 500 # Simulate server error
60+
61+
with (
62+
patch.object(airos_device.session, "get", return_value=mock_status_response),
63+
pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
64+
):
65+
await airos_device.status()
66+
67+
68+
@pytest.mark.asyncio
69+
async def test_status_invalid_json_response(airos_device):
70+
"""Test status() with a response that is not valid JSON."""
71+
airos_device.connected = True
72+
mock_status_response = MagicMock()
73+
mock_status_response.__aenter__.return_value = mock_status_response
74+
mock_status_response.text = AsyncMock(return_value="This is not JSON")
75+
mock_status_response.status = 200
76+
77+
with (
78+
patch.object(airos_device.session, "get", return_value=mock_status_response),
79+
pytest.raises(airos.exceptions.AirOSDataMissingError),
80+
):
81+
await airos_device.status()
82+
83+
84+
@pytest.mark.asyncio
85+
async def test_status_missing_interface_key_data(airos_device):
86+
"""Test status() with a response missing critical data fields."""
87+
airos_device.connected = True
88+
# The derived_data() function is called with a mocked response
89+
mock_status_response = MagicMock()
90+
mock_status_response.__aenter__.return_value = mock_status_response
91+
mock_status_response.text = AsyncMock(
92+
return_value=json.dumps({"system": {}})
93+
) # Missing 'interfaces'
94+
mock_status_response.status = 200
95+
96+
with (
97+
patch.object(airos_device.session, "get", return_value=mock_status_response),
98+
pytest.raises(airos.exceptions.AirOSKeyDataMissingError),
99+
):
100+
await airos_device.status()
101+
102+
103+
@pytest.mark.asyncio
104+
async def test_derived_data_no_interfaces_key(airos_device):
105+
"""Test derived_data() with a response that has no 'interfaces' key."""
106+
# This will directly test the 'if not interfaces:' branch (line 206)
107+
with pytest.raises(airos.exceptions.AirOSKeyDataMissingError):
108+
airos_device.derived_data({})
109+
110+
111+
@pytest.mark.asyncio
112+
async def test_derived_data_no_br0_eth0_ath0(airos_device):
113+
"""Test derived_data() with an unexpected interface list, to test the fallback logic."""
114+
fixture_data = {
115+
"interfaces": [
116+
{"ifname": "wan0", "enabled": True, "hwaddr": "11:22:33:44:55:66"}
117+
]
118+
}
119+
120+
adjusted_data = airos_device.derived_data(fixture_data)
121+
assert adjusted_data["derived"]["mac_interface"] == "wan0"
122+
assert adjusted_data["derived"]["mac"] == "11:22:33:44:55:66"
123+
124+
125+
# --- Tests for stakick() ---
126+
@pytest.mark.asyncio
127+
async def test_stakick_when_not_connected(airos_device):
128+
"""Test stakick() before a successful login."""
129+
airos_device.connected = False
130+
with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
131+
await airos_device.stakick("01:23:45:67:89:aB")
132+
133+
134+
@pytest.mark.asyncio
135+
async def test_stakick_no_mac_address(airos_device):
136+
"""Test stakick() with a None mac_address."""
137+
airos_device.connected = True
138+
with pytest.raises(airos.exceptions.AirOSDataMissingError):
139+
await airos_device.stakick(None)
140+
141+
142+
@pytest.mark.asyncio
143+
async def test_stakick_non_200_response(airos_device):
144+
"""Test stakick() with a non-successful HTTP response."""
145+
airos_device.connected = True
146+
mock_stakick_response = MagicMock()
147+
mock_stakick_response.__aenter__.return_value = mock_stakick_response
148+
mock_stakick_response.text = AsyncMock(return_value="Error")
149+
mock_stakick_response.status = 500
150+
151+
with patch.object(airos_device.session, "post", return_value=mock_stakick_response):
152+
assert not await airos_device.stakick("01:23:45:67:89:aB")
153+
154+
155+
@pytest.mark.asyncio
156+
async def test_stakick_connection_error(airos_device):
157+
"""Test aiohttp ClientError during stakick."""
158+
airos_device.connected = True
159+
with (
160+
patch.object(airos_device.session, "post", side_effect=aiohttp.ClientError),
161+
pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
162+
):
163+
await airos_device.stakick("01:23:45:67:89:aB")
164+
165+
166+
# --- Tests for provmode() (Complete Coverage) ---
167+
@pytest.mark.asyncio
168+
async def test_provmode_when_not_connected(airos_device):
169+
"""Test provmode() before a successful login."""
170+
airos_device.connected = False
171+
with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
172+
await airos_device.provmode(active=True)
173+
174+
175+
@pytest.mark.asyncio
176+
async def test_provmode_activate_success(airos_device):
177+
"""Test successful activation of provisioning mode."""
178+
airos_device.connected = True
179+
mock_provmode_response = MagicMock()
180+
mock_provmode_response.__aenter__.return_value = mock_provmode_response
181+
mock_provmode_response.status = 200
182+
183+
with patch.object(
184+
airos_device.session, "post", return_value=mock_provmode_response
185+
):
186+
assert await airos_device.provmode(active=True)
187+
188+
189+
@pytest.mark.asyncio
190+
async def test_provmode_deactivate_success(airos_device):
191+
"""Test successful deactivation of provisioning mode."""
192+
airos_device.connected = True
193+
mock_provmode_response = MagicMock()
194+
mock_provmode_response.__aenter__.return_value = mock_provmode_response
195+
mock_provmode_response.status = 200
196+
197+
with patch.object(
198+
airos_device.session, "post", return_value=mock_provmode_response
199+
):
200+
assert await airos_device.provmode(active=False)
201+
202+
203+
@pytest.mark.asyncio
204+
async def test_provmode_non_200_response(airos_device):
205+
"""Test provmode() with a non-successful HTTP response."""
206+
airos_device.connected = True
207+
mock_provmode_response = MagicMock()
208+
mock_provmode_response.__aenter__.return_value = mock_provmode_response
209+
mock_provmode_response.text = AsyncMock(return_value="Error")
210+
mock_provmode_response.status = 500
211+
212+
with patch.object(
213+
airos_device.session, "post", return_value=mock_provmode_response
214+
):
215+
assert not await airos_device.provmode(active=True)
216+
217+
218+
@pytest.mark.asyncio
219+
async def test_provmode_connection_error(airos_device):
220+
"""Test aiohttp ClientError during provmode."""
221+
airos_device.connected = True
222+
with (
223+
patch.object(airos_device.session, "post", side_effect=aiohttp.ClientError),
224+
pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
225+
):
226+
await airos_device.provmode(active=True)

0 commit comments

Comments
 (0)