test_config_flow.py 18 KB

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