test_config_flow.py 18 KB

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