Skip to content

Commit df78bf9

Browse files
authored
Merge pull request #204 from binarydev/add-tests
Add tests
2 parents cb26595 + 4bc4016 commit df78bf9

17 files changed

+793
-13
lines changed

.github/workflows/constraints.txt

Lines changed: 0 additions & 8 deletions
This file was deleted.

.github/workflows/tests.yaml

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ on:
1111
- cron: "0 0 * * *"
1212

1313
env:
14-
DEFAULT_PYTHON: 3.11
14+
DEFAULT_PYTHON: 3.13
1515

1616
jobs:
1717
pre-commit:
@@ -28,12 +28,12 @@ jobs:
2828

2929
- name: Upgrade pip
3030
run: |
31-
pip install --constraint=.github/workflows/constraints.txt pip
31+
pip install pip
3232
pip --version
3333
3434
- name: Install Python modules
3535
run: |
36-
pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports pipreqs
36+
pip install pre-commit black flake8 reorder-python-imports pipreqs
3737
3838
- name: Run pre-commit on all files
3939
run: |
@@ -61,3 +61,28 @@ jobs:
6161

6262
- name: Hassfest validation
6363
uses: "home-assistant/actions/hassfest@master"
64+
65+
pytest:
66+
runs-on: "ubuntu-latest"
67+
name: Pytest
68+
steps:
69+
- name: Check out the repository
70+
uses: actions/checkout@v4.2.2
71+
72+
- name: Set up Python ${{ env.DEFAULT_PYTHON }}
73+
uses: actions/setup-python@v5.6.0
74+
with:
75+
python-version: ${{ env.DEFAULT_PYTHON }}
76+
77+
- name: Upgrade pip
78+
run: |
79+
pip install pip
80+
pip --version
81+
82+
- name: Install dependencies
83+
run: |
84+
pip install -r requirements.txt
85+
86+
- name: Run pytest
87+
run: |
88+
pytest --maxfail=3 --disable-warnings --color=yes
File renamed without changes.

custom_components/generac/__init__.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@
1919
from .coordinator import GeneracDataUpdateCoordinator
2020
from .utils import async_client_session
2121

22-
2322
_LOGGER: logging.Logger = logging.getLogger(__package__)
2423

2524

pytest.ini

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[pytest]
2+
asyncio_mode = auto
3+
markers =
4+
socket: Enables socket connections

requirements.txt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
beautifulsoup4==4.13.4
22
dacite==1.9.2
3-
homeassistant==2025.3.3
3+
homeassistant==2025.7.3
44
voluptuous==0.15.2
55
pre-commit==4.2.0
66
reorder-python-imports==3.15.0
7+
pytest==8.4.0
8+
pytest-homeassistant-custom-component==0.13.263
9+
pytest-asyncio==1.0.0
10+
pytest-socket==0.7.0
11+
respx==0.22.0
12+
aioresponses==0.7.8
713
Brotli==1.1.0

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Tests for the Generac integration."""

tests/conftest.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""Global fixtures for generac integration."""
2+
import pytest
3+
4+
pytest_plugins = "pytest_homeassistant_custom_component"
5+
6+
7+
@pytest.fixture(autouse=True)
8+
def auto_enable_custom_integrations(enable_custom_integrations):
9+
"""Enable custom integrations defined in the test dir."""
10+
yield

tests/test_api.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""Test the Generac API."""
2+
import re
3+
4+
import aiohttp
5+
import pytest
6+
from aioresponses import aioresponses
7+
from custom_components.generac.api import GeneracApiClient
8+
from custom_components.generac.api import get_setting_json
9+
10+
11+
def test_get_setting_json():
12+
"""Test the get_setting_json function."""
13+
html = """<html>
14+
<head>
15+
<script>
16+
var SETTINGS = {"key": "value"};
17+
</script>
18+
</head>
19+
<body>
20+
</body>
21+
</html>"""
22+
assert get_setting_json(html) == {"key": "value"}
23+
24+
25+
def test_get_setting_json_no_settings():
26+
"""Test the get_setting_json function when there are no settings."""
27+
html = """<html>
28+
<head>
29+
</head>
30+
<body>
31+
</body>
32+
</html>"""
33+
assert get_setting_json(html) is None
34+
35+
36+
@pytest.mark.asyncio
37+
async def test_api_flow():
38+
"""Test the full API flow."""
39+
with aioresponses() as m:
40+
m.get(
41+
"https://app.mobilelinkgen.com/api/Auth/SignIn?email=test-username",
42+
status=200,
43+
body="""<html><head><script>
44+
var SETTINGS = {"csrf": "test-csrf", "transId": "test-trans-id", "config": {}, "hosts": {}};
45+
</script></head><body></body></html>""",
46+
)
47+
m.post(
48+
re.compile(
49+
r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/SelfAsserted.*"
50+
),
51+
status=200,
52+
payload={"status": "200"},
53+
)
54+
m.get(
55+
re.compile(
56+
r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/api/CombinedSigninAndSignup/confirmed.*"
57+
),
58+
status=200,
59+
body="""<html><body><form action="https://app.mobilelinkgen.com/test-action"><input name="state" value="test-state"><input name="code" value="test-code"></form></body></html>""",
60+
)
61+
m.post("https://app.mobilelinkgen.com/test-action", status=200)
62+
m.get(
63+
"https://app.mobilelinkgen.com/api/v2/Apparatus/list",
64+
status=200,
65+
payload=[
66+
{
67+
"apparatusId": 12345,
68+
"type": 0,
69+
"name": "test-name",
70+
}
71+
],
72+
)
73+
m.get(
74+
"https://app.mobilelinkgen.com/api/v1/Apparatus/details/12345",
75+
status=200,
76+
payload={"key": "value"},
77+
)
78+
79+
async with aiohttp.ClientSession() as session:
80+
client = GeneracApiClient("test-username", "test-password", session)
81+
data = await client.async_get_data()
82+
assert data is not None
83+
assert data["12345"].apparatus.apparatusId == 12345
84+
# This is a bit of a hack, but we don't have the ApparatusDetail model fully defined for this test
85+
# assert data["12345"].detail.raw == {"key": "value"}
86+
assert client.csrf == "test-csrf"

tests/test_binary_sensor.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Test the Generac binary sensor platform."""
2+
from unittest.mock import MagicMock
3+
4+
from custom_components.generac.binary_sensor import GeneracConnectedSensor
5+
from custom_components.generac.binary_sensor import GeneracConnectingSensor
6+
from custom_components.generac.binary_sensor import GeneracMaintenanceAlertSensor
7+
from custom_components.generac.binary_sensor import GeneracWarningSensor
8+
from custom_components.generac.models import Apparatus
9+
from custom_components.generac.models import ApparatusDetail
10+
from custom_components.generac.models import Item
11+
12+
13+
def get_mock_item(
14+
is_connected: bool,
15+
is_connecting: bool,
16+
has_maintenance_alert: bool,
17+
show_warning: bool,
18+
) -> Item:
19+
"""Return a mock Item object."""
20+
return Item(
21+
apparatus=Apparatus(),
22+
apparatusDetail=ApparatusDetail(
23+
isConnected=is_connected,
24+
isConnecting=is_connecting,
25+
hasMaintenanceAlert=has_maintenance_alert,
26+
showWarning=show_warning,
27+
),
28+
)
29+
30+
31+
async def test_connected_sensor(hass):
32+
"""Test the connected sensor."""
33+
coordinator = MagicMock()
34+
entry = MagicMock()
35+
36+
# Test when connected
37+
item = get_mock_item(True, False, False, False)
38+
sensor = GeneracConnectedSensor(coordinator, entry, "12345", item)
39+
assert sensor.is_on is True
40+
assert sensor.name == "generac_12345_is_connected"
41+
assert sensor.device_class == "connectivity"
42+
43+
# Test when not connected
44+
item = get_mock_item(False, False, False, False)
45+
sensor = GeneracConnectedSensor(coordinator, entry, "12345", item)
46+
assert sensor.is_on is False
47+
48+
49+
async def test_connecting_sensor(hass):
50+
"""Test the connecting sensor."""
51+
coordinator = MagicMock()
52+
entry = MagicMock()
53+
54+
# Test when connecting
55+
item = get_mock_item(False, True, False, False)
56+
sensor = GeneracConnectingSensor(coordinator, entry, "12345", item)
57+
assert sensor.is_on is True
58+
assert sensor.name == "generac_12345_is_connecting"
59+
assert sensor.device_class == "connectivity"
60+
61+
# Test when not connecting
62+
item = get_mock_item(False, False, False, False)
63+
sensor = GeneracConnectingSensor(coordinator, entry, "12345", item)
64+
assert sensor.is_on is False
65+
66+
67+
async def test_maintenance_alert_sensor(hass):
68+
"""Test the maintenance alert sensor."""
69+
coordinator = MagicMock()
70+
entry = MagicMock()
71+
72+
# Test when maintenance alert is active
73+
item = get_mock_item(False, False, True, False)
74+
sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item)
75+
assert sensor.is_on is True
76+
assert sensor.name == "generac_12345_has_maintenance_alert"
77+
assert sensor.device_class == "safety"
78+
79+
# Test when maintenance alert is not active
80+
item = get_mock_item(False, False, False, False)
81+
sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item)
82+
assert sensor.is_on is False
83+
84+
85+
async def test_warning_sensor(hass):
86+
"""Test the warning sensor."""
87+
coordinator = MagicMock()
88+
entry = MagicMock()
89+
90+
# Test when warning is active
91+
item = get_mock_item(False, False, False, True)
92+
sensor = GeneracWarningSensor(coordinator, entry, "12345", item)
93+
assert sensor.is_on is True
94+
assert sensor.name == "generac_12345_show_warning"
95+
assert sensor.device_class == "safety"
96+
97+
# Test when warning is not active
98+
item = get_mock_item(False, False, False, False)
99+
sensor = GeneracWarningSensor(coordinator, entry, "12345", item)
100+
assert sensor.is_on is False

0 commit comments

Comments
 (0)