test_config_flow.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
  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. @pytest.fixture(autouse=True)
  18. def bypass_manifest_dependencies():
  19. """zwave_js/frontend/lovelace can't be set up in the test harness."""
  20. with patch(
  21. "homeassistant.setup._async_process_dependencies", return_value=[]
  22. ):
  23. yield
  24. # ---------------------------------------------------------------------------
  25. # Config flow
  26. # ---------------------------------------------------------------------------
  27. async def test_user_flow_creates_entry(hass):
  28. result = await hass.config_entries.flow.async_init(
  29. DOMAIN, context={"source": SOURCE_USER}
  30. )
  31. assert result["type"] is FlowResultType.FORM
  32. assert result["step_id"] == "user"
  33. result = await hass.config_entries.flow.async_configure(
  34. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  35. )
  36. await hass.async_block_till_done()
  37. assert result["type"] is FlowResultType.CREATE_ENTRY
  38. assert result["data"] == {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  39. async def test_user_flow_no_locks_shows_error(hass):
  40. result = await hass.config_entries.flow.async_init(
  41. DOMAIN, context={"source": SOURCE_USER}
  42. )
  43. result = await hass.config_entries.flow.async_configure(
  44. result["flow_id"], {CONF_LOCK_ENTITY_IDS: []}
  45. )
  46. assert result["type"] is FlowResultType.FORM
  47. assert result["errors"] == {CONF_LOCK_ENTITY_IDS: "no_locks_selected"}
  48. async def test_second_entry_blocked(hass, config_entry):
  49. """manifest single_config_entry: a second entry cannot be created."""
  50. config_entry.add_to_hass(hass)
  51. result = await hass.config_entries.flow.async_init(
  52. DOMAIN, context={"source": SOURCE_USER}
  53. )
  54. assert result["type"] is FlowResultType.ABORT
  55. assert result["reason"] == "single_instance_allowed"
  56. # ---------------------------------------------------------------------------
  57. # Options flow
  58. # ---------------------------------------------------------------------------
  59. def _entry_with_group() -> MockConfigEntry:
  60. return MockConfigEntry(
  61. domain=DOMAIN,
  62. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
  63. options={
  64. CONF_GROUPS: [{
  65. CONF_GROUP_NAME: "All Locks",
  66. CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2],
  67. }]
  68. },
  69. entry_id="test_entry_options",
  70. )
  71. async def _setup_entry(hass, entry):
  72. entry.add_to_hass(hass)
  73. assert await hass.config_entries.async_setup(entry.entry_id)
  74. await hass.async_block_till_done()
  75. async def _begin_options(hass, entry, step: str):
  76. result = await hass.config_entries.options.async_init(entry.entry_id)
  77. assert result["type"] is FlowResultType.MENU
  78. return await hass.config_entries.options.async_configure(
  79. result["flow_id"], {"next_step_id": step}
  80. )
  81. async def _finish(hass, flow_id):
  82. result = await hass.config_entries.options.async_configure(
  83. flow_id, {"next_step_id": "finish"}
  84. )
  85. await hass.async_block_till_done()
  86. return result
  87. async def test_options_add_group(hass, config_entry):
  88. await _setup_entry(hass, config_entry)
  89. result = await _begin_options(hass, config_entry, "add_group")
  90. result = await hass.config_entries.options.async_configure(
  91. result["flow_id"],
  92. {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  93. )
  94. assert result["type"] is FlowResultType.MENU
  95. result = await _finish(hass, result["flow_id"])
  96. assert result["type"] is FlowResultType.CREATE_ENTRY
  97. assert config_entry.options[CONF_GROUPS] == [
  98. {CONF_GROUP_NAME: "Front", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  99. ]
  100. async def test_options_duplicate_group_name_rejected(hass):
  101. entry = _entry_with_group()
  102. await _setup_entry(hass, entry)
  103. result = await _begin_options(hass, entry, "add_group")
  104. result = await hass.config_entries.options.async_configure(
  105. result["flow_id"],
  106. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]},
  107. )
  108. assert result["type"] is FlowResultType.FORM
  109. assert result["errors"] == {CONF_GROUP_NAME: "group_name_exists"}
  110. async def test_options_remove_lock_prunes_it_from_groups(hass):
  111. entry = _entry_with_group()
  112. await _setup_entry(hass, entry)
  113. result = await _begin_options(hass, entry, "manage_locks")
  114. result = await hass.config_entries.options.async_configure(
  115. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  116. )
  117. assert result["type"] is FlowResultType.MENU
  118. await _finish(hass, result["flow_id"])
  119. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID]
  120. # Group survives but no longer references the removed lock.
  121. assert entry.options[CONF_GROUPS] == [
  122. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  123. ]
  124. async def test_options_group_dropped_when_all_its_locks_removed(hass):
  125. entry = MockConfigEntry(
  126. domain=DOMAIN,
  127. data={CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]},
  128. options={
  129. CONF_GROUPS: [{
  130. CONF_GROUP_NAME: "Back only",
  131. CONF_LOCK_ENTITY_IDS: [LOCK_2],
  132. }]
  133. },
  134. entry_id="test_entry_prune",
  135. )
  136. await _setup_entry(hass, entry)
  137. result = await _begin_options(hass, entry, "manage_locks")
  138. result = await hass.config_entries.options.async_configure(
  139. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  140. )
  141. await _finish(hass, result["flow_id"])
  142. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID]
  143. assert entry.options[CONF_GROUPS] == []
  144. async def test_options_abandoned_flow_changes_nothing(hass):
  145. """Lock changes are staged on the flow; without 'finish' the entry is untouched."""
  146. entry = _entry_with_group()
  147. await _setup_entry(hass, entry)
  148. result = await _begin_options(hass, entry, "manage_locks")
  149. result = await hass.config_entries.options.async_configure(
  150. result["flow_id"], {CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID]}
  151. )
  152. assert result["type"] is FlowResultType.MENU
  153. # No finish step — abandon the flow here.
  154. assert entry.data[CONF_LOCK_ENTITY_IDS] == [LOCK_ENTITY_ID, LOCK_2]
  155. assert entry.options[CONF_GROUPS] == [
  156. {CONF_GROUP_NAME: "All Locks", CONF_LOCK_ENTITY_IDS: [LOCK_ENTITY_ID, LOCK_2]}
  157. ]