test_config_flow.py 16 KB

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