test_config_flow.py 19 KB

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