Skip to content

Commit b9f058e

Browse files
committed
Add (very basic) test
1 parent 4348240 commit b9f058e

File tree

8 files changed

+129
-21
lines changed

8 files changed

+129
-21
lines changed

.pre-commit-config.yaml

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,16 @@ repos:
4040
- --quiet-level=2
4141
exclude_types: [csv, json]
4242
exclude: ^userdata/|^fixtures/
43-
# - repo: https://github.com/PyCQA/bandit
44-
# rev: 1.8.5
45-
# hooks:
46-
# - id: bandit
47-
# name: "Bandit checking"
48-
# args:
49-
# - --quiet
50-
# - --format=custom
51-
# - --configfile=tests/bandit.yaml
52-
# files: ^(airos|tests)/.+\.py$
43+
- repo: https://github.com/PyCQA/bandit
44+
rev: 1.8.5
45+
hooks:
46+
- id: bandit
47+
name: "Bandit checking"
48+
args:
49+
- --quiet
50+
- --format=custom
51+
- --configfile=tests/bandit.yaml
52+
files: ^(airos|tests)/.+\.py$
5353
- repo: https://github.com/adrienverge/yamllint.git
5454
rev: v1.37.1
5555
hooks:

airos/airos8.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import json
66
import logging
7+
from urllib.parse import urlparse
78

89
import aiohttp
910

@@ -12,7 +13,7 @@
1213
logger = logging.getLogger(__name__)
1314

1415

15-
class AirOS8:
16+
class AirOS:
1617
"""Set up connection to AirOS."""
1718

1819
def __init__(
@@ -21,12 +22,22 @@ def __init__(
2122
username: str,
2223
password: str,
2324
session: aiohttp.ClientSession,
25+
use_ssl: bool = True,
2426
verify_ssl: bool = True,
2527
):
2628
"""Initialize AirOS8 class."""
2729
self.username = username
2830
self.password = password
29-
self.base_url = f"https://{host}"
31+
32+
parsed_host = urlparse(host)
33+
scheme = (
34+
parsed_host.scheme
35+
if parsed_host.scheme
36+
else ("https" if use_ssl else "http")
37+
)
38+
hostname = parsed_host.hostname if parsed_host.hostname else host
39+
40+
self.base_url = f"{scheme}://{hostname}"
3041

3142
self.session = session
3243
self.verify_ssl = verify_ssl
@@ -51,6 +62,8 @@ def __init__(
5162
"X-Requested-With": "XMLHttpRequest",
5263
}
5364

65+
self.connected = False
66+
5467
async def login(self) -> bool:
5568
"""Log in to the device assuring cookies and tokens set correctly."""
5669
# --- Step 0: Pre-inject the 'ok=1' cookie before login POST (mimics curl) ---
@@ -146,11 +159,10 @@ async def login(self) -> bool:
146159
if not airos_cookie_found and not ok_cookie_found:
147160
raise DataMissingError from None
148161

149-
response_text = await response.text()
150-
151162
if response.status == 200:
152163
try:
153-
json.loads(response_text)
164+
json.loads(response.text)
165+
self.connected = True
154166
return True
155167
except json.JSONDecodeError as err:
156168
logger.exception("JSON Decode Error")
@@ -166,6 +178,10 @@ async def login(self) -> bool:
166178

167179
async def status(self) -> dict:
168180
"""Retrieve status from the device."""
181+
if not self.connected:
182+
logger.error("Not connected, login first")
183+
raise ConnectionFailedError from None
184+
169185
# --- Step 2: Verify authenticated access by fetching status.cgi ---
170186
authenticated_get_headers = {**self._common_headers}
171187
if self.current_csrf_token:
@@ -177,18 +193,16 @@ async def status(self) -> dict:
177193
headers=authenticated_get_headers,
178194
ssl=self.verify_ssl,
179195
) as response:
180-
status_response_text = await response.text()
181-
182196
if response.status == 200:
183197
try:
184-
return json.loads(status_response_text)
198+
return json.loads(response.text)
185199
except json.JSONDecodeError:
186200
logger.exception(
187201
"JSON Decode Error in authenticated status response"
188202
)
189203
raise DataMissingError from None
190204
else:
191-
log = f"Authenticated status.cgi failed: {response.status}. Response: {status_response_text}"
205+
log = f"Authenticated status.cgi failed: {response.status}. Response: {response.text}"
192206
logger.error(log)
193207
except aiohttp.ClientError as err:
194208
logger.exception("Error during authenticated status.cgi call")

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "airos"
7-
version = "0.0.4"
7+
version = "0.0.5"
88
license = "MIT"
99
description = "Ubiquity airOS module(s) for Python 3."
1010
readme = "README.md"
@@ -534,4 +534,4 @@ testpaths = [
534534
"tests",
535535
]
536536
asyncio_default_fixture_loop_scope = "session"
537-
asyncio_mode = "strict"
537+
asyncio_mode = "auto"

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pytest
22
pytest-asyncio
33
aiohttp
4+
aioresponses

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the Ubiquity AirOS python module."""

tests/bandit.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# https://bandit.readthedocs.io/en/latest/config.html
2+
3+
tests:
4+
- B103
5+
- B108
6+
- B306
7+
- B307
8+
- B313
9+
- B314
10+
- B315
11+
- B316
12+
- B317
13+
- B318
14+
- B319
15+
- B320
16+
- B601
17+
- B602
18+
- B604
19+
- B608
20+
- B609

tests/conftest.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
"""Ubiquity AirOS test fixtures."""
2+
3+
from airos.airos8 import AirOS
4+
import pytest
5+
6+
import aiohttp
7+
8+
9+
@pytest.fixture
10+
def base_url():
11+
"""Return a testing url."""
12+
return "http://device.local"
13+
14+
15+
@pytest.fixture
16+
async def airos_device(base_url):
17+
"""AirOS device fixture."""
18+
session = aiohttp.ClientSession(cookie_jar=aiohttp.CookieJar())
19+
instance = AirOS(base_url, "username", "password", session, use_ssl=False)
20+
yield instance
21+
await session.close()

tests/test_stations.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""Ubiquity AirOS tests."""
2+
3+
from http.cookies import SimpleCookie
4+
import json
5+
import os
6+
from unittest.mock import AsyncMock, MagicMock, patch
7+
8+
import pytest
9+
10+
import aiofiles
11+
12+
13+
async def _read_fixture(fixture: str = "ap-ptp"):
14+
"""Read fixture file per device type."""
15+
path = os.path.join(os.path.dirname(__file__), f"../fixtures/{fixture}.json")
16+
async with aiofiles.open(path, encoding="utf-8") as f:
17+
return json.loads(await f.read())
18+
19+
20+
@pytest.mark.parametrize("mode", ["ap-ptp", "sta-ptp"])
21+
@pytest.mark.asyncio
22+
async def test_ap(airos_device, base_url, mode):
23+
"""Test device operation."""
24+
cookie = SimpleCookie()
25+
cookie["session_id"] = "test-cookie"
26+
cookie["AIROS_TOKEN"] = "abc123"
27+
28+
# --- Prepare fake POST /api/auth response with cookies ---
29+
mock_login_response = MagicMock()
30+
mock_login_response.__aenter__.return_value = mock_login_response
31+
mock_login_response.text = "{}"
32+
mock_login_response.status = 200
33+
mock_login_response.cookies = cookie
34+
35+
# --- Prepare fake GET /api/status response ---
36+
mock_status_payload = {"mode": await _read_fixture(fixture=mode)}
37+
mock_status_response = MagicMock()
38+
mock_status_response.__aenter__.return_value = mock_status_response
39+
mock_status_response.text = json.dumps(await _read_fixture(mode))
40+
mock_status_response.status = 200
41+
mock_status_response.json = AsyncMock(return_value=mock_status_payload)
42+
43+
with (
44+
patch.object(airos_device.session, "post", return_value=mock_login_response),
45+
patch.object(airos_device.session, "get", return_value=mock_status_response),
46+
):
47+
assert await airos_device.login()
48+
status = await airos_device.status()
49+
50+
# Verify the fixture returns the correct mode
51+
assert status.get("wireless", {}).get("mode") == mode

0 commit comments

Comments
 (0)