test_config_flow.py 19 KB

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