forked from mac-zhou/midea-ac-py
-
-
Notifications
You must be signed in to change notification settings - Fork 20
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add devcontainer, test requirements file and basic pytest fixtures * Add pytest options file to set asyncio mode * Add some minimal config flow tests * Add new test that fails due to refresh and apply at the same time * Add unittests to GH actions
- Loading branch information
Showing
8 changed files
with
290 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"image": "mcr.microsoft.com/devcontainers/python:3.13", | ||
"features": { | ||
"ghcr.io/devcontainers/features/git:1": {} | ||
}, | ||
"postCreateCommand": "pip install autopep8 isort; pip install -r tests/requirements.txt" | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
[pytest] | ||
asyncio_mode = auto | ||
asyncio_default_fixture_loop_scope = function |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
"""Tests for the Midea Smart AC integration.""" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
"""Pytest fixtures for testing Midea Smart AC.""" | ||
import pytest | ||
|
||
|
||
@pytest.fixture(autouse=True) | ||
def auto_enable_custom_integrations(enable_custom_integrations): | ||
yield |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
pytest | ||
pytest-homeassistant-custom-component | ||
|
||
msmart-ng |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,168 @@ | ||
"""Tests for the config flow.""" | ||
|
||
import logging | ||
from unittest.mock import patch | ||
|
||
import pytest | ||
from homeassistant import config_entries | ||
from homeassistant.config_entries import ConfigEntryState | ||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.data_entry_flow import FlowResultType, InvalidData | ||
from msmart.lan import AuthenticationError | ||
from pytest_homeassistant_custom_component.common import MockConfigEntry | ||
|
||
from custom_components.midea_ac.const import CONF_BEEP, CONF_KEY, DOMAIN | ||
|
||
logging.basicConfig(level=logging.DEBUG) | ||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async def test_config_flow_options(hass: HomeAssistant) -> None: | ||
"""Test the config flow starts with a menu with manual and discover options.""" | ||
# Check initial flow is a menu with two options | ||
result = await hass.config_entries.flow.async_init( | ||
DOMAIN, context={"source": config_entries.SOURCE_USER} | ||
) | ||
|
||
assert result["step_id"] == "user" | ||
assert result["type"] is FlowResultType.MENU | ||
assert result["menu_options"] == ["discover", "manual"] | ||
|
||
# Check discover flow can be started | ||
discover_form_result = await hass.config_entries.flow.async_init( | ||
DOMAIN, context={"source": "discover"} | ||
) | ||
assert discover_form_result["type"] is FlowResultType.FORM | ||
assert discover_form_result["step_id"] == "discover" | ||
assert not discover_form_result["errors"] | ||
|
||
# Check manual flow can be started | ||
manual_form_result = await hass.config_entries.flow.async_init( | ||
DOMAIN, context={"source": "manual"} | ||
) | ||
assert manual_form_result["type"] is FlowResultType.FORM | ||
assert manual_form_result["step_id"] == "manual" | ||
assert not manual_form_result["errors"] | ||
|
||
|
||
async def test_manual_flow(hass: HomeAssistant) -> None: | ||
"""Test the manual flow validates input and failed connections return errors.""" | ||
result = await hass.config_entries.flow.async_init( | ||
DOMAIN, context={"source": "manual"} | ||
) | ||
assert result | ||
|
||
invalid_input = [ | ||
{ | ||
CONF_HOST: None | ||
}, | ||
{ | ||
CONF_HOST: "localhost", | ||
CONF_PORT: None | ||
}, | ||
{ | ||
CONF_HOST: "localhost", | ||
CONF_PORT: 6444, | ||
CONF_ID: None | ||
} | ||
] | ||
for input in invalid_input: | ||
with pytest.raises(InvalidData): | ||
result = await hass.config_entries.flow.async_configure( | ||
result["flow_id"], | ||
user_input=input | ||
) | ||
|
||
with (patch("custom_components.midea_ac.config_flow.AC.refresh", | ||
return_value=False) as refresh_mock, | ||
patch("custom_components.midea_ac.config_flow.AC.authenticate", | ||
side_effect=AuthenticationError) as authenticate_mock): | ||
# Check manually configuring a V2 device | ||
result = await hass.config_entries.flow.async_configure( | ||
result["flow_id"], | ||
user_input={ | ||
CONF_HOST: "localhost", | ||
CONF_PORT: 6444, | ||
CONF_ID: "1234" | ||
} | ||
) | ||
assert result | ||
# Refresh should be called | ||
refresh_mock.assert_awaited_once() | ||
# Authenticate shouldn't be called | ||
authenticate_mock.assert_not_awaited() | ||
# Connection should fail | ||
assert result["errors"] == {"base": "cannot_connect"} | ||
|
||
refresh_mock.reset_mock() | ||
authenticate_mock.reset_mock() | ||
|
||
# Check manually configuring a V3 device | ||
result = await hass.config_entries.flow.async_configure( | ||
result["flow_id"], | ||
user_input={ | ||
CONF_HOST: "localhost", | ||
CONF_PORT: 6444, | ||
CONF_ID: "1234", | ||
CONF_TOKEN: "1234", | ||
CONF_KEY: "1234" | ||
|
||
} | ||
) | ||
assert result | ||
# Authenticate should be called | ||
authenticate_mock.assert_awaited_once() | ||
# Refresh should be not called | ||
refresh_mock.assert_not_awaited() | ||
# Connection should fail | ||
assert result["errors"] == {"base": "cannot_connect"} | ||
|
||
|
||
async def test_options_flow_init(hass: HomeAssistant) -> None: | ||
"""Test the integration options flow works and default options are set.""" | ||
|
||
# Create a mock config entry | ||
mock_config_entry = MockConfigEntry( | ||
domain=DOMAIN, | ||
data={ | ||
CONF_ID: "1234", | ||
CONF_HOST: "localhost", | ||
CONF_PORT: 6444, | ||
CONF_TOKEN: None, | ||
CONF_KEY: None, | ||
} | ||
) | ||
|
||
# Patch refresh and get_capabilities calls to allow integration to setup | ||
with (patch("custom_components.midea_ac.config_flow.AC.get_capabilities"), | ||
patch("custom_components.midea_ac.config_flow.AC.refresh")): | ||
# Add mock config entry to HASS and setup integration | ||
mock_config_entry.add_to_hass(hass) | ||
await hass.config_entries.async_setup(mock_config_entry.entry_id) | ||
await hass.async_block_till_done() | ||
|
||
assert mock_config_entry.entry_id in hass.data[DOMAIN] | ||
assert mock_config_entry.state is ConfigEntryState.LOADED | ||
|
||
# Show options form | ||
result = await hass.config_entries.options.async_init(mock_config_entry.entry_id) | ||
assert result["step_id"] == "init" | ||
assert result["type"] is FlowResultType.FORM | ||
assert not result["errors"] | ||
|
||
with patch("custom_components.midea_ac.async_setup_entry", | ||
return_value=True) as mock_setup_entry: | ||
result = await hass.config_entries.options.async_configure( | ||
result["flow_id"], | ||
user_input={ | ||
CONF_BEEP: False | ||
}, | ||
) | ||
await hass.async_block_till_done() | ||
|
||
assert result["type"] is FlowResultType.CREATE_ENTRY | ||
assert mock_config_entry.options == { | ||
CONF_BEEP: False, | ||
} | ||
assert len(mock_setup_entry.mock_calls) == 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
"""Tests for the data update coordinator flow.""" | ||
|
||
import asyncio | ||
import logging | ||
from unittest.mock import AsyncMock, MagicMock, patch | ||
|
||
import pytest | ||
from homeassistant.config_entries import ConfigEntryState | ||
from homeassistant.const import CONF_HOST, CONF_ID, CONF_PORT, CONF_TOKEN | ||
from homeassistant.core import HomeAssistant | ||
from msmart.lan import _LanProtocol | ||
from pytest_homeassistant_custom_component.common import MockConfigEntry | ||
|
||
from custom_components.midea_ac.const import CONF_KEY, DOMAIN | ||
|
||
|
||
async def _setup_integration(hass: HomeAssistant) -> MockConfigEntry: | ||
"""Set up the integration with a mock config entry.""" | ||
|
||
# Create a mock config entry | ||
mock_config_entry = MockConfigEntry( | ||
domain=DOMAIN, | ||
data={ | ||
CONF_ID: "1234", | ||
CONF_HOST: "localhost", | ||
CONF_PORT: 6444, | ||
CONF_TOKEN: None, | ||
CONF_KEY: None, | ||
} | ||
) | ||
|
||
# Patch refresh and get_capabilities calls to allow integration to setup | ||
with (patch("custom_components.midea_ac.config_flow.AC.get_capabilities"), | ||
patch("custom_components.midea_ac.config_flow.AC.refresh")): | ||
# Add mock config entry to HASS and setup integration | ||
mock_config_entry.add_to_hass(hass) | ||
await hass.config_entries.async_setup(mock_config_entry.entry_id) | ||
await hass.async_block_till_done() | ||
|
||
assert mock_config_entry.entry_id in hass.data[DOMAIN] | ||
assert mock_config_entry.state is ConfigEntryState.LOADED | ||
|
||
return mock_config_entry | ||
|
||
|
||
async def test_concurrent_refresh_exception( | ||
hass: HomeAssistant, | ||
) -> None: | ||
"""Test concurrent refreshes can cause an exception.""" | ||
|
||
# Setup the integration | ||
mock_config_entry = await _setup_integration(hass) | ||
assert mock_config_entry | ||
|
||
# Fetch the coordinator | ||
coordinator = hass.data[DOMAIN][mock_config_entry.entry_id] | ||
device = coordinator.device | ||
lan = device._lan | ||
|
||
# Mock the read_available method so send() will be reached | ||
lan._read_available = MagicMock() | ||
lan._read_available.__aiter__.return_value = None | ||
|
||
# Mock connect and protocol objects so network won't be used | ||
lan._connect = AsyncMock() | ||
lan._protocol = _LanProtocol() | ||
lan._protocol._peer = "127.0.0.1:6444" | ||
|
||
# Mock the transport so connection wil be seen as alive | ||
lan._protocol._transport = MagicMock() | ||
lan._protocol._transport.is_closing = MagicMock(return_value=False) | ||
|
||
# logging.getLogger("msmart").setLevel(logging.DEBUG) | ||
# logging.getLogger("custom_components.midea_ac").setLevel(logging.DEBUG) | ||
|
||
# Check that concurrent calls to network actions can throw | ||
with pytest.raises(AttributeError): | ||
task1 = asyncio.create_task(coordinator.async_request_refresh()) | ||
await asyncio.sleep(3) | ||
task2 = asyncio.create_task(coordinator.apply()) | ||
await asyncio.gather(task1, task2) |