test_config_flow.py 19 KB

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