test_config_flow.py 20 KB

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