"""Tests for the config flow and options flow (staged commit semantics).""" import pytest from unittest.mock import patch from homeassistant.config_entries import SOURCE_USER from homeassistant.data_entry_flow import FlowResultType from pytest_homeassistant_custom_component.common import MockConfigEntry from custom_components.zlock_slots.const import ( DOMAIN, CONF_LOCK_ENTITY_IDS, CONF_GROUPS, CONF_GROUP_NAME, ) from tests.conftest import LOCK_ENTITY_ID, FakeNode _ZWAVE_HELPER = "homeassistant.components.zwave_js.helpers.async_get_node_from_entity_id" LOCK_2 = "lock.back_door" @pytest.fixture(autouse=True) def mock_zwave_node(): with patch(_ZWAVE_HELPER, return_value=FakeNode()): yield # --------------------------------------------------------------------------- # Config flow # --------------------------------------------------------------------------- async def test_user_flow_creates_entry(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY assert result["data"] == {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} async def test_user_flow_no_locks_shows_error(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: []} ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_LOCK_ENTITY_IDS: "no_locks_selected"} async def test_second_entry_blocked(hass, config_entry): """manifest single_config_entry: a second entry cannot be created.""" config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "single_instance_allowed" # --------------------------------------------------------------------------- # Options flow # --------------------------------------------------------------------------- def _entry_with_group() -> MockConfigEntry: return MockConfigEntry( domain=DOMAIN, data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, options={ CONF_GROUPS: [{ CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2], }] }, entry_id="test_entry_options", ) async def _setup_entry(hass, entry): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() async def _begin_options(hass, entry, step: str): result = await hass.config_entries.options.async_init(entry.entry_id) assert result["type"] is FlowResultType.MENU return await hass.config_entries.options.async_configure( result["flow_id"], {"next_step_id": step} ) async def _finish(hass, flow_id): result = await hass.config_entries.options.async_configure( flow_id, {"next_step_id": "finish"} ) await hass.async_block_till_done() return result async def test_options_add_group(hass, config_entry): await _setup_entry(hass, config_entry) result = await _begin_options(hass, config_entry, "add_group") result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}, ) assert result["type"] is FlowResultType.MENU result = await _finish(hass, result["flow_id"]) assert result["type"] is FlowResultType.CREATE_ENTRY assert config_entry.options[CONF_GROUPS] == [ {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ] async def test_options_duplicate_group_name_rejected(hass): entry = _entry_with_group() await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "add_group") result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}, ) assert result["type"] is FlowResultType.FORM assert result["errors"] == {CONF_GROUP_NAME: "group_name_exists"} async def test_options_remove_lock_prunes_it_from_groups(hass): entry = _entry_with_group() await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "manage_locks") result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ) assert result["type"] is FlowResultType.MENU await _finish(hass, result["flow_id"]) assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID] # Group survives but no longer references the removed lock. assert entry.options[CONF_GROUPS] == [ {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ] async def test_options_group_dropped_when_all_its_locks_removed(hass): entry = MockConfigEntry( domain=DOMAIN, data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, options={ CONF_GROUPS: [{ CONF_GROUP_NAME: "Back only", CONF_LOCK_ENTITY_IDS: [LOCK_2], }] }, entry_id="test_entry_prune", ) await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "manage_locks") result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ) await _finish(hass, result["flow_id"]) assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID] assert entry.options[CONF_GROUPS] == [] async def test_options_edit_group_single_group_skips_selection(hass): entry = _entry_with_group() await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "edit_group") assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_group_locks" result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ) assert result["type"] is FlowResultType.MENU result = await _finish(hass, result["flow_id"]) assert result["type"] is FlowResultType.CREATE_ENTRY assert entry.options[CONF_GROUPS] == [ {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ] async def test_options_edit_group_multiple_groups_prompts_for_selection(hass): entry = MockConfigEntry( domain=DOMAIN, data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, options={ CONF_GROUPS: [ {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, {CONF_GROUP_NAME: "Front only", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}, ] }, entry_id="test_entry_edit_group", ) await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "edit_group") assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_group" result = await hass.config_entries.options.async_configure( result["flow_id"], {"group": "Front only"} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "edit_group_locks" result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]} ) result = await _finish(hass, result["flow_id"]) assert entry.options[CONF_GROUPS] == [ {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, {CONF_GROUP_NAME: "Front only", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}, ] async def test_options_edit_group_no_groups_returns_to_menu(hass, config_entry): await _setup_entry(hass, config_entry) result = await _begin_options(hass, config_entry, "edit_group") assert result["type"] is FlowResultType.MENU async def test_options_abandoned_flow_changes_nothing(hass): """Lock changes are staged on the flow; without 'finish' the entry is untouched.""" entry = _entry_with_group() await _setup_entry(hass, entry) result = await _begin_options(hass, entry, "manage_locks") result = await hass.config_entries.options.async_configure( result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]} ) assert result["type"] is FlowResultType.MENU # No finish step — abandon the flow here. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID, LOCK_2] assert entry.options[CONF_GROUPS] == [ {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]} ]