test_config_flow.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. """Tests for the config flow."""
  2. from unittest.mock import ANY, AsyncMock, MagicMock, patch
  3. from homeassistant.const import CONF_HOST, CONF_NAME
  4. import pytest
  5. from pytest_homeassistant_custom_component.common import MockConfigEntry
  6. import voluptuous as vol
  7. from custom_components.tuya_local import config_flow
  8. from custom_components.tuya_local.const import (
  9. CONF_CLIMATE,
  10. CONF_DEVICE_ID,
  11. CONF_LOCAL_KEY,
  12. CONF_LOCK,
  13. CONF_SWITCH,
  14. CONF_TYPE,
  15. DOMAIN,
  16. )
  17. @pytest.fixture(autouse=True)
  18. def auto_enable_custom_integrations(enable_custom_integrations):
  19. yield
  20. @pytest.fixture
  21. def bypass_setup():
  22. """Prevent actual setup of the integration after config flow."""
  23. with patch(
  24. "custom_components.tuya_local.async_setup_entry",
  25. return_value=True,
  26. ):
  27. yield
  28. async def test_init_entry(hass):
  29. """Test initialisation of the config flow."""
  30. entry = MockConfigEntry(
  31. domain=DOMAIN,
  32. version=2,
  33. title="test",
  34. data={
  35. CONF_DEVICE_ID: "deviceid",
  36. CONF_HOST: "hostname",
  37. CONF_LOCAL_KEY: "localkey",
  38. CONF_TYPE: "kogan_heater",
  39. CONF_CLIMATE: True,
  40. CONF_LOCK: True,
  41. },
  42. )
  43. entry.add_to_hass(hass)
  44. await hass.config_entries.async_setup(entry.entry_id)
  45. await hass.async_block_till_done()
  46. state = hass.states.get("climate.test")
  47. assert state
  48. async def test_flow_user_init(hass):
  49. """Test the initialisation of the form in the first step of the config flow."""
  50. result = await hass.config_entries.flow.async_init(
  51. DOMAIN, context={"source": "user"}
  52. )
  53. expected = {
  54. "data_schema": vol.Schema(config_flow.individual_config_schema()),
  55. "description_placeholders": None,
  56. "errors": {},
  57. "flow_id": ANY,
  58. "handler": DOMAIN,
  59. "step_id": "user",
  60. "type": "form",
  61. "last_step": ANY,
  62. }
  63. assert expected == result
  64. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  65. async def test_async_test_connection_valid(mock_device, hass):
  66. """Test that device is returned when connection is valid."""
  67. mock_instance = AsyncMock()
  68. mock_instance.has_returned_state = True
  69. mock_device.return_value = mock_instance
  70. device = await config_flow.async_test_connection(
  71. {
  72. CONF_DEVICE_ID: "deviceid",
  73. CONF_LOCAL_KEY: "localkey",
  74. CONF_HOST: "hostname",
  75. },
  76. hass,
  77. )
  78. assert device == mock_instance
  79. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  80. async def test_async_test_connection_invalid(mock_device, hass):
  81. """Test that None is returned when connection is invalid."""
  82. mock_instance = AsyncMock()
  83. mock_instance.has_returned_state = False
  84. mock_device.return_value = mock_instance
  85. device = await config_flow.async_test_connection(
  86. {
  87. CONF_DEVICE_ID: "deviceid",
  88. CONF_LOCAL_KEY: "localkey",
  89. CONF_HOST: "hostname",
  90. },
  91. hass,
  92. )
  93. assert device is None
  94. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  95. async def test_flow_user_init_invalid_config(mock_test, hass):
  96. """Test errors populated when config is invalid."""
  97. mock_test.return_value = None
  98. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  99. result = await hass.config_entries.flow.async_configure(
  100. flow["flow_id"],
  101. user_input={
  102. CONF_DEVICE_ID: "deviceid",
  103. CONF_HOST: "hostname",
  104. CONF_LOCAL_KEY: "badkey",
  105. },
  106. )
  107. assert {"base": "connection"} == result["errors"]
  108. def setup_device_mock(mock, failure=False, type="test"):
  109. mock_type = MagicMock()
  110. mock_type.legacy_type = type
  111. mock_iter = MagicMock()
  112. mock_iter.__aiter__.return_value = [mock_type] if not failure else []
  113. mock.async_possible_types = MagicMock(return_value=mock_iter)
  114. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  115. async def test_flow_user_init_data_valid(mock_test, hass):
  116. """Test we advance to the next step when connection config is valid."""
  117. mock_device = MagicMock()
  118. setup_device_mock(mock_device)
  119. mock_test.return_value = mock_device
  120. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  121. result = await hass.config_entries.flow.async_configure(
  122. flow["flow_id"],
  123. user_input={
  124. CONF_DEVICE_ID: "deviceid",
  125. CONF_HOST: "hostname",
  126. CONF_LOCAL_KEY: "localkey",
  127. },
  128. )
  129. assert "form" == result["type"]
  130. assert "select_type" == result["step_id"]
  131. @patch.object(config_flow.ConfigFlowHandler, "device")
  132. async def test_flow_select_type_init(mock_device, hass):
  133. """Test the initialisation of the form in the 2nd step of the config flow."""
  134. setup_device_mock(mock_device)
  135. result = await hass.config_entries.flow.async_init(
  136. DOMAIN, context={"source": "select_type"}
  137. )
  138. expected = {
  139. "data_schema": ANY,
  140. "description_placeholders": None,
  141. "errors": None,
  142. "flow_id": ANY,
  143. "handler": DOMAIN,
  144. "step_id": "select_type",
  145. "type": "form",
  146. "last_step": ANY,
  147. }
  148. assert expected == result
  149. # Check the schema. Simple comparison does not work since they are not
  150. # the same object
  151. try:
  152. result["data_schema"]({CONF_TYPE: "test"})
  153. except vol.MultipleInvalid:
  154. assert False
  155. try:
  156. result["data_schema"]({CONF_TYPE: "not_test"})
  157. assert False
  158. except vol.MultipleInvalid:
  159. pass
  160. @patch.object(config_flow.ConfigFlowHandler, "device")
  161. async def test_flow_select_type_aborts_when_no_match(mock_device, hass):
  162. """Test the flow aborts when an unsupported device is used."""
  163. setup_device_mock(mock_device, failure=True)
  164. result = await hass.config_entries.flow.async_init(
  165. DOMAIN, context={"source": "select_type"}
  166. )
  167. assert result["type"] == "abort"
  168. assert result["reason"] == "not_supported"
  169. @patch.object(config_flow.ConfigFlowHandler, "device")
  170. async def test_flow_select_type_data_valid(mock_device, hass):
  171. """Test the flow continues when valid data is supplied."""
  172. setup_device_mock(mock_device, type="kogan_switch")
  173. flow = await hass.config_entries.flow.async_init(
  174. DOMAIN, context={"source": "select_type"}
  175. )
  176. result = await hass.config_entries.flow.async_configure(
  177. flow["flow_id"],
  178. user_input={CONF_TYPE: "kogan_switch"},
  179. )
  180. assert "form" == result["type"]
  181. assert "choose_entities" == result["step_id"]
  182. async def test_flow_choose_entities_init(hass):
  183. """Test the initialisation of the form in the 3rd step of the config flow."""
  184. with patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "kogan_switch"}):
  185. result = await hass.config_entries.flow.async_init(
  186. DOMAIN, context={"source": "choose_entities"}
  187. )
  188. expected = {
  189. "data_schema": ANY,
  190. "description_placeholders": None,
  191. "errors": None,
  192. "flow_id": ANY,
  193. "handler": DOMAIN,
  194. "step_id": "choose_entities",
  195. "type": "form",
  196. "last_step": ANY,
  197. }
  198. assert expected == result
  199. # Check the schema. Simple comparison does not work since they are not
  200. # the same object
  201. try:
  202. result["data_schema"]({CONF_NAME: "test", CONF_SWITCH: True})
  203. except vol.MultipleInvalid:
  204. assert False
  205. try:
  206. result["data_schema"]({CONF_CLIMATE: True})
  207. assert False
  208. except vol.MultipleInvalid:
  209. pass
  210. async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
  211. """Test the flow ends when data is valid."""
  212. with patch.dict(
  213. config_flow.ConfigFlowHandler.data,
  214. {
  215. CONF_DEVICE_ID: "deviceid",
  216. CONF_LOCAL_KEY: "localkey",
  217. CONF_HOST: "hostname",
  218. CONF_TYPE: "kogan_heater",
  219. },
  220. ):
  221. flow = await hass.config_entries.flow.async_init(
  222. DOMAIN, context={"source": "choose_entities"}
  223. )
  224. result = await hass.config_entries.flow.async_configure(
  225. flow["flow_id"],
  226. user_input={CONF_NAME: "test", CONF_CLIMATE: True, CONF_LOCK: False},
  227. )
  228. expected = {
  229. "version": 2,
  230. "type": "create_entry",
  231. "flow_id": ANY,
  232. "handler": DOMAIN,
  233. "title": "test",
  234. "description": None,
  235. "description_placeholders": None,
  236. "result": ANY,
  237. "options": {},
  238. "data": {
  239. CONF_CLIMATE: True,
  240. CONF_DEVICE_ID: "deviceid",
  241. CONF_HOST: "hostname",
  242. CONF_LOCAL_KEY: "localkey",
  243. CONF_LOCK: False,
  244. CONF_TYPE: "kogan_heater",
  245. },
  246. }
  247. assert expected == result
  248. async def test_options_flow_init(hass):
  249. """Test config flow options."""
  250. config_entry = MockConfigEntry(
  251. domain=DOMAIN,
  252. unique_id="uniqueid",
  253. data={
  254. CONF_DEVICE_ID: "deviceid",
  255. CONF_HOST: "hostname",
  256. CONF_LOCAL_KEY: "localkey",
  257. CONF_NAME: "test",
  258. CONF_SWITCH: True,
  259. CONF_TYPE: "kogan_switch",
  260. },
  261. )
  262. config_entry.add_to_hass(hass)
  263. assert await hass.config_entries.async_setup(config_entry.entry_id)
  264. await hass.async_block_till_done()
  265. # show initial form
  266. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  267. assert "form" == result["type"]
  268. assert "user" == result["step_id"]
  269. assert {} == result["errors"]
  270. assert result["data_schema"](
  271. {
  272. CONF_HOST: "hostname",
  273. CONF_LOCAL_KEY: "localkey",
  274. CONF_SWITCH: True,
  275. }
  276. )
  277. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  278. async def test_options_flow_modifies_config(mock_test, hass):
  279. mock_device = MagicMock()
  280. mock_test.return_value = mock_device
  281. config_entry = MockConfigEntry(
  282. domain=DOMAIN,
  283. unique_id="uniqueid",
  284. data={
  285. CONF_CLIMATE: True,
  286. CONF_DEVICE_ID: "deviceid",
  287. CONF_HOST: "hostname",
  288. CONF_LOCAL_KEY: "localkey",
  289. CONF_LOCK: True,
  290. CONF_NAME: "test",
  291. CONF_TYPE: "kogan_heater",
  292. },
  293. )
  294. config_entry.add_to_hass(hass)
  295. assert await hass.config_entries.async_setup(config_entry.entry_id)
  296. await hass.async_block_till_done()
  297. # show initial form
  298. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  299. # submit updated config
  300. result = await hass.config_entries.options.async_configure(
  301. form["flow_id"],
  302. user_input={
  303. CONF_CLIMATE: True,
  304. CONF_HOST: "new_hostname",
  305. CONF_LOCAL_KEY: "new_key",
  306. CONF_LOCK: False,
  307. },
  308. )
  309. expected = {
  310. CONF_CLIMATE: True,
  311. CONF_HOST: "new_hostname",
  312. CONF_LOCAL_KEY: "new_key",
  313. CONF_LOCK: False,
  314. }
  315. assert "create_entry" == result["type"]
  316. assert "" == result["title"]
  317. assert result["result"] is True
  318. assert expected == result["data"]
  319. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  320. async def test_options_flow_fails_when_connection_fails(mock_test, hass):
  321. mock_test.return_value = None
  322. config_entry = MockConfigEntry(
  323. domain=DOMAIN,
  324. unique_id="uniqueid",
  325. data={
  326. CONF_DEVICE_ID: "deviceid",
  327. CONF_HOST: "hostname",
  328. CONF_LOCAL_KEY: "localkey",
  329. CONF_NAME: "test",
  330. CONF_SWITCH: True,
  331. CONF_TYPE: "kogan_switch",
  332. },
  333. )
  334. config_entry.add_to_hass(hass)
  335. assert await hass.config_entries.async_setup(config_entry.entry_id)
  336. await hass.async_block_till_done()
  337. # show initial form
  338. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  339. # submit updated config
  340. result = await hass.config_entries.options.async_configure(
  341. form["flow_id"],
  342. user_input={
  343. CONF_HOST: "new_hostname",
  344. CONF_LOCAL_KEY: "new_key",
  345. CONF_SWITCH: False,
  346. },
  347. )
  348. assert "form" == result["type"]
  349. assert "user" == result["step_id"]
  350. assert {"base": "connection"} == result["errors"]
  351. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  352. async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
  353. mock_device = MagicMock()
  354. mock_test.return_value = mock_device
  355. config_entry = MockConfigEntry(
  356. domain=DOMAIN,
  357. unique_id="uniqueid",
  358. data={
  359. CONF_DEVICE_ID: "deviceid",
  360. CONF_HOST: "hostname",
  361. CONF_LOCAL_KEY: "localkey",
  362. CONF_NAME: "test",
  363. CONF_SWITCH: True,
  364. CONF_TYPE: "non_existing",
  365. },
  366. )
  367. config_entry.add_to_hass(hass)
  368. assert await hass.config_entries.async_setup(config_entry.entry_id)
  369. await hass.async_block_till_done()
  370. # show initial form
  371. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  372. assert result["type"] == "abort"
  373. assert result["reason"] == "not_supported"