test_config_flow.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633
  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 (
  8. config_flow,
  9. async_migrate_entry,
  10. async_setup_entry,
  11. )
  12. from custom_components.tuya_local.const import (
  13. CONF_CLIMATE,
  14. CONF_DEVICE_ID,
  15. CONF_FAN,
  16. CONF_HUMIDIFIER,
  17. CONF_LIGHT,
  18. CONF_LOCAL_KEY,
  19. CONF_LOCK,
  20. CONF_SWITCH,
  21. CONF_TYPE,
  22. DOMAIN,
  23. )
  24. @pytest.fixture(autouse=True)
  25. def auto_enable_custom_integrations(enable_custom_integrations):
  26. yield
  27. @pytest.fixture
  28. def bypass_setup():
  29. """Prevent actual setup of the integration after config flow."""
  30. with patch(
  31. "custom_components.tuya_local.async_setup_entry",
  32. return_value=True,
  33. ):
  34. yield
  35. async def test_init_entry(hass):
  36. """Test initialisation of the config flow."""
  37. entry = MockConfigEntry(
  38. domain=DOMAIN,
  39. version=6,
  40. title="test",
  41. data={
  42. CONF_DEVICE_ID: "deviceid",
  43. CONF_HOST: "hostname",
  44. CONF_LOCAL_KEY: "localkey",
  45. CONF_TYPE: "kogan_kahtp_heater",
  46. CONF_CLIMATE: True,
  47. "lock_child_lock": True,
  48. },
  49. )
  50. entry.add_to_hass(hass)
  51. await hass.config_entries.async_setup(entry.entry_id)
  52. await hass.async_block_till_done()
  53. assert hass.states.get("climate.test")
  54. assert hass.states.get("lock.test_child_lock")
  55. @patch("custom_components.tuya_local.setup_device")
  56. async def test_migrate_entry(mock_setup, hass):
  57. """Test migration from old entry format."""
  58. mock_device = MagicMock()
  59. mock_device.async_inferred_type = AsyncMock(return_value="goldair_gpph_heater")
  60. mock_setup.return_value = mock_device
  61. entry = MockConfigEntry(
  62. domain=DOMAIN,
  63. version=1,
  64. title="test",
  65. data={
  66. CONF_DEVICE_ID: "deviceid",
  67. CONF_HOST: "hostname",
  68. CONF_LOCAL_KEY: "localkey",
  69. CONF_TYPE: "auto",
  70. CONF_CLIMATE: True,
  71. "child_lock": True,
  72. "display_light": True,
  73. },
  74. )
  75. assert await async_migrate_entry(hass, entry)
  76. mock_device.async_inferred_type = AsyncMock(return_value=None)
  77. mock_device.reset_mock()
  78. entry = MockConfigEntry(
  79. domain=DOMAIN,
  80. version=1,
  81. title="test2",
  82. data={
  83. CONF_DEVICE_ID: "deviceid",
  84. CONF_HOST: "hostname",
  85. CONF_LOCAL_KEY: "localkey",
  86. CONF_TYPE: "unknown",
  87. CONF_CLIMATE: False,
  88. },
  89. )
  90. assert not await async_migrate_entry(hass, entry)
  91. mock_device.reset_mock()
  92. entry = MockConfigEntry(
  93. domain=DOMAIN,
  94. version=2,
  95. title="test3",
  96. data={
  97. CONF_DEVICE_ID: "deviceid",
  98. CONF_HOST: "hostname",
  99. CONF_LOCAL_KEY: "localkey",
  100. CONF_TYPE: "auto",
  101. CONF_CLIMATE: False,
  102. },
  103. )
  104. assert not await async_migrate_entry(hass, entry)
  105. mock_device.async_inferred_type = AsyncMock(return_value="smartplugv1")
  106. mock_device.reset_mock()
  107. entry = MockConfigEntry(
  108. domain=DOMAIN,
  109. version=3,
  110. title="test4",
  111. data={
  112. CONF_DEVICE_ID: "deviceid",
  113. CONF_HOST: "hostname",
  114. CONF_LOCAL_KEY: "localkey",
  115. CONF_TYPE: "smartplugv1",
  116. CONF_SWITCH: True,
  117. },
  118. )
  119. assert await async_migrate_entry(hass, entry)
  120. mock_device.async_inferred_type = AsyncMock(return_value="smartplugv2")
  121. mock_device.reset_mock()
  122. entry = MockConfigEntry(
  123. domain=DOMAIN,
  124. version=3,
  125. title="test5",
  126. data={
  127. CONF_DEVICE_ID: "deviceid",
  128. CONF_HOST: "hostname",
  129. CONF_LOCAL_KEY: "localkey",
  130. CONF_TYPE: "smartplugv1",
  131. CONF_SWITCH: True,
  132. },
  133. )
  134. assert await async_migrate_entry(hass, entry)
  135. mock_device.async_inferred_type = AsyncMock(return_value="goldair_dehumidifier")
  136. mock_device.reset_mock()
  137. entry = MockConfigEntry(
  138. domain=DOMAIN,
  139. version=4,
  140. title="test6",
  141. data={
  142. CONF_DEVICE_ID: "deviceid",
  143. CONF_HOST: "hostname",
  144. CONF_LOCAL_KEY: "localkey",
  145. CONF_TYPE: "goldair_dehumidifier",
  146. CONF_HUMIDIFIER: True,
  147. CONF_FAN: True,
  148. CONF_LIGHT: True,
  149. CONF_LOCK: False,
  150. CONF_SWITCH: True,
  151. },
  152. )
  153. assert await async_migrate_entry(hass, entry)
  154. mock_device.async_inferred_type = AsyncMock(
  155. return_value="grid_connect_usb_double_power_point"
  156. )
  157. mock_device.reset_mock()
  158. entry = MockConfigEntry(
  159. domain=DOMAIN,
  160. version=6,
  161. title="test7",
  162. data={
  163. CONF_DEVICE_ID: "deviceid",
  164. CONF_HOST: "hostname",
  165. CONF_LOCAL_KEY: "localkey",
  166. CONF_TYPE: "grid_connect_usb_double_power_point",
  167. "switch_main_switch": True,
  168. "switch_left_outlet": True,
  169. "switch_right_outlet": True,
  170. },
  171. )
  172. assert await async_migrate_entry(hass, entry)
  173. async def test_flow_user_init(hass):
  174. """Test the initialisation of the form in the first step of the config flow."""
  175. result = await hass.config_entries.flow.async_init(
  176. DOMAIN, context={"source": "user"}
  177. )
  178. expected = {
  179. "data_schema": ANY,
  180. "description_placeholders": None,
  181. "errors": {},
  182. "flow_id": ANY,
  183. "handler": DOMAIN,
  184. "step_id": "user",
  185. "type": "form",
  186. "last_step": ANY,
  187. }
  188. assert expected == result
  189. # Check the schema. Simple comparison does not work since they are not
  190. # the same object
  191. try:
  192. result["data_schema"](
  193. {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: "test", CONF_HOST: "test"}
  194. )
  195. except vol.MultipleInvalid:
  196. assert False
  197. try:
  198. result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
  199. assert False
  200. except vol.MultipleInvalid:
  201. pass
  202. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  203. async def test_async_test_connection_valid(mock_device, hass):
  204. """Test that device is returned when connection is valid."""
  205. mock_instance = AsyncMock()
  206. mock_instance.has_returned_state = True
  207. mock_device.return_value = mock_instance
  208. device = await config_flow.async_test_connection(
  209. {
  210. CONF_DEVICE_ID: "deviceid",
  211. CONF_LOCAL_KEY: "localkey",
  212. CONF_HOST: "hostname",
  213. },
  214. hass,
  215. )
  216. assert device == mock_instance
  217. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  218. async def test_async_test_connection_invalid(mock_device, hass):
  219. """Test that None is returned when connection is invalid."""
  220. mock_instance = AsyncMock()
  221. mock_instance.has_returned_state = False
  222. mock_device.return_value = mock_instance
  223. device = await config_flow.async_test_connection(
  224. {
  225. CONF_DEVICE_ID: "deviceid",
  226. CONF_LOCAL_KEY: "localkey",
  227. CONF_HOST: "hostname",
  228. },
  229. hass,
  230. )
  231. assert device is None
  232. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  233. async def test_flow_user_init_invalid_config(mock_test, hass):
  234. """Test errors populated when config is invalid."""
  235. mock_test.return_value = None
  236. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  237. result = await hass.config_entries.flow.async_configure(
  238. flow["flow_id"],
  239. user_input={
  240. CONF_DEVICE_ID: "deviceid",
  241. CONF_HOST: "hostname",
  242. CONF_LOCAL_KEY: "badkey",
  243. },
  244. )
  245. assert {"base": "connection"} == result["errors"]
  246. def setup_device_mock(mock, failure=False, type="test"):
  247. mock_type = MagicMock()
  248. mock_type.legacy_type = type
  249. mock_type.config_type = type
  250. mock_type.match_quality.return_value = 100
  251. mock_iter = MagicMock()
  252. mock_iter.__aiter__.return_value = [mock_type] if not failure else []
  253. mock.async_possible_types = MagicMock(return_value=mock_iter)
  254. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  255. async def test_flow_user_init_data_valid(mock_test, hass):
  256. """Test we advance to the next step when connection config is valid."""
  257. mock_device = MagicMock()
  258. setup_device_mock(mock_device)
  259. mock_test.return_value = mock_device
  260. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  261. result = await hass.config_entries.flow.async_configure(
  262. flow["flow_id"],
  263. user_input={
  264. CONF_DEVICE_ID: "deviceid",
  265. CONF_HOST: "hostname",
  266. CONF_LOCAL_KEY: "localkey",
  267. },
  268. )
  269. assert "form" == result["type"]
  270. assert "select_type" == result["step_id"]
  271. @patch.object(config_flow.ConfigFlowHandler, "device")
  272. async def test_flow_select_type_init(mock_device, hass):
  273. """Test the initialisation of the form in the 2nd step of the config flow."""
  274. setup_device_mock(mock_device)
  275. result = await hass.config_entries.flow.async_init(
  276. DOMAIN, context={"source": "select_type"}
  277. )
  278. expected = {
  279. "data_schema": ANY,
  280. "description_placeholders": None,
  281. "errors": None,
  282. "flow_id": ANY,
  283. "handler": DOMAIN,
  284. "step_id": "select_type",
  285. "type": "form",
  286. "last_step": ANY,
  287. }
  288. assert expected == result
  289. # Check the schema. Simple comparison does not work since they are not
  290. # the same object
  291. try:
  292. result["data_schema"]({CONF_TYPE: "test"})
  293. except vol.MultipleInvalid:
  294. assert False
  295. try:
  296. result["data_schema"]({CONF_TYPE: "not_test"})
  297. assert False
  298. except vol.MultipleInvalid:
  299. pass
  300. @patch.object(config_flow.ConfigFlowHandler, "device")
  301. async def test_flow_select_type_aborts_when_no_match(mock_device, hass):
  302. """Test the flow aborts when an unsupported device is used."""
  303. setup_device_mock(mock_device, failure=True)
  304. result = await hass.config_entries.flow.async_init(
  305. DOMAIN, context={"source": "select_type"}
  306. )
  307. assert result["type"] == "abort"
  308. assert result["reason"] == "not_supported"
  309. @patch.object(config_flow.ConfigFlowHandler, "device")
  310. async def test_flow_select_type_data_valid(mock_device, hass):
  311. """Test the flow continues when valid data is supplied."""
  312. setup_device_mock(mock_device, type="kogan_switch")
  313. flow = await hass.config_entries.flow.async_init(
  314. DOMAIN, context={"source": "select_type"}
  315. )
  316. result = await hass.config_entries.flow.async_configure(
  317. flow["flow_id"],
  318. user_input={CONF_TYPE: "kogan_switch"},
  319. )
  320. assert "form" == result["type"]
  321. assert "choose_entities" == result["step_id"]
  322. async def test_flow_choose_entities_init(hass):
  323. """Test the initialisation of the form in the 3rd step of the config flow."""
  324. with patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "kogan_switch"}):
  325. result = await hass.config_entries.flow.async_init(
  326. DOMAIN, context={"source": "choose_entities"}
  327. )
  328. expected = {
  329. "data_schema": ANY,
  330. "description_placeholders": None,
  331. "errors": None,
  332. "flow_id": ANY,
  333. "handler": DOMAIN,
  334. "step_id": "choose_entities",
  335. "type": "form",
  336. "last_step": ANY,
  337. }
  338. assert expected == result
  339. # Check the schema. Simple comparison does not work since they are not
  340. # the same object
  341. try:
  342. result["data_schema"]({CONF_NAME: "test", CONF_SWITCH: True})
  343. except vol.MultipleInvalid:
  344. assert False
  345. try:
  346. result["data_schema"]({CONF_CLIMATE: True})
  347. assert False
  348. except vol.MultipleInvalid:
  349. pass
  350. async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
  351. """Test the flow ends when data is valid."""
  352. with patch.dict(
  353. config_flow.ConfigFlowHandler.data,
  354. {
  355. CONF_DEVICE_ID: "deviceid",
  356. CONF_LOCAL_KEY: "localkey",
  357. CONF_HOST: "hostname",
  358. CONF_TYPE: "kogan_kahtp_heater",
  359. },
  360. ):
  361. flow = await hass.config_entries.flow.async_init(
  362. DOMAIN, context={"source": "choose_entities"}
  363. )
  364. result = await hass.config_entries.flow.async_configure(
  365. flow["flow_id"],
  366. user_input={
  367. CONF_NAME: "test",
  368. CONF_CLIMATE: True,
  369. "lock_child_lock": False,
  370. },
  371. )
  372. expected = {
  373. "version": 7,
  374. "type": "create_entry",
  375. "flow_id": ANY,
  376. "handler": DOMAIN,
  377. "title": "test",
  378. "description": None,
  379. "description_placeholders": None,
  380. "result": ANY,
  381. "options": {},
  382. "data": {
  383. CONF_CLIMATE: True,
  384. CONF_DEVICE_ID: "deviceid",
  385. CONF_HOST: "hostname",
  386. CONF_LOCAL_KEY: "localkey",
  387. "lock_child_lock": False,
  388. CONF_TYPE: "kogan_kahtp_heater",
  389. },
  390. }
  391. assert expected == result
  392. async def test_options_flow_init(hass):
  393. """Test config flow options."""
  394. config_entry = MockConfigEntry(
  395. domain=DOMAIN,
  396. version=7,
  397. unique_id="uniqueid",
  398. data={
  399. CONF_DEVICE_ID: "deviceid",
  400. CONF_HOST: "hostname",
  401. CONF_LOCAL_KEY: "localkey",
  402. CONF_NAME: "test",
  403. CONF_SWITCH: True,
  404. CONF_TYPE: "smartplugv1",
  405. },
  406. )
  407. config_entry.add_to_hass(hass)
  408. assert await hass.config_entries.async_setup(config_entry.entry_id)
  409. await hass.async_block_till_done()
  410. # show initial form
  411. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  412. assert "form" == result["type"]
  413. assert "user" == result["step_id"]
  414. assert {} == result["errors"]
  415. assert result["data_schema"](
  416. {
  417. CONF_HOST: "hostname",
  418. CONF_LOCAL_KEY: "localkey",
  419. CONF_SWITCH: True,
  420. }
  421. )
  422. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  423. async def test_options_flow_modifies_config(mock_test, hass):
  424. mock_device = MagicMock()
  425. mock_test.return_value = mock_device
  426. config_entry = MockConfigEntry(
  427. domain=DOMAIN,
  428. version=7,
  429. unique_id="uniqueid",
  430. data={
  431. CONF_CLIMATE: True,
  432. CONF_DEVICE_ID: "deviceid",
  433. CONF_HOST: "hostname",
  434. CONF_LOCAL_KEY: "localkey",
  435. "lock_child_lock": True,
  436. CONF_NAME: "test",
  437. CONF_TYPE: "kogan_kahtp_heater",
  438. },
  439. )
  440. config_entry.add_to_hass(hass)
  441. assert await hass.config_entries.async_setup(config_entry.entry_id)
  442. await hass.async_block_till_done()
  443. # show initial form
  444. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  445. # submit updated config
  446. result = await hass.config_entries.options.async_configure(
  447. form["flow_id"],
  448. user_input={
  449. CONF_CLIMATE: True,
  450. CONF_HOST: "new_hostname",
  451. CONF_LOCAL_KEY: "new_key",
  452. "lock_child_lock": False,
  453. },
  454. )
  455. expected = {
  456. CONF_CLIMATE: True,
  457. CONF_HOST: "new_hostname",
  458. CONF_LOCAL_KEY: "new_key",
  459. "lock_child_lock": False,
  460. }
  461. assert "create_entry" == result["type"]
  462. assert "" == result["title"]
  463. assert result["result"] is True
  464. assert expected == result["data"]
  465. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  466. async def test_options_flow_fails_when_connection_fails(mock_test, hass):
  467. mock_test.return_value = None
  468. config_entry = MockConfigEntry(
  469. domain=DOMAIN,
  470. version=7,
  471. unique_id="uniqueid",
  472. data={
  473. CONF_DEVICE_ID: "deviceid",
  474. CONF_HOST: "hostname",
  475. CONF_LOCAL_KEY: "localkey",
  476. CONF_NAME: "test",
  477. CONF_SWITCH: True,
  478. CONF_TYPE: "smartplugv1",
  479. },
  480. )
  481. config_entry.add_to_hass(hass)
  482. assert await hass.config_entries.async_setup(config_entry.entry_id)
  483. await hass.async_block_till_done()
  484. # show initial form
  485. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  486. # submit updated config
  487. result = await hass.config_entries.options.async_configure(
  488. form["flow_id"],
  489. user_input={
  490. CONF_HOST: "new_hostname",
  491. CONF_LOCAL_KEY: "new_key",
  492. CONF_SWITCH: False,
  493. },
  494. )
  495. assert "form" == result["type"]
  496. assert "user" == result["step_id"]
  497. assert {"base": "connection"} == result["errors"]
  498. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  499. async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
  500. mock_device = MagicMock()
  501. mock_test.return_value = mock_device
  502. config_entry = MockConfigEntry(
  503. domain=DOMAIN,
  504. version=7,
  505. unique_id="uniqueid",
  506. data={
  507. CONF_DEVICE_ID: "deviceid",
  508. CONF_HOST: "hostname",
  509. CONF_LOCAL_KEY: "localkey",
  510. CONF_NAME: "test",
  511. CONF_SWITCH: True,
  512. CONF_TYPE: "non_existing",
  513. },
  514. )
  515. config_entry.add_to_hass(hass)
  516. await hass.config_entries.async_setup(config_entry.entry_id)
  517. await hass.async_block_till_done()
  518. # show initial form
  519. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  520. assert result["type"] == "abort"
  521. assert result["reason"] == "not_supported"
  522. # More tests to exercise code branches that earlier tests missed.
  523. @patch("custom_components.tuya_local.setup_device")
  524. async def test_async_setup_entry_for_dehumidifier(mock_setup, hass):
  525. """Test setting up based on a config entry. Repeats test_init_entry."""
  526. config_entry = MockConfigEntry(
  527. domain=DOMAIN,
  528. version=7,
  529. unique_id="uniqueid",
  530. data={
  531. CONF_CLIMATE: False,
  532. CONF_DEVICE_ID: "deviceid",
  533. CONF_FAN: True,
  534. CONF_HOST: "hostname",
  535. CONF_HUMIDIFIER: True,
  536. CONF_LIGHT: True,
  537. CONF_LOCK: False,
  538. CONF_LOCAL_KEY: "localkey",
  539. CONF_NAME: "test",
  540. CONF_TYPE: "dehumidifier",
  541. },
  542. )
  543. assert await async_setup_entry(hass, config_entry)
  544. @patch("custom_components.tuya_local.setup_device")
  545. async def test_async_setup_entry_for_switch(mock_device, hass):
  546. """Test setting up based on a config entry. Repeats test_init_entry."""
  547. config_entry = MockConfigEntry(
  548. domain=DOMAIN,
  549. version=7,
  550. unique_id="uniqueid",
  551. data={
  552. CONF_DEVICE_ID: "deviceid",
  553. CONF_HOST: "hostname",
  554. CONF_LOCAL_KEY: "localkey",
  555. CONF_NAME: "test",
  556. CONF_SWITCH: True,
  557. CONF_TYPE: "smartplugv2",
  558. },
  559. )
  560. assert await async_setup_entry(hass, config_entry)