3
0

test_config_flow.py 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. """Tests for the config flow and options flow (staged commit semantics)."""
  2. import pytest
  3. from unittest.mock import patch
  4. from homeassistant.config_entries import SOURCE_USER
  5. from homeassistant.data_entry_flow import FlowResultType
  6. from pytest_homeassistant_custom_component.common import MockConfigEntry
  7. from custom_components.zlock_slots.const import (
  8. DOMAIN, CONF_LOCK_ENTITY_IDS, CONF_GROUPS, CONF_GROUP_NAME,
  9. )
  10. from tests.conftest import LOCK_ENTITY_ID, FakeNode
  11. _ZWAVE_HELPER = "homeassistant.components.zwave_js.helpers.async_get_node_from_entity_id"
  12. LOCK_2 = "lock.back_door"
  13. @pytest.fixture(autouse=True)
  14. def mock_zwave_node():
  15. with patch(_ZWAVE_HELPER, return_value=FakeNode()):
  16. yield
  17. # ---------------------------------------------------------------------------
  18. # Config flow
  19. # ---------------------------------------------------------------------------
  20. async def test_user_flow_creates_entry(hass):
  21. result = await hass.config_entries.flow.async_init(
  22. DOMAIN, context={"source": SOURCE_USER}
  23. )
  24. assert result["type"] is FlowResultType.FORM
  25. assert result["step_id"] == "user"
  26. result = await hass.config_entries.flow.async_configure(
  27. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  28. )
  29. await hass.async_block_till_done()
  30. assert result["type"] is FlowResultType.CREATE_ENTRY
  31. assert result["data"] == {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  32. async def test_user_flow_no_locks_shows_error(hass):
  33. result = await hass.config_entries.flow.async_init(
  34. DOMAIN, context={"source": SOURCE_USER}
  35. )
  36. result = await hass.config_entries.flow.async_configure(
  37. result["flow_id"], {CONF_LOCK_ENTITY_IDS: []}
  38. )
  39. assert result["type"] is FlowResultType.FORM
  40. assert result["errors"] == {CONF_LOCK_ENTITY_IDS: "no_locks_selected"}
  41. async def test_second_entry_blocked(hass, config_entry):
  42. """manifest single_config_entry: a second entry cannot be created."""
  43. config_entry.add_to_hass(hass)
  44. result = await hass.config_entries.flow.async_init(
  45. DOMAIN, context={"source": SOURCE_USER}
  46. )
  47. assert result["type"] is FlowResultType.ABORT
  48. assert result["reason"] == "single_instance_allowed"
  49. # ---------------------------------------------------------------------------
  50. # Options flow
  51. # ---------------------------------------------------------------------------
  52. def _entry_with_group() -> MockConfigEntry:
  53. return MockConfigEntry(
  54. domain=DOMAIN,
  55. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
  56. options={
  57. CONF_GROUPS: [{
  58. CONF_GROUP_NAME: "All Locks",
  59. CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2],
  60. }]
  61. },
  62. entry_id="test_entry_options",
  63. )
  64. async def _setup_entry(hass, entry):
  65. entry.add_to_hass(hass)
  66. assert await hass.config_entries.async_setup(entry.entry_id)
  67. await hass.async_block_till_done()
  68. async def _begin_options(hass, entry, step: str):
  69. result = await hass.config_entries.options.async_init(entry.entry_id)
  70. assert result["type"] is FlowResultType.MENU
  71. return await hass.config_entries.options.async_configure(
  72. result["flow_id"], {"next_step_id": step}
  73. )
  74. async def _finish(hass, flow_id):
  75. result = await hass.config_entries.options.async_configure(
  76. flow_id, {"next_step_id": "finish"}
  77. )
  78. await hass.async_block_till_done()
  79. return result
  80. async def test_options_add_group(hass, config_entry):
  81. await _setup_entry(hass, config_entry)
  82. result = await _begin_options(hass, config_entry, "add_group")
  83. result = await hass.config_entries.options.async_configure(
  84. result["flow_id"],
  85. {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  86. )
  87. assert result["type"] is FlowResultType.MENU
  88. result = await _finish(hass, result["flow_id"])
  89. assert result["type"] is FlowResultType.CREATE_ENTRY
  90. assert config_entry.options[CONF_GROUPS] == [
  91. {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  92. ]
  93. async def test_options_duplicate_group_name_rejected(hass):
  94. entry = _entry_with_group()
  95. await _setup_entry(hass, entry)
  96. result = await _begin_options(hass, entry, "add_group")
  97. result = await hass.config_entries.options.async_configure(
  98. result["flow_id"],
  99. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  100. )
  101. assert result["type"] is FlowResultType.FORM
  102. assert result["errors"] == {CONF_GROUP_NAME: "group_name_exists"}
  103. async def test_options_remove_lock_prunes_it_from_groups(hass):
  104. entry = _entry_with_group()
  105. await _setup_entry(hass, entry)
  106. result = await _begin_options(hass, entry, "manage_locks")
  107. result = await hass.config_entries.options.async_configure(
  108. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  109. )
  110. assert result["type"] is FlowResultType.MENU
  111. await _finish(hass, result["flow_id"])
  112. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID]
  113. # Group survives but no longer references the removed lock.
  114. assert entry.options[CONF_GROUPS] == [
  115. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  116. ]
  117. async def test_options_group_dropped_when_all_its_locks_removed(hass):
  118. entry = MockConfigEntry(
  119. domain=DOMAIN,
  120. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
  121. options={
  122. CONF_GROUPS: [{
  123. CONF_GROUP_NAME: "Back only",
  124. CONF_LOCK_ENTITY_IDS: [LOCK_2],
  125. }]
  126. },
  127. entry_id="test_entry_prune",
  128. )
  129. await _setup_entry(hass, entry)
  130. result = await _begin_options(hass, entry, "manage_locks")
  131. result = await hass.config_entries.options.async_configure(
  132. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  133. )
  134. await _finish(hass, result["flow_id"])
  135. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID]
  136. assert entry.options[CONF_GROUPS] == []
  137. async def test_options_abandoned_flow_changes_nothing(hass):
  138. """Lock changes are staged on the flow; without 'finish' the entry is untouched."""
  139. entry = _entry_with_group()
  140. await _setup_entry(hass, entry)
  141. result = await _begin_options(hass, entry, "manage_locks")
  142. result = await hass.config_entries.options.async_configure(
  143. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  144. )
  145. assert result["type"] is FlowResultType.MENU
  146. # No finish step — abandon the flow here.
  147. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID, LOCK_2]
  148. assert entry.options[CONF_GROUPS] == [
  149. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}
  150. ]