test_config_flow.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  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 config_flow, async_migrate_entry
  8. from custom_components.tuya_local.const import (
  9. CONF_CLIMATE,
  10. CONF_DEVICE_ID,
  11. CONF_LOCAL_KEY,
  12. CONF_LOCK,
  13. CONF_SWITCH,
  14. CONF_TYPE,
  15. DOMAIN,
  16. )
  17. @pytest.fixture(autouse=True)
  18. def auto_enable_custom_integrations(enable_custom_integrations):
  19. yield
  20. @pytest.fixture
  21. def bypass_setup():
  22. """Prevent actual setup of the integration after config flow."""
  23. with patch(
  24. "custom_components.tuya_local.async_setup_entry",
  25. return_value=True,
  26. ):
  27. yield
  28. async def test_init_entry(hass):
  29. """Test initialisation of the config flow."""
  30. entry = MockConfigEntry(
  31. domain=DOMAIN,
  32. version=2,
  33. title="test",
  34. data={
  35. CONF_DEVICE_ID: "deviceid",
  36. CONF_HOST: "hostname",
  37. CONF_LOCAL_KEY: "localkey",
  38. CONF_TYPE: "kogan_heater",
  39. CONF_CLIMATE: True,
  40. CONF_LOCK: True,
  41. },
  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. state = hass.states.get("climate.test")
  47. assert state
  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="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. CONF_CLIMATE: True,
  64. "child_lock": True,
  65. "display_light": True,
  66. },
  67. )
  68. assert await async_migrate_entry(hass, entry)
  69. async def test_flow_user_init(hass):
  70. """Test the initialisation of the form in the first step of the config flow."""
  71. result = await hass.config_entries.flow.async_init(
  72. DOMAIN, context={"source": "user"}
  73. )
  74. expected = {
  75. "data_schema": ANY,
  76. "description_placeholders": None,
  77. "errors": {},
  78. "flow_id": ANY,
  79. "handler": DOMAIN,
  80. "step_id": "user",
  81. "type": "form",
  82. "last_step": ANY,
  83. }
  84. assert expected == result
  85. # Check the schema. Simple comparison does not work since they are not
  86. # the same object
  87. try:
  88. result["data_schema"](
  89. {CONF_DEVICE_ID: "test", CONF_LOCAL_KEY: "test", CONF_HOST: "test"}
  90. )
  91. except vol.MultipleInvalid:
  92. assert False
  93. try:
  94. result["data_schema"]({CONF_DEVICE_ID: "missing_some"})
  95. assert False
  96. except vol.MultipleInvalid:
  97. pass
  98. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  99. async def test_async_test_connection_valid(mock_device, hass):
  100. """Test that device is returned when connection is valid."""
  101. mock_instance = AsyncMock()
  102. mock_instance.has_returned_state = True
  103. mock_device.return_value = mock_instance
  104. device = await config_flow.async_test_connection(
  105. {
  106. CONF_DEVICE_ID: "deviceid",
  107. CONF_LOCAL_KEY: "localkey",
  108. CONF_HOST: "hostname",
  109. },
  110. hass,
  111. )
  112. assert device == mock_instance
  113. @patch("custom_components.tuya_local.config_flow.TuyaLocalDevice")
  114. async def test_async_test_connection_invalid(mock_device, hass):
  115. """Test that None is returned when connection is invalid."""
  116. mock_instance = AsyncMock()
  117. mock_instance.has_returned_state = False
  118. mock_device.return_value = mock_instance
  119. device = await config_flow.async_test_connection(
  120. {
  121. CONF_DEVICE_ID: "deviceid",
  122. CONF_LOCAL_KEY: "localkey",
  123. CONF_HOST: "hostname",
  124. },
  125. hass,
  126. )
  127. assert device is None
  128. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  129. async def test_flow_user_init_invalid_config(mock_test, hass):
  130. """Test errors populated when config is invalid."""
  131. mock_test.return_value = None
  132. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  133. result = await hass.config_entries.flow.async_configure(
  134. flow["flow_id"],
  135. user_input={
  136. CONF_DEVICE_ID: "deviceid",
  137. CONF_HOST: "hostname",
  138. CONF_LOCAL_KEY: "badkey",
  139. },
  140. )
  141. assert {"base": "connection"} == result["errors"]
  142. def setup_device_mock(mock, failure=False, type="test"):
  143. mock_type = MagicMock()
  144. mock_type.legacy_type = type
  145. mock_iter = MagicMock()
  146. mock_iter.__aiter__.return_value = [mock_type] if not failure else []
  147. mock.async_possible_types = MagicMock(return_value=mock_iter)
  148. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  149. async def test_flow_user_init_data_valid(mock_test, hass):
  150. """Test we advance to the next step when connection config is valid."""
  151. mock_device = MagicMock()
  152. setup_device_mock(mock_device)
  153. mock_test.return_value = mock_device
  154. flow = await hass.config_entries.flow.async_init(DOMAIN, context={"source": "user"})
  155. result = await hass.config_entries.flow.async_configure(
  156. flow["flow_id"],
  157. user_input={
  158. CONF_DEVICE_ID: "deviceid",
  159. CONF_HOST: "hostname",
  160. CONF_LOCAL_KEY: "localkey",
  161. },
  162. )
  163. assert "form" == result["type"]
  164. assert "select_type" == result["step_id"]
  165. @patch.object(config_flow.ConfigFlowHandler, "device")
  166. async def test_flow_select_type_init(mock_device, hass):
  167. """Test the initialisation of the form in the 2nd step of the config flow."""
  168. setup_device_mock(mock_device)
  169. result = await hass.config_entries.flow.async_init(
  170. DOMAIN, context={"source": "select_type"}
  171. )
  172. expected = {
  173. "data_schema": ANY,
  174. "description_placeholders": None,
  175. "errors": None,
  176. "flow_id": ANY,
  177. "handler": DOMAIN,
  178. "step_id": "select_type",
  179. "type": "form",
  180. "last_step": ANY,
  181. }
  182. assert expected == result
  183. # Check the schema. Simple comparison does not work since they are not
  184. # the same object
  185. try:
  186. result["data_schema"]({CONF_TYPE: "test"})
  187. except vol.MultipleInvalid:
  188. assert False
  189. try:
  190. result["data_schema"]({CONF_TYPE: "not_test"})
  191. assert False
  192. except vol.MultipleInvalid:
  193. pass
  194. @patch.object(config_flow.ConfigFlowHandler, "device")
  195. async def test_flow_select_type_aborts_when_no_match(mock_device, hass):
  196. """Test the flow aborts when an unsupported device is used."""
  197. setup_device_mock(mock_device, failure=True)
  198. result = await hass.config_entries.flow.async_init(
  199. DOMAIN, context={"source": "select_type"}
  200. )
  201. assert result["type"] == "abort"
  202. assert result["reason"] == "not_supported"
  203. @patch.object(config_flow.ConfigFlowHandler, "device")
  204. async def test_flow_select_type_data_valid(mock_device, hass):
  205. """Test the flow continues when valid data is supplied."""
  206. setup_device_mock(mock_device, type="kogan_switch")
  207. flow = await hass.config_entries.flow.async_init(
  208. DOMAIN, context={"source": "select_type"}
  209. )
  210. result = await hass.config_entries.flow.async_configure(
  211. flow["flow_id"],
  212. user_input={CONF_TYPE: "kogan_switch"},
  213. )
  214. assert "form" == result["type"]
  215. assert "choose_entities" == result["step_id"]
  216. async def test_flow_choose_entities_init(hass):
  217. """Test the initialisation of the form in the 3rd step of the config flow."""
  218. with patch.dict(config_flow.ConfigFlowHandler.data, {CONF_TYPE: "kogan_switch"}):
  219. result = await hass.config_entries.flow.async_init(
  220. DOMAIN, context={"source": "choose_entities"}
  221. )
  222. expected = {
  223. "data_schema": ANY,
  224. "description_placeholders": None,
  225. "errors": None,
  226. "flow_id": ANY,
  227. "handler": DOMAIN,
  228. "step_id": "choose_entities",
  229. "type": "form",
  230. "last_step": ANY,
  231. }
  232. assert expected == result
  233. # Check the schema. Simple comparison does not work since they are not
  234. # the same object
  235. try:
  236. result["data_schema"]({CONF_NAME: "test", CONF_SWITCH: True})
  237. except vol.MultipleInvalid:
  238. assert False
  239. try:
  240. result["data_schema"]({CONF_CLIMATE: True})
  241. assert False
  242. except vol.MultipleInvalid:
  243. pass
  244. async def test_flow_choose_entities_creates_config_entry(hass, bypass_setup):
  245. """Test the flow ends when data is valid."""
  246. with patch.dict(
  247. config_flow.ConfigFlowHandler.data,
  248. {
  249. CONF_DEVICE_ID: "deviceid",
  250. CONF_LOCAL_KEY: "localkey",
  251. CONF_HOST: "hostname",
  252. CONF_TYPE: "kogan_heater",
  253. },
  254. ):
  255. flow = await hass.config_entries.flow.async_init(
  256. DOMAIN, context={"source": "choose_entities"}
  257. )
  258. result = await hass.config_entries.flow.async_configure(
  259. flow["flow_id"],
  260. user_input={CONF_NAME: "test", CONF_CLIMATE: True, CONF_LOCK: False},
  261. )
  262. expected = {
  263. "version": 2,
  264. "type": "create_entry",
  265. "flow_id": ANY,
  266. "handler": DOMAIN,
  267. "title": "test",
  268. "description": None,
  269. "description_placeholders": None,
  270. "result": ANY,
  271. "options": {},
  272. "data": {
  273. CONF_CLIMATE: True,
  274. CONF_DEVICE_ID: "deviceid",
  275. CONF_HOST: "hostname",
  276. CONF_LOCAL_KEY: "localkey",
  277. CONF_LOCK: False,
  278. CONF_TYPE: "kogan_heater",
  279. },
  280. }
  281. assert expected == result
  282. async def test_options_flow_init(hass):
  283. """Test config flow options."""
  284. config_entry = MockConfigEntry(
  285. domain=DOMAIN,
  286. unique_id="uniqueid",
  287. data={
  288. CONF_DEVICE_ID: "deviceid",
  289. CONF_HOST: "hostname",
  290. CONF_LOCAL_KEY: "localkey",
  291. CONF_NAME: "test",
  292. CONF_SWITCH: True,
  293. CONF_TYPE: "kogan_switch",
  294. },
  295. )
  296. config_entry.add_to_hass(hass)
  297. assert await hass.config_entries.async_setup(config_entry.entry_id)
  298. await hass.async_block_till_done()
  299. # show initial form
  300. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  301. assert "form" == result["type"]
  302. assert "user" == result["step_id"]
  303. assert {} == result["errors"]
  304. assert result["data_schema"](
  305. {
  306. CONF_HOST: "hostname",
  307. CONF_LOCAL_KEY: "localkey",
  308. CONF_SWITCH: True,
  309. }
  310. )
  311. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  312. async def test_options_flow_modifies_config(mock_test, hass):
  313. mock_device = MagicMock()
  314. mock_test.return_value = mock_device
  315. config_entry = MockConfigEntry(
  316. domain=DOMAIN,
  317. unique_id="uniqueid",
  318. data={
  319. CONF_CLIMATE: True,
  320. CONF_DEVICE_ID: "deviceid",
  321. CONF_HOST: "hostname",
  322. CONF_LOCAL_KEY: "localkey",
  323. CONF_LOCK: True,
  324. CONF_NAME: "test",
  325. CONF_TYPE: "kogan_heater",
  326. },
  327. )
  328. config_entry.add_to_hass(hass)
  329. assert await hass.config_entries.async_setup(config_entry.entry_id)
  330. await hass.async_block_till_done()
  331. # show initial form
  332. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  333. # submit updated config
  334. result = await hass.config_entries.options.async_configure(
  335. form["flow_id"],
  336. user_input={
  337. CONF_CLIMATE: True,
  338. CONF_HOST: "new_hostname",
  339. CONF_LOCAL_KEY: "new_key",
  340. CONF_LOCK: False,
  341. },
  342. )
  343. expected = {
  344. CONF_CLIMATE: True,
  345. CONF_HOST: "new_hostname",
  346. CONF_LOCAL_KEY: "new_key",
  347. CONF_LOCK: False,
  348. }
  349. assert "create_entry" == result["type"]
  350. assert "" == result["title"]
  351. assert result["result"] is True
  352. assert expected == result["data"]
  353. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  354. async def test_options_flow_fails_when_connection_fails(mock_test, hass):
  355. mock_test.return_value = None
  356. config_entry = MockConfigEntry(
  357. domain=DOMAIN,
  358. unique_id="uniqueid",
  359. data={
  360. CONF_DEVICE_ID: "deviceid",
  361. CONF_HOST: "hostname",
  362. CONF_LOCAL_KEY: "localkey",
  363. CONF_NAME: "test",
  364. CONF_SWITCH: True,
  365. CONF_TYPE: "kogan_switch",
  366. },
  367. )
  368. config_entry.add_to_hass(hass)
  369. assert await hass.config_entries.async_setup(config_entry.entry_id)
  370. await hass.async_block_till_done()
  371. # show initial form
  372. form = await hass.config_entries.options.async_init(config_entry.entry_id)
  373. # submit updated config
  374. result = await hass.config_entries.options.async_configure(
  375. form["flow_id"],
  376. user_input={
  377. CONF_HOST: "new_hostname",
  378. CONF_LOCAL_KEY: "new_key",
  379. CONF_SWITCH: False,
  380. },
  381. )
  382. assert "form" == result["type"]
  383. assert "user" == result["step_id"]
  384. assert {"base": "connection"} == result["errors"]
  385. @patch("custom_components.tuya_local.config_flow.async_test_connection")
  386. async def test_options_flow_fails_when_config_is_missing(mock_test, hass):
  387. mock_device = MagicMock()
  388. mock_test.return_value = mock_device
  389. config_entry = MockConfigEntry(
  390. domain=DOMAIN,
  391. unique_id="uniqueid",
  392. data={
  393. CONF_DEVICE_ID: "deviceid",
  394. CONF_HOST: "hostname",
  395. CONF_LOCAL_KEY: "localkey",
  396. CONF_NAME: "test",
  397. CONF_SWITCH: True,
  398. CONF_TYPE: "non_existing",
  399. },
  400. )
  401. config_entry.add_to_hass(hass)
  402. assert await hass.config_entries.async_setup(config_entry.entry_id)
  403. await hass.async_block_till_done()
  404. # show initial form
  405. result = await hass.config_entries.options.async_init(config_entry.entry_id)
  406. assert result["type"] == "abort"
  407. assert result["reason"] == "not_supported"