diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9fe5d356..e8457c48 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -24,11 +24,14 @@ jobs: uses: actions/setup-python@v6 with: python-version: ${{ matrix.python-version }} + - name: Install dependencies run: | python -m pip install --upgrade pip python -m pip install flake8 pytest homeassistant luxtronik==0.3.14 requests>=2.28.2 getmac==0.8.2 if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f tests/requirements-dev.txt ]; then pip install -r tests/requirements-dev.txt; fi + - name: Set PYTHONPATH run: echo "PYTHONPATH=$PYTHONPATH:$(pwd)" >> $GITHUB_ENV - name: Lint with flake8 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..7cd5a2f9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,33 @@ +name: Luxtronik Integration Tests + +on: + workflow_dispatch: + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.11' + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r tests/requirements-dev.txt + + - name: Run tests with coverage + run: | + pytest --cov=custom_components.luxtronik --cov-report=xml --cov-report=term tests/ + + - name: Upload coverage report + uses: actions/upload-artifact@v3 + with: + name: coverage-report + path: coverage.xml + diff --git a/tests/requirements-dev.txt b/tests/requirements-dev.txt new file mode 100644 index 00000000..6a3c93ee --- /dev/null +++ b/tests/requirements-dev.txt @@ -0,0 +1,4 @@ +pytest +pytest-asyncio +pytest-cov +pytest-homeassistant-custom-component \ No newline at end of file diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py new file mode 100644 index 00000000..72176679 --- /dev/null +++ b/tests/test_config_flow.py @@ -0,0 +1,94 @@ +import pytest +pytestmark = pytest.mark.enable_custom_integrations + +from unittest.mock import AsyncMock, MagicMock + +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResult +from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo +from homeassistant.const import CONF_HOST, CONF_PORT + +from custom_components.luxtronik.config_flow import LuxtronikFlowHandler, LuxtronikOptionsFlowHandler +from custom_components.luxtronik.const import DOMAIN, DEFAULT_PORT + + +@pytest.fixture +def mock_hass(): + hass = MagicMock(spec=HomeAssistant) + hass.async_add_executor_job = AsyncMock() + hass.config_entries.async_entries = MagicMock(return_value=[]) + hass.config_entries.async_update_entry = AsyncMock() + hass.config_entries.async_reload = AsyncMock() + return hass + +@pytest.fixture +def flow_handler(mock_hass): + handler = LuxtronikFlowHandler() + handler.hass = mock_hass + return handler + +@pytest.mark.asyncio +async def test_async_step_user_with_available_devices(flow_handler): + flow_handler._async_current_entries = MagicMock(return_value=[]) + flow_handler._discover_devices = AsyncMock(return_value=[("192.168.1.100", 8888)]) + + result = await flow_handler.async_step_user() + + assert result["type"] == "form" + assert result["step_id"] == "select_devices" + +@pytest.mark.asyncio +async def test_async_step_user_with_no_available_devices(flow_handler): + flow_handler._async_current_entries = MagicMock(return_value=[ + MagicMock(data={CONF_HOST: "192.168.1.100", CONF_PORT: 8888}) + ]) + flow_handler._discover_devices = AsyncMock(return_value=[("192.168.1.100", 8888)]) + + result = await flow_handler.async_step_user() + + assert result["type"] == "form" + assert result["step_id"] == "manual_entry" + +@pytest.mark.asyncio +async def test_async_step_select_devices_success(flow_handler): + flow_handler._connect_and_get_coordinator = AsyncMock(return_value=MagicMock(unique_id="123")) + flow_handler._set_unique_id_or_abort = AsyncMock(return_value=True) + + result = await flow_handler.async_step_select_devices({"selected_devices": ["192.168.1.100:8888"]}) + + assert result["type"] == "create_entry" + assert result["title"] == "192.168.1.100:8888" + +@pytest.mark.asyncio +async def test_async_step_manual_entry_success(flow_handler): + flow_handler._connect_and_get_coordinator = AsyncMock(return_value=MagicMock(unique_id="123")) + flow_handler._set_unique_id_or_abort = AsyncMock(return_value=True) + + result = await flow_handler.async_step_manual_entry({CONF_HOST: "192.168.1.100", CONF_PORT: 8888}) + + assert result["type"] == "create_entry" + assert result["title"] == "192.168.1.100:8888" + +@pytest.mark.asyncio +async def test_async_step_dhcp_success(flow_handler): + flow_handler._discover_devices = AsyncMock(return_value=[("192.168.1.100", 8888)]) + flow_handler._connect_and_get_coordinator = AsyncMock(return_value=MagicMock(unique_id="123")) + flow_handler._set_unique_id_or_abort = AsyncMock(return_value=True) + + dhcp_info = MagicMock(ip="192.168.1.100", hostname="luxtronik") + result = await flow_handler.async_step_dhcp(dhcp_info) + + assert result["type"] == "create_entry" + assert result["title"] == "192.168.1.100:8888" + +@pytest.mark.asyncio +async def test_options_flow_update_options(): + config_entry = MagicMock(data={CONF_HOST: "192.168.1.100", CONF_PORT: 8888}, options={}) + handler = LuxtronikOptionsFlowHandler(config_entry) + handler.hass = MagicMock() + handler.hass.config_entries.async_update_entry = AsyncMock() + handler.hass.config_entries.async_reload = AsyncMock() + + result = await handler.async_step_user({}) + + assert result["type"] == "create_entry" diff --git a/tests/test_options_flow.py b/tests/test_options_flow.py new file mode 100644 index 00000000..54b7de6c --- /dev/null +++ b/tests/test_options_flow.py @@ -0,0 +1,65 @@ +import pytest +pytestmark = pytest.mark.enable_custom_integrations + +from homeassistant import data_entry_flow +from homeassistant.core import HomeAssistant +from homeassistant.const import CONF_HOST, CONF_PORT + +from custom_components.luxtronik.config_flow import LuxtronikFlowHandler, LuxtronikOptionsFlowHandler +from custom_components.luxtronik.const import DOMAIN,CONF_HA_SENSOR_INDOOR_TEMPERATURE, DEFAULT_PORT + + +@pytest.fixture +def mock_config_entry(): + class MockConfigEntry: + data = { + CONF_HOST: "192.168.1.100", + CONF_PORT: 8888, + } + options = {} + entry_id = "test_entry" + return MockConfigEntry() + +@pytest.fixture +def mock_connect(monkeypatch): + class MockCoordinator: + manufacturer = "Luxtronik" + model = "HP" + serial_number = "123456789" + monkeypatch.setattr( + "custom_components.luxtronik.coordinator.LuxtronikCoordinator.connect", + lambda hass, config: MockCoordinator() + ) + return MockCoordinator() + +@pytest.mark.asyncio +async def test_options_flow_form_rendering(hass: HomeAssistant, mock_config_entry, mock_connect): + flow = LuxtronikOptionsFlowHandler(mock_config_entry) + flow.hass = hass + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user" + assert "data_schema" in result + +@pytest.mark.asyncio +async def test_options_flow_updates_options(hass: HomeAssistant, mock_config_entry, mock_connect): + flow = LuxtronikOptionsFlowHandler(mock_config_entry) + flow.hass = hass + + user_input = { + CONF_HA_SENSOR_INDOOR_TEMPERATURE: "sensor.indoor_temp" + } + + result = await flow.async_step_user(user_input) + assert result["type"] == data_entry_flow.FlowResultType.CREATE_ENTRY + assert result["data"] == {} + +@pytest.mark.asyncio +async def test_options_flow_init_redirects_to_user(hass: HomeAssistant, mock_config_entry): + flow = LuxtronikOptionsFlowHandler(mock_config_entry) + flow.hass = hass + + result = await flow.async_step_init() + assert result["type"] == data_entry_flow.FlowResultType.FORM + assert result["step_id"] == "user"