test_config_flow.py 20 KB

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