File: test_airos8.py

package info (click to toggle)
python-airos 0.6.4-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 704 kB
  • sloc: python: 2,967; sh: 19; makefile: 3
file content (420 lines) | stat: -rw-r--r-- 15,508 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
"""Additional tests for airOS8 module."""

from http.cookies import SimpleCookie
import json
from unittest.mock import AsyncMock, MagicMock, patch

import aiofiles
import aiohttp
from mashumaro.exceptions import MissingField
import pytest

from airos.airos8 import AirOS8
import airos.exceptions


@pytest.mark.skip(reason="broken, needs investigation")
@pytest.mark.asyncio
async def test_login_no_csrf_token(airos8_device: AirOS8) -> None:
    """Test login response without a CSRF token header."""
    cookie = SimpleCookie()
    cookie["AIROS_TOKEN"] = "abc"

    mock_login_response = MagicMock()
    mock_login_response.__aenter__.return_value = mock_login_response
    mock_login_response.text = AsyncMock(return_value="{}")
    mock_login_response.status = 200
    mock_login_response.cookies = cookie  # Use the SimpleCookie object
    mock_login_response.headers = {}  # Simulate missing X-CSRF-ID

    with patch.object(
        airos8_device.session, "request", return_value=mock_login_response
    ):
        # We expect a return of None as the CSRF token is missing
        await airos8_device.login()


@pytest.mark.asyncio
async def test_login_connection_error(airos8_device: AirOS8) -> None:
    """Test aiohttp ClientError during login attempt."""
    with (
        patch.object(airos8_device.session, "request", side_effect=aiohttp.ClientError),
        pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
    ):
        await airos8_device.login()


# --- Tests for status() and derived_data() logic ---
@pytest.mark.asyncio
async def test_status_when_not_connected(airos8_device: AirOS8) -> None:
    """Test calling status() before a successful login."""
    airos8_device.connected = False  # Ensure connected state is false
    with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
        await airos8_device.status()


# pylint: disable=pointless-string-statement
'''
@pytest.mark.asyncio
async def test_status_non_200_response(airos8_device: AirOS8) -> None:
    """Test status() with a non-successful HTTP response."""
    airos8_device.connected = True
    mock_status_response = MagicMock()
    mock_status_response.__aenter__.return_value = mock_status_response
    mock_status_response.text = AsyncMock(return_value="Error")
    mock_status_response.status = 500  # Simulate server error

    with (
        patch.object(airos8_device.session, "request", return_value=mock_status_response),
        pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
    ):
        await airos8_device.status()
'''


@pytest.mark.asyncio
async def test_status_invalid_json_response(airos8_device: AirOS8) -> None:
    """Test status() with a response that is not valid JSON."""
    airos8_device.connected = True
    mock_status_response = MagicMock()
    mock_status_response.__aenter__.return_value = mock_status_response
    mock_status_response.text = AsyncMock(return_value="This is not JSON")
    mock_status_response.status = 200

    with (
        patch.object(
            airos8_device.session, "request", return_value=mock_status_response
        ),
        pytest.raises(airos.exceptions.AirOSDataMissingError),
    ):
        await airos8_device.status()


@pytest.mark.asyncio
async def test_status_missing_interface_key_data(airos8_device: AirOS8) -> None:
    """Test status() with a response missing critical data fields."""
    airos8_device.connected = True
    # The derived_data() function is called with a mocked response
    mock_status_response = MagicMock()
    mock_status_response.__aenter__.return_value = mock_status_response
    mock_status_response.text = AsyncMock(
        return_value=json.dumps({"system": {}})
    )  # Missing 'interfaces'
    mock_status_response.status = 200

    with (
        patch.object(
            airos8_device.session, "request", return_value=mock_status_response
        ),
        pytest.raises(airos.exceptions.AirOSKeyDataMissingError),
    ):
        await airos8_device.status()


@pytest.mark.asyncio
async def test_derived_data_no_interfaces_key(airos8_device: AirOS8) -> None:
    """Test derived_data() with a response that has no 'interfaces' key."""
    # This will directly test the 'if not interfaces:' branch (line 206)
    with pytest.raises(airos.exceptions.AirOSKeyDataMissingError):
        airos8_device.derived_data({})


@pytest.mark.asyncio
async def test_derived_data_no_br0_eth0_ath0(airos8_device: AirOS8) -> None:
    """Test derived_data() with an unexpected interface list, to test the fallback logic."""
    fixture_data = {
        "host": {
            "fwversion": "v8.0.0",
        },
        "interfaces": [
            {"ifname": "wan0", "enabled": True, "hwaddr": "11:22:33:44:55:66"}
        ],
    }

    adjusted_data = airos8_device.derived_data(fixture_data)
    assert adjusted_data["derived"]["mac_interface"] == "wan0"
    assert adjusted_data["derived"]["mac"] == "11:22:33:44:55:66"


# --- Tests for stakick() ---
@pytest.mark.asyncio
async def test_stakick_when_not_connected(airos8_device: AirOS8) -> None:
    """Test stakick() before a successful login."""
    airos8_device.connected = False
    with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
        await airos8_device.stakick("01:23:45:67:89:aB")


@pytest.mark.asyncio
async def test_stakick_no_mac_address(airos8_device: AirOS8) -> None:
    """Test stakick() with a None mac_address."""
    airos8_device.connected = True
    with pytest.raises(airos.exceptions.AirOSDataMissingError):
        await airos8_device.stakick(None)


@pytest.mark.skip(reason="broken, needs investigation")
@pytest.mark.asyncio
async def test_stakick_non_200_response(airos8_device: AirOS8) -> None:
    """Test stakick() with a non-successful HTTP response."""
    airos8_device.connected = True
    mock_stakick_response = MagicMock()
    mock_stakick_response.__aenter__.return_value = mock_stakick_response
    mock_stakick_response.text = AsyncMock(return_value="Error")
    mock_stakick_response.status = 500

    with patch.object(
        airos8_device.session, "request", return_value=mock_stakick_response
    ):
        assert not await airos8_device.stakick("01:23:45:67:89:aB")


@pytest.mark.asyncio
async def test_stakick_connection_error(airos8_device: AirOS8) -> None:
    """Test aiohttp ClientError during stakick."""
    airos8_device.connected = True
    with (
        patch.object(airos8_device.session, "request", side_effect=aiohttp.ClientError),
        pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
    ):
        await airos8_device.stakick("01:23:45:67:89:aB")


# --- Tests for provmode() (Complete Coverage) ---
@pytest.mark.asyncio
async def test_provmode_when_not_connected(airos8_device: AirOS8) -> None:
    """Test provmode() before a successful login."""
    airos8_device.connected = False
    with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
        await airos8_device.provmode(active=True)


@pytest.mark.skip(reason="broken, needs investigation")
@pytest.mark.asyncio
async def test_provmode_activate_success(airos8_device: AirOS8) -> None:
    """Test successful activation of provisioning mode."""
    airos8_device.connected = True
    mock_provmode_response = MagicMock()
    mock_provmode_response.__aenter__.return_value = mock_provmode_response
    mock_provmode_response.status = 200
    mock_provmode_response.text = AsyncMock()
    mock_provmode_response.text.return_value = ""

    with patch.object(
        airos8_device.session, "request", return_value=mock_provmode_response
    ):
        assert await airos8_device.provmode(active=True)


@pytest.mark.skip(reason="broken, needs investigation")
@pytest.mark.asyncio
async def test_provmode_deactivate_success(airos8_device: AirOS8) -> None:
    """Test successful deactivation of provisioning mode."""
    airos8_device.connected = True
    mock_provmode_response = MagicMock()
    mock_provmode_response.__aenter__.return_value = mock_provmode_response
    mock_provmode_response.status = 200
    mock_provmode_response.text = AsyncMock()
    mock_provmode_response.text.return_value = ""

    with patch.object(
        airos8_device.session, "request", return_value=mock_provmode_response
    ):
        assert await airos8_device.provmode(active=False)


@pytest.mark.skip(reason="broken, needs investigation")
@pytest.mark.asyncio
async def test_provmode_non_200_response(airos8_device: AirOS8) -> None:
    """Test provmode() with a non-successful HTTP response."""
    airos8_device.connected = True
    mock_provmode_response = MagicMock()
    mock_provmode_response.__aenter__.return_value = mock_provmode_response
    mock_provmode_response.text = AsyncMock(return_value="Error")
    mock_provmode_response.status = 500

    with patch.object(
        airos8_device.session, "request", return_value=mock_provmode_response
    ):
        assert not await airos8_device.provmode(active=True)


@pytest.mark.asyncio
async def test_provmode_connection_error(airos8_device: AirOS8) -> None:
    """Test aiohttp ClientError during provmode."""
    airos8_device.connected = True
    with (
        patch.object(airos8_device.session, "request", side_effect=aiohttp.ClientError),
        pytest.raises(airos.exceptions.AirOSDeviceConnectionError),
    ):
        await airos8_device.provmode(active=True)


@pytest.mark.asyncio
async def test_status_missing_required_key_in_json(airos8_device: AirOS8) -> None:
    """Test status() with a response missing a key required by the dataclass."""
    airos8_device.connected = True
    # Fixture is valid JSON, but is missing the entire 'wireless' block,
    # which is a required field for the AirOS8Data dataclass.
    invalid_data = {
        "host": {"hostname": "test", "fwversion": "v8.0.0"},
        "interfaces": [
            {"ifname": "br0", "hwaddr": "11:22:33:44:55:66", "enabled": True}
        ],
    }

    mock_status_response = MagicMock()
    mock_status_response.__aenter__.return_value = mock_status_response
    mock_status_response.text = AsyncMock(return_value=json.dumps(invalid_data))
    mock_status_response.status = 200

    with (
        patch.object(
            airos8_device.session, "request", return_value=mock_status_response
        ),
        patch("airos.base._LOGGER.exception") as mock_log_exception,
        pytest.raises(airos.exceptions.AirOSKeyDataMissingError) as excinfo,
    ):
        await airos8_device.status()

    # Check that the specific mashumaro error is logged and caught
    mock_log_exception.assert_called_once()
    assert "Failed to deserialize AirOS data" in mock_log_exception.call_args[0][0]
    # --- MODIFICATION START ---
    # Assert that the cause of our exception is the correct type from mashumaro
    assert isinstance(excinfo.value.__cause__, MissingField)


# --- Tests for warnings() and update_check() ---
@pytest.mark.asyncio
async def test_warnings_correctly_parses_json() -> None:
    """Test that the warnings() method correctly parses a valid JSON response."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = True

    mock_response = MagicMock()
    mock_response.__aenter__.return_value = mock_response
    mock_response.status = 200
    async with aiofiles.open("fixtures/warnings.json") as f:
        content = await f.read()
        mock_response_data = json.loads(content)
    mock_response.text = AsyncMock(return_value=json.dumps(mock_response_data))

    with patch.object(airos8_device.session, "request", return_value=mock_response):
        result = await airos8_device.warnings()
        assert result["isDefaultPasswd"] is False
        assert result["chAvailable"] is False


@pytest.mark.asyncio
async def test_warnings_raises_exception_on_invalid_json() -> None:
    """Test that warnings() raises an exception on invalid JSON response."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = True

    mock_response = MagicMock()
    mock_response.__aenter__.return_value = mock_response
    mock_response.status = 200
    mock_response.text = AsyncMock(return_value="This is not JSON")

    with (
        patch.object(airos8_device.session, "request", return_value=mock_response),
        pytest.raises(airos.exceptions.AirOSDataMissingError),
    ):
        await airos8_device.warnings()


@pytest.mark.asyncio
async def test_update_check_correctly_parses_json() -> None:
    """Test that update_check() method correctly parses a valid JSON response."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = True
    airos8_device.current_csrf_token = "mock-csrf-token"

    mock_response = MagicMock()
    mock_response.__aenter__.return_value = mock_response
    mock_response.status = 200
    async with aiofiles.open("fixtures/update_check_available.json") as f:
        content = await f.read()
        mock_response_data = json.loads(content)
    mock_response.text = AsyncMock(return_value=json.dumps(mock_response_data))

    with patch.object(airos8_device.session, "request", return_value=mock_response):
        result = await airos8_device.update_check()
        assert result["version"] == "v8.7.19"
        assert result["update"] is True


@pytest.mark.asyncio
async def test_update_check_raises_exception_on_invalid_json() -> None:
    """Test that update_check() raises an exception on invalid JSON response."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = True
    airos8_device.current_csrf_token = "mock-csrf-token"

    mock_response = MagicMock()
    mock_response.__aenter__.return_value = mock_response
    mock_response.status = 200
    mock_response.text = AsyncMock(return_value="This is not JSON")

    with (
        patch.object(airos8_device.session, "request", return_value=mock_response),
        pytest.raises(airos.exceptions.AirOSDataMissingError),
    ):
        await airos8_device.update_check()


@pytest.mark.asyncio
async def test_warnings_when_not_connected() -> None:
    """Test calling warnings() before a successful login."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = False  # Explicitly set connected state to false

    with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
        await airos8_device.warnings()


@pytest.mark.asyncio
async def test_update_check_when_not_connected() -> None:
    """Test calling update_check() before a successful login."""
    mock_session = MagicMock()
    airos8_device = AirOS8(
        host="http://192.168.1.3",
        username="test",
        password="test",
        session=mock_session,
    )
    airos8_device.connected = False  # Explicitly set connected state to false

    with pytest.raises(airos.exceptions.AirOSDeviceConnectionError):
        await airos8_device.update_check()