diff --git a/custom_components/irm_kmi/repairs.py b/custom_components/irm_kmi/repairs.py index 7a815f3..1f0fc56 100644 --- a/custom_components/irm_kmi/repairs.py +++ b/custom_components/irm_kmi/repairs.py @@ -88,6 +88,6 @@ async def async_create_fix_flow( hass: HomeAssistant, issue_id: str, data: dict[str, str | int | float | None] | None, -) -> RepairsFlow: +) -> OutOfBeneluxRepairFlow: """Create flow.""" return OutOfBeneluxRepairFlow(data) diff --git a/custom_components/irm_kmi/translations/en.json b/custom_components/irm_kmi/translations/en.json index 029ca3f..99d79ac 100644 --- a/custom_components/irm_kmi/translations/en.json +++ b/custom_components/irm_kmi/translations/en.json @@ -14,6 +14,11 @@ "use_deprecated_forecast_attribute": "Use the deprecated forecat attribute" } } + }, + "error": { + "out_of_benelux": "{zone} is out of Benelux. Pick a zone in Benelux.", + "api_error": "Could not get data from the API", + "zone_not_exist": "{zone} does not exist" } }, "selector": { @@ -57,7 +62,7 @@ "fix_flow": { "step": { "confirm": { - "title": "Title for the confirm step", + "title": "Repair: {zone} is outside of Benelux", "description": "This integration can only get data for location in the Benelux. Move the zone or delete this configuration entry." } }, diff --git a/tests/conftest.py b/tests/conftest.py index f18e5be..909aa15 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -85,6 +85,14 @@ def mock_get_forecast_api_error(): return +@pytest.fixture +def mock_get_forecast_api_error_repair(): + """Mock a call to IrmKmiApiClient.get_forecasts_coord() so that it raises an error""" + with patch("custom_components.irm_kmi.repairs.IrmKmiApiClient.get_forecasts_coord", + side_effet=IrmKmiApiError): + return + + @pytest.fixture() def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: """Return a mocked IrmKmi api client.""" @@ -100,7 +108,7 @@ def mock_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMoc @pytest.fixture() -def mock_irm_kmi_api_out_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: +def mock_irm_kmi_api_coordinator_out_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: """Return a mocked IrmKmi api client.""" fixture: str = "forecast_out_of_benelux.json" @@ -113,6 +121,34 @@ def mock_irm_kmi_api_out_benelux(request: pytest.FixtureRequest) -> Generator[No yield irm_kmi +@pytest.fixture() +def mock_irm_kmi_api_repair_in_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked IrmKmi api client.""" + fixture: str = "forecast.json" + + forecast = json.loads(load_fixture(fixture)) + with patch( + "custom_components.irm_kmi.repairs.IrmKmiApiClient", autospec=True + ) as irm_kmi_api_mock: + irm_kmi = irm_kmi_api_mock.return_value + irm_kmi.get_forecasts_coord.return_value = forecast + yield irm_kmi + + +@pytest.fixture() +def mock_irm_kmi_api_repair_out_of_benelux(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: + """Return a mocked IrmKmi api client.""" + fixture: str = "forecast_out_of_benelux.json" + + forecast = json.loads(load_fixture(fixture)) + with patch( + "custom_components.irm_kmi.repairs.IrmKmiApiClient", autospec=True + ) as irm_kmi_api_mock: + irm_kmi = irm_kmi_api_mock.return_value + irm_kmi.get_forecasts_coord.return_value = forecast + yield irm_kmi + + @pytest.fixture() def mock_exception_irm_kmi_api(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]: """Return a mocked IrmKmi api client.""" diff --git a/tests/test_config_flow.py b/tests/test_config_flow.py index ef1790c..56bb67a 100644 --- a/tests/test_config_flow.py +++ b/tests/test_config_flow.py @@ -13,7 +13,7 @@ from custom_components.irm_kmi import async_migrate_entry from custom_components.irm_kmi.const import ( CONF_DARK_MODE, CONF_STYLE, CONF_USE_DEPRECATED_FORECAST, CONFIG_FLOW_VERSION, DOMAIN, OPTION_DEPRECATED_FORECAST_NOT_USED, - OPTION_STYLE_STD) + OPTION_STYLE_SATELLITE, OPTION_STYLE_STD) async def test_full_user_flow( @@ -86,6 +86,53 @@ async def test_config_flow_with_api_error( assert 'base' in result2.get('errors') +async def test_config_flow_unknown_zone(hass: HomeAssistant) -> None: + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_ZONE: "zone.what", + CONF_STYLE: OPTION_STYLE_STD, + CONF_DARK_MODE: False}, + ) + + assert result2.get("type") == FlowResultType.FORM + assert result2.get("step_id") == "user" + assert CONF_ZONE in result2.get('errors') + + +async def test_option_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry +) -> None: + mock_config_entry.add_to_hass(hass) + + assert not mock_config_entry.options + + result = await hass.config_entries.options.async_init(mock_config_entry.entry_id, data=None) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_STYLE: OPTION_STYLE_SATELLITE, + CONF_DARK_MODE: True, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED + } + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["data"] == { + CONF_STYLE: OPTION_STYLE_SATELLITE, + CONF_DARK_MODE: True, + CONF_USE_DEPRECATED_FORECAST: OPTION_DEPRECATED_FORECAST_NOT_USED + } + + async def test_config_entry_migration(hass: HomeAssistant) -> None: """Ensure that config entry migration takes the configuration to the latest version""" entry = MockConfigEntry( diff --git a/tests/test_repairs.py b/tests/test_repairs.py new file mode 100644 index 0000000..504c189 --- /dev/null +++ b/tests/test_repairs.py @@ -0,0 +1,163 @@ +import logging +from unittest.mock import MagicMock + +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType +from homeassistant.helpers import issue_registry +from pytest_homeassistant_custom_component.common import MockConfigEntry + +from custom_components.irm_kmi import DOMAIN, IrmKmiCoordinator +from custom_components.irm_kmi.const import (REPAIR_OPT_DELETE, + REPAIR_OPT_MOVE, REPAIR_SOLUTION) +from custom_components.irm_kmi.repairs import (OutOfBeneluxRepairFlow, + async_create_fix_flow) + +_LOGGER = logging.getLogger(__name__) + + +async def get_repair_flow( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry +) -> OutOfBeneluxRepairFlow: + hass.states.async_set( + "zone.home", + 0, + {"latitude": 50.738681639, "longitude": 4.054077148}, + ) + mock_config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + coordinator = IrmKmiCoordinator(hass, mock_config_entry) + await coordinator._async_update_data() + ir = issue_registry.async_get(hass) + issue = ir.async_get_issue(DOMAIN, "zone_moved") + repair_flow = await async_create_fix_flow(hass, issue.issue_id, issue.data) + repair_flow.hass = hass + return repair_flow + + +async def test_repair_triggers_when_out_of_benelux( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + hass.states.async_set( + "zone.home", + 0, + {"latitude": 50.738681639, "longitude": 4.054077148}, + ) + + mock_config_entry.add_to_hass(hass) + + coordinator = IrmKmiCoordinator(hass, mock_config_entry) + await coordinator._async_update_data() + + ir = issue_registry.async_get(hass) + + issue = ir.async_get_issue(DOMAIN, "zone_moved") + + assert issue is not None + assert issue.data == {'config_entry_id': mock_config_entry.entry_id, 'zone': "zone.home"} + assert issue.translation_key == "zone_moved" + assert issue.is_fixable + assert issue.translation_placeholders == {'zone': "zone.home"} + + +async def test_repair_flow( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_irm_kmi_api_repair_in_benelux: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + repair_flow = await get_repair_flow(hass, mock_config_entry) + result = await repair_flow.async_step_init() + + assert result['type'] == FlowResultType.FORM + assert result['errors'] == {} + assert result['description_placeholders'] == {"zone": "zone.home"} + + user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE} + + result = await repair_flow.async_step_confirm(user_input) + + assert result['type'] == FlowResultType.CREATE_ENTRY + assert result['title'] == "" + assert result['data'] == {} + + +async def test_repair_flow_invalid_choice( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_irm_kmi_api_repair_in_benelux: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + repair_flow = await get_repair_flow(hass, mock_config_entry) + result = await repair_flow.async_step_init() + + assert result['type'] == FlowResultType.FORM + user_input = {REPAIR_SOLUTION: "whut?"} + + result = await repair_flow.async_step_confirm(user_input) + + assert result['type'] == FlowResultType.FORM + assert REPAIR_SOLUTION in result['errors'] + assert result['errors'][REPAIR_SOLUTION] == 'invalid_choice' + + +async def test_repair_flow_api_error( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_get_forecast_api_error_repair: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + repair_flow = await get_repair_flow(hass, mock_config_entry) + result = await repair_flow.async_step_init() + + assert result['type'] == FlowResultType.FORM + user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE} + + result = await repair_flow.async_step_confirm(user_input) + + assert result['type'] == FlowResultType.FORM + assert REPAIR_SOLUTION in result['errors'] + assert result['errors'][REPAIR_SOLUTION] == 'api_error' + + +async def test_repair_flow_out_of_benelux( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_irm_kmi_api_repair_out_of_benelux: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + repair_flow = await get_repair_flow(hass, mock_config_entry) + result = await repair_flow.async_step_init() + + assert result['type'] == FlowResultType.FORM + user_input = {REPAIR_SOLUTION: REPAIR_OPT_MOVE} + + result = await repair_flow.async_step_confirm(user_input) + + assert result['type'] == FlowResultType.FORM + assert REPAIR_SOLUTION in result['errors'] + assert result['errors'][REPAIR_SOLUTION] == 'out_of_benelux' + + +async def test_repair_flow_delete_entry( + hass: HomeAssistant, + mock_irm_kmi_api_coordinator_out_benelux: MagicMock, + mock_config_entry: MockConfigEntry +) -> None: + repair_flow = await get_repair_flow(hass, mock_config_entry) + result = await repair_flow.async_step_init() + + assert result['type'] == FlowResultType.FORM + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert hass.config_entries.async_entries(DOMAIN)[0].entry_id == mock_config_entry.entry_id + + user_input = {REPAIR_SOLUTION: REPAIR_OPT_DELETE} + result = await repair_flow.async_step_confirm(user_input) + + assert result['type'] == FlowResultType.CREATE_ENTRY + assert result['title'] == "" + assert result['data'] == {} + assert len(hass.config_entries.async_entries(DOMAIN)) == 0