Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
168283b
fix(tests): fix broken api tests
binarydev Jul 14, 2025
b748619
Set up basic tests and moved local_test for the API call into a prope…
binarydev Jul 14, 2025
51dcb8b
feat(tests): add tests for coordinator
binarydev Jul 14, 2025
1798737
feat(tests): add tests for binary sensors
binarydev Jul 14, 2025
25cb0fe
feat(tests): add tests for sensors
binarydev Jul 14, 2025
463ce92
Add test coverage for weather.py
binarydev Jul 14, 2025
d488a4f
feat(tests): add tests for diagnostics and entity
binarydev Jul 20, 2025
295b081
feat(tests): add tests for image
binarydev Jul 20, 2025
8199aba
Fix pip package versions
binarydev Jul 20, 2025
0b20814
Move local test back to root directory since it's not meant to be a u…
binarydev Jul 20, 2025
2f7caaa
Update homeassistant package version
binarydev Jul 20, 2025
e2285cd
Prevent local access test from being used by pytest
binarydev Jul 20, 2025
1529116
Restore missing newline for requirements file
binarydev Jul 20, 2025
d27074e
Bump pre-commit from 4.1.0 to 4.2.0
dependabot[bot] Mar 19, 2025
687ce6e
Bump reorder-python-imports from 3.14.0 to 3.15.0
dependabot[bot] May 26, 2025
a988a87
added helper for aiohttp to return clientsession with quote_cookie=F…
Jun 19, 2025
9b92487
Fix linting errors
binarydev Jun 19, 2025
1d12786
Set up basic tests and moved local_test for the API call into a prope…
binarydev Jul 14, 2025
9d43e1a
Move local test back to root directory since it's not meant to be a u…
binarydev Jul 20, 2025
d27d426
Prevent local access test from being used by pytest
binarydev Jul 20, 2025
17db3a7
Sort out dependencies
binarydev Jul 25, 2025
ac2b6f0
Remove aiohttp, was readded in rebase by mistake
binarydev Jul 25, 2025
40b47bd
Fix lint errors
binarydev Jul 25, 2025
3ec5ee3
Remove flake8 and sort out imports
binarydev Jul 25, 2025
3e21802
Sort out pre-commits
binarydev Jul 25, 2025
ed809c1
Restore flake8 support
binarydev Jul 25, 2025
e8a27be
Add pytests to the github workflow
binarydev Jul 25, 2025
a488e1c
Remove empty lines
binarydev Jul 25, 2025
3094b9a
Remove constraints for unit tests
binarydev Jul 25, 2025
6d514af
Bump python default version to 3.13
binarydev Jul 25, 2025
4bc4016
Remove package constraints for github workflows
binarydev Jul 25, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions .github/workflows/constraints.txt

This file was deleted.

31 changes: 28 additions & 3 deletions .github/workflows/tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ on:
- cron: "0 0 * * *"

env:
DEFAULT_PYTHON: 3.11
DEFAULT_PYTHON: 3.13

jobs:
pre-commit:
Expand All @@ -28,12 +28,12 @@ jobs:

- name: Upgrade pip
run: |
pip install --constraint=.github/workflows/constraints.txt pip
pip install pip
pip --version

- name: Install Python modules
run: |
pip install --constraint=.github/workflows/constraints.txt pre-commit black flake8 reorder-python-imports pipreqs
pip install pre-commit black flake8 reorder-python-imports pipreqs

- name: Run pre-commit on all files
run: |
Expand Down Expand Up @@ -61,3 +61,28 @@ jobs:

- name: Hassfest validation
uses: "home-assistant/actions/hassfest@master"

pytest:
runs-on: "ubuntu-latest"
name: Pytest
steps:
- name: Check out the repository
uses: actions/checkout@v4.2.2

- name: Set up Python ${{ env.DEFAULT_PYTHON }}
uses: actions/setup-python@v5.6.0
with:
python-version: ${{ env.DEFAULT_PYTHON }}

- name: Upgrade pip
run: |
pip install pip
pip --version

- name: Install dependencies
run: |
pip install -r requirements.txt

- name: Run pytest
run: |
pytest --maxfail=3 --disable-warnings --color=yes
File renamed without changes.
1 change: 0 additions & 1 deletion custom_components/generac/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
from .coordinator import GeneracDataUpdateCoordinator
from .utils import async_client_session


_LOGGER: logging.Logger = logging.getLogger(__package__)


Expand Down
4 changes: 4 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[pytest]
asyncio_mode = auto
markers =
socket: Enables socket connections
8 changes: 7 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
beautifulsoup4==4.13.4
dacite==1.9.2
homeassistant==2025.3.3
homeassistant==2025.7.3
voluptuous==0.15.2
pre-commit==4.2.0
reorder-python-imports==3.15.0
pytest==8.4.0
pytest-homeassistant-custom-component==0.13.263
pytest-asyncio==1.0.0
pytest-socket==0.7.0
respx==0.22.0
aioresponses==0.7.8
Brotli==1.1.0
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Generac integration."""
10 changes: 10 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Global fixtures for generac integration."""
import pytest

pytest_plugins = "pytest_homeassistant_custom_component"


@pytest.fixture(autouse=True)
def auto_enable_custom_integrations(enable_custom_integrations):
"""Enable custom integrations defined in the test dir."""
yield
86 changes: 86 additions & 0 deletions tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Test the Generac API."""
import re

import aiohttp
import pytest
from aioresponses import aioresponses
from custom_components.generac.api import GeneracApiClient
from custom_components.generac.api import get_setting_json


def test_get_setting_json():
"""Test the get_setting_json function."""
html = """<html>
<head>
<script>
var SETTINGS = {"key": "value"};
</script>
</head>
<body>
</body>
</html>"""
assert get_setting_json(html) == {"key": "value"}


def test_get_setting_json_no_settings():
"""Test the get_setting_json function when there are no settings."""
html = """<html>
<head>
</head>
<body>
</body>
</html>"""
assert get_setting_json(html) is None


@pytest.mark.asyncio
async def test_api_flow():
"""Test the full API flow."""
with aioresponses() as m:
m.get(
"https://app.mobilelinkgen.com/api/Auth/SignIn?email=test-username",
status=200,
body="""<html><head><script>
var SETTINGS = {"csrf": "test-csrf", "transId": "test-trans-id", "config": {}, "hosts": {}};
</script></head><body></body></html>""",
)
m.post(
re.compile(
r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/SelfAsserted.*"
),
status=200,
payload={"status": "200"},
)
m.get(
re.compile(
r"https://generacconnectivity.b2clogin.com/generacconnectivity.onmicrosoft.com/B2C_1A_MobileLink_SignIn/api/CombinedSigninAndSignup/confirmed.*"
),
status=200,
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>""",
)
m.post("https://app.mobilelinkgen.com/test-action", status=200)
m.get(
"https://app.mobilelinkgen.com/api/v2/Apparatus/list",
status=200,
payload=[
{
"apparatusId": 12345,
"type": 0,
"name": "test-name",
}
],
)
m.get(
"https://app.mobilelinkgen.com/api/v1/Apparatus/details/12345",
status=200,
payload={"key": "value"},
)

async with aiohttp.ClientSession() as session:
client = GeneracApiClient("test-username", "test-password", session)
data = await client.async_get_data()
assert data is not None
assert data["12345"].apparatus.apparatusId == 12345
# This is a bit of a hack, but we don't have the ApparatusDetail model fully defined for this test
# assert data["12345"].detail.raw == {"key": "value"}
assert client.csrf == "test-csrf"
100 changes: 100 additions & 0 deletions tests/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
"""Test the Generac binary sensor platform."""
from unittest.mock import MagicMock

from custom_components.generac.binary_sensor import GeneracConnectedSensor
from custom_components.generac.binary_sensor import GeneracConnectingSensor
from custom_components.generac.binary_sensor import GeneracMaintenanceAlertSensor
from custom_components.generac.binary_sensor import GeneracWarningSensor
from custom_components.generac.models import Apparatus
from custom_components.generac.models import ApparatusDetail
from custom_components.generac.models import Item


def get_mock_item(
is_connected: bool,
is_connecting: bool,
has_maintenance_alert: bool,
show_warning: bool,
) -> Item:
"""Return a mock Item object."""
return Item(
apparatus=Apparatus(),
apparatusDetail=ApparatusDetail(
isConnected=is_connected,
isConnecting=is_connecting,
hasMaintenanceAlert=has_maintenance_alert,
showWarning=show_warning,
),
)


async def test_connected_sensor(hass):
"""Test the connected sensor."""
coordinator = MagicMock()
entry = MagicMock()

# Test when connected
item = get_mock_item(True, False, False, False)
sensor = GeneracConnectedSensor(coordinator, entry, "12345", item)
assert sensor.is_on is True
assert sensor.name == "generac_12345_is_connected"
assert sensor.device_class == "connectivity"

# Test when not connected
item = get_mock_item(False, False, False, False)
sensor = GeneracConnectedSensor(coordinator, entry, "12345", item)
assert sensor.is_on is False


async def test_connecting_sensor(hass):
"""Test the connecting sensor."""
coordinator = MagicMock()
entry = MagicMock()

# Test when connecting
item = get_mock_item(False, True, False, False)
sensor = GeneracConnectingSensor(coordinator, entry, "12345", item)
assert sensor.is_on is True
assert sensor.name == "generac_12345_is_connecting"
assert sensor.device_class == "connectivity"

# Test when not connecting
item = get_mock_item(False, False, False, False)
sensor = GeneracConnectingSensor(coordinator, entry, "12345", item)
assert sensor.is_on is False


async def test_maintenance_alert_sensor(hass):
"""Test the maintenance alert sensor."""
coordinator = MagicMock()
entry = MagicMock()

# Test when maintenance alert is active
item = get_mock_item(False, False, True, False)
sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item)
assert sensor.is_on is True
assert sensor.name == "generac_12345_has_maintenance_alert"
assert sensor.device_class == "safety"

# Test when maintenance alert is not active
item = get_mock_item(False, False, False, False)
sensor = GeneracMaintenanceAlertSensor(coordinator, entry, "12345", item)
assert sensor.is_on is False


async def test_warning_sensor(hass):
"""Test the warning sensor."""
coordinator = MagicMock()
entry = MagicMock()

# Test when warning is active
item = get_mock_item(False, False, False, True)
sensor = GeneracWarningSensor(coordinator, entry, "12345", item)
assert sensor.is_on is True
assert sensor.name == "generac_12345_show_warning"
assert sensor.device_class == "safety"

# Test when warning is not active
item = get_mock_item(False, False, False, False)
sensor = GeneracWarningSensor(coordinator, entry, "12345", item)
assert sensor.is_on is False
41 changes: 41 additions & 0 deletions tests/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Test the Generac config flow."""
from unittest.mock import patch

from custom_components.generac.const import DOMAIN
from homeassistant import config_entries
from homeassistant import setup
from homeassistant.core import HomeAssistant


async def test_form(hass: HomeAssistant) -> None:
"""Test we get the form."""
await setup.async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
assert result["type"] == "form"
assert result["errors"] == {}

with patch(
"custom_components.generac.config_flow.GeneracApiClient.async_get_data",
return_value=True,
), patch(
"custom_components.generac.async_setup_entry",
return_value=True,
) as mock_setup_entry:
result2 = await hass.config_entries.flow.async_configure(
result["flow_id"],
{
"username": "test-username",
"password": "test-password",
},
)
await hass.async_block_till_done()

assert result2["type"] == "create_entry"
assert result2["title"] == "test-username"
assert result2["data"] == {
"username": "test-username",
"password": "test-password",
}
assert len(mock_setup_entry.mock_calls) == 1
43 changes: 43 additions & 0 deletions tests/test_coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Test the Generac data update coordinator."""
from unittest.mock import AsyncMock
from unittest.mock import MagicMock

import pytest
from custom_components.generac.coordinator import GeneracDataUpdateCoordinator
from homeassistant.helpers.update_coordinator import UpdateFailed


async def test_coordinator_init(hass):
"""Test the coordinator initialization."""
config_entry = MagicMock()
config_entry.options = {}
client = MagicMock()
coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry)
assert coordinator.hass is hass
assert coordinator.api is client
assert coordinator._config_entry is config_entry
assert not coordinator.is_online


async def test_coordinator_update_data(hass):
"""Test the coordinator update data."""
config_entry = MagicMock()
config_entry.options = {}
client = MagicMock()
client.async_get_data = AsyncMock(return_value={"foo": "bar"})
coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry)
coordinator.data = await coordinator._async_update_data()
assert coordinator.data == {"foo": "bar"}
assert coordinator.is_online


async def test_coordinator_update_data_fails(hass):
"""Test the coordinator update data fails."""
config_entry = MagicMock()
config_entry.options = {}
client = MagicMock()
client.async_get_data = AsyncMock(side_effect=Exception)
coordinator = GeneracDataUpdateCoordinator(hass, client, config_entry)
with pytest.raises(UpdateFailed):
await coordinator._async_update_data()
assert not coordinator.is_online
Loading