test_config_flow.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429
  1. from unittest.mock import AsyncMock, patch
  2. import pytest
  3. import caldav.lib.error as caldav_error
  4. from homeassistant.config_entries import SOURCE_USER
  5. from homeassistant.core import HomeAssistant
  6. from custom_components.room_cal_ha.const import (
  7. CONF_API_BASE_URL,
  8. CONF_API_TOKEN,
  9. CONF_CALDAV_URL,
  10. CONF_CALENDAR_NAME,
  11. CONF_PASSWORD,
  12. CONF_REFRESH_INTERVAL,
  13. CONF_RESERVATION_DEFAULTS,
  14. CONF_ROOM_NAME,
  15. CONF_SYNC_MODE,
  16. CONF_USERNAME,
  17. CONF_ZWAVE_LOCKS,
  18. CONF_ZWAVE_SLOT,
  19. DEFAULT_REFRESH_INTERVAL,
  20. DOMAIN,
  21. SYNC_MODE_API,
  22. SYNC_MODE_CALDAV,
  23. SYNC_MODE_MANUAL,
  24. )
  25. _EMPTY_DEFAULTS = {
  26. "guest_name": "",
  27. "check_in_time": "",
  28. "check_out_time": "",
  29. "wifi_ssid": "",
  30. "wifi_key": "",
  31. "door_code": "",
  32. }
  33. _EMPTY_LOCKS = {CONF_ZWAVE_LOCKS: [], CONF_ZWAVE_SLOT: 1}
  34. async def _options_init(hass: HomeAssistant, mock_entry):
  35. """Start an options flow; returns the init-step result."""
  36. mock_entry.add_to_hass(hass)
  37. result = await hass.config_entries.options.async_init(mock_entry.entry_id)
  38. assert result["type"] == "menu"
  39. assert result["step_id"] == "init"
  40. return result
  41. async def _select_section(hass: HomeAssistant, flow_id: str, step_id: str):
  42. """Choose a section from the init menu."""
  43. return await hass.config_entries.options.async_configure(
  44. flow_id, user_input={"next_step_id": step_id}
  45. )
  46. async def _submit_locks(hass: HomeAssistant, flow_id: str, user_input: dict | None = None):
  47. return await hass.config_entries.options.async_configure(
  48. flow_id, user_input=user_input if user_input is not None else _EMPTY_LOCKS
  49. )
  50. # ---------------------------------------------------------------------------
  51. # Config flow — user step
  52. # ---------------------------------------------------------------------------
  53. async def test_user_step_shows_form(hass: HomeAssistant):
  54. result = await hass.config_entries.flow.async_init(
  55. DOMAIN, context={"source": SOURCE_USER}
  56. )
  57. assert result["type"] == "form"
  58. assert result["step_id"] == "user"
  59. async def test_user_step_creates_entry_with_room_name(hass: HomeAssistant):
  60. result = await hass.config_entries.flow.async_init(
  61. DOMAIN, context={"source": SOURCE_USER}
  62. )
  63. result = await hass.config_entries.flow.async_configure(
  64. result["flow_id"],
  65. user_input={CONF_ROOM_NAME: "Beach House"},
  66. )
  67. assert result["type"] == "create_entry"
  68. assert result["title"] == "Beach House"
  69. assert result["data"][CONF_ROOM_NAME] == "Beach House"
  70. # ---------------------------------------------------------------------------
  71. # Options flow — init (menu) step
  72. # ---------------------------------------------------------------------------
  73. async def test_options_init_shows_menu(hass: HomeAssistant, mock_entry):
  74. result = await _options_init(hass, mock_entry)
  75. assert result["type"] == "menu"
  76. assert result["step_id"] == "init"
  77. async def test_options_init_sync_leads_to_sync_mode(hass: HomeAssistant, mock_entry):
  78. result = await _options_init(hass, mock_entry)
  79. result = await _select_section(hass, result["flow_id"], "sync_mode")
  80. assert result["type"] == "form"
  81. assert result["step_id"] == "sync_mode"
  82. async def test_options_init_defaults_leads_to_defaults(hass: HomeAssistant, mock_entry):
  83. result = await _options_init(hass, mock_entry)
  84. result = await _select_section(hass, result["flow_id"], "defaults")
  85. assert result["type"] == "form"
  86. assert result["step_id"] == "defaults"
  87. async def test_options_init_locks_leads_to_locks(hass: HomeAssistant, mock_entry):
  88. result = await _options_init(hass, mock_entry)
  89. result = await _select_section(hass, result["flow_id"], "locks")
  90. assert result["type"] == "form"
  91. assert result["step_id"] == "locks"
  92. # ---------------------------------------------------------------------------
  93. # Options flow — sync section
  94. # ---------------------------------------------------------------------------
  95. async def test_options_sync_mode_manual_saves(hass: HomeAssistant, mock_entry):
  96. result = await _options_init(hass, mock_entry)
  97. result = await _select_section(hass, result["flow_id"], "sync_mode")
  98. assert result["step_id"] == "sync_mode"
  99. result = await hass.config_entries.options.async_configure(
  100. result["flow_id"],
  101. user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
  102. )
  103. assert result["type"] == "create_entry"
  104. assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_MANUAL
  105. async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssistant, mock_entry):
  106. result = await _options_init(hass, mock_entry)
  107. result = await _select_section(hass, result["flow_id"], "sync_mode")
  108. result = await hass.config_entries.options.async_configure(
  109. result["flow_id"],
  110. user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV},
  111. )
  112. assert result["type"] == "form"
  113. assert result["step_id"] == "caldav"
  114. async def test_options_sync_mode_api_proceeds_to_api_step(hass: HomeAssistant, mock_entry):
  115. result = await _options_init(hass, mock_entry)
  116. result = await _select_section(hass, result["flow_id"], "sync_mode")
  117. result = await hass.config_entries.options.async_configure(
  118. result["flow_id"],
  119. user_input={CONF_SYNC_MODE: SYNC_MODE_API},
  120. )
  121. assert result["type"] == "form"
  122. assert result["step_id"] == "api"
  123. # ---------------------------------------------------------------------------
  124. # Options flow — CalDAV step
  125. # ---------------------------------------------------------------------------
  126. async def _reach_caldav_step(hass: HomeAssistant, mock_entry):
  127. result = await _options_init(hass, mock_entry)
  128. result = await _select_section(hass, result["flow_id"], "sync_mode")
  129. result = await hass.config_entries.options.async_configure(
  130. result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
  131. )
  132. assert result["step_id"] == "caldav"
  133. return result
  134. async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
  135. result = await _reach_caldav_step(hass, mock_entry)
  136. with patch("custom_components.room_cal_ha.config_flow._test_caldav_auth", return_value=None):
  137. result = await hass.config_entries.options.async_configure(
  138. result["flow_id"],
  139. user_input={
  140. CONF_CALDAV_URL: "https://cal.example.com",
  141. CONF_CALENDAR_NAME: "Room",
  142. CONF_USERNAME: "user",
  143. CONF_PASSWORD: "pass",
  144. CONF_REFRESH_INTERVAL: 30,
  145. },
  146. )
  147. assert result["type"] == "create_entry"
  148. assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
  149. assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"
  150. assert result["data"][CONF_REFRESH_INTERVAL] == 30
  151. async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
  152. result = await _reach_caldav_step(hass, mock_entry)
  153. with patch(
  154. "custom_components.room_cal_ha.config_flow._test_caldav_auth",
  155. side_effect=caldav_error.AuthorizationError,
  156. ):
  157. result = await hass.config_entries.options.async_configure(
  158. result["flow_id"],
  159. user_input={
  160. CONF_CALDAV_URL: "https://cal.example.com",
  161. CONF_CALENDAR_NAME: "Room",
  162. CONF_USERNAME: "user",
  163. CONF_PASSWORD: "bad",
  164. CONF_REFRESH_INTERVAL: 15,
  165. },
  166. )
  167. assert result["type"] == "form"
  168. assert result["errors"]["base"] == "invalid_auth"
  169. async def test_options_caldav_connect_error_shows_error(hass: HomeAssistant, mock_entry):
  170. result = await _reach_caldav_step(hass, mock_entry)
  171. with patch(
  172. "custom_components.room_cal_ha.config_flow._test_caldav_auth",
  173. side_effect=Exception("timeout"),
  174. ):
  175. result = await hass.config_entries.options.async_configure(
  176. result["flow_id"],
  177. user_input={
  178. CONF_CALDAV_URL: "https://cal.example.com",
  179. CONF_CALENDAR_NAME: "Room",
  180. CONF_USERNAME: "user",
  181. CONF_PASSWORD: "pass",
  182. CONF_REFRESH_INTERVAL: 15,
  183. },
  184. )
  185. assert result["type"] == "form"
  186. assert result["errors"]["base"] == "cannot_connect"
  187. # ---------------------------------------------------------------------------
  188. # Options flow — API step
  189. # ---------------------------------------------------------------------------
  190. async def _reach_api_step(hass: HomeAssistant, mock_entry):
  191. result = await _options_init(hass, mock_entry)
  192. result = await _select_section(hass, result["flow_id"], "sync_mode")
  193. result = await hass.config_entries.options.async_configure(
  194. result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
  195. )
  196. assert result["step_id"] == "api"
  197. return result
  198. async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
  199. result = await _reach_api_step(hass, mock_entry)
  200. with patch(
  201. "custom_components.room_cal_ha.config_flow._test_api_connection",
  202. new=AsyncMock(return_value=None),
  203. ):
  204. result = await hass.config_entries.options.async_configure(
  205. result["flow_id"],
  206. user_input={
  207. CONF_API_BASE_URL: "https://api.example.com",
  208. CONF_API_TOKEN: "tok-secret",
  209. CONF_REFRESH_INTERVAL: 10,
  210. },
  211. )
  212. assert result["type"] == "create_entry"
  213. assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_API
  214. assert result["data"][CONF_API_BASE_URL] == "https://api.example.com"
  215. assert result["data"][CONF_REFRESH_INTERVAL] == 10
  216. async def test_options_api_bad_token_shows_error(hass: HomeAssistant, mock_entry):
  217. from custom_components.room_cal_ha.config_flow import _ApiAuthError
  218. result = await _reach_api_step(hass, mock_entry)
  219. with patch(
  220. "custom_components.room_cal_ha.config_flow._test_api_connection",
  221. new=AsyncMock(side_effect=_ApiAuthError),
  222. ):
  223. result = await hass.config_entries.options.async_configure(
  224. result["flow_id"],
  225. user_input={
  226. CONF_API_BASE_URL: "https://api.example.com",
  227. CONF_API_TOKEN: "bad-token",
  228. CONF_REFRESH_INTERVAL: 15,
  229. },
  230. )
  231. assert result["type"] == "form"
  232. assert result["errors"]["base"] == "invalid_auth"
  233. async def test_options_api_connect_error_shows_error(hass: HomeAssistant, mock_entry):
  234. result = await _reach_api_step(hass, mock_entry)
  235. with patch(
  236. "custom_components.room_cal_ha.config_flow._test_api_connection",
  237. new=AsyncMock(side_effect=Exception("timeout")),
  238. ):
  239. result = await hass.config_entries.options.async_configure(
  240. result["flow_id"],
  241. user_input={
  242. CONF_API_BASE_URL: "https://api.example.com",
  243. CONF_API_TOKEN: "tok",
  244. CONF_REFRESH_INTERVAL: 15,
  245. },
  246. )
  247. assert result["type"] == "form"
  248. assert result["errors"]["base"] == "cannot_connect"
  249. # ---------------------------------------------------------------------------
  250. # Options flow — defaults step
  251. # ---------------------------------------------------------------------------
  252. async def _reach_defaults_step(hass: HomeAssistant, mock_entry):
  253. result = await _options_init(hass, mock_entry)
  254. result = await _select_section(hass, result["flow_id"], "defaults")
  255. assert result["step_id"] == "defaults"
  256. return result
  257. async def test_options_defaults_saves_values(hass: HomeAssistant, mock_entry):
  258. result = await _reach_defaults_step(hass, mock_entry)
  259. result = await hass.config_entries.options.async_configure(
  260. result["flow_id"],
  261. user_input={
  262. "guest_name": "Default Guest",
  263. "check_in_time": "15:00",
  264. "check_out_time": "11:00",
  265. "wifi_ssid": "HomeNet",
  266. "wifi_key": "secret",
  267. "door_code": "1234",
  268. },
  269. )
  270. assert result["type"] == "create_entry"
  271. defaults = result["data"][CONF_RESERVATION_DEFAULTS]
  272. assert defaults["guest_name"] == "Default Guest"
  273. assert defaults["check_in_time"] == "15:00"
  274. assert defaults["check_out_time"] == "11:00"
  275. assert defaults["wifi_ssid"] == "HomeNet"
  276. assert defaults["wifi_key"] == "secret"
  277. assert defaults["door_code"] == "1234"
  278. async def test_options_defaults_empty_strings_become_none(hass: HomeAssistant, mock_entry):
  279. result = await _reach_defaults_step(hass, mock_entry)
  280. result = await hass.config_entries.options.async_configure(
  281. result["flow_id"],
  282. user_input=_EMPTY_DEFAULTS,
  283. )
  284. assert result["type"] == "create_entry"
  285. defaults = result["data"][CONF_RESERVATION_DEFAULTS]
  286. assert defaults["guest_name"] is None
  287. assert defaults["check_in_time"] is None
  288. assert defaults["wifi_ssid"] is None
  289. async def test_options_defaults_invalid_time_shows_error(hass: HomeAssistant, mock_entry):
  290. result = await _reach_defaults_step(hass, mock_entry)
  291. result = await hass.config_entries.options.async_configure(
  292. result["flow_id"],
  293. user_input={**_EMPTY_DEFAULTS, "check_in_time": "not-a-time"},
  294. )
  295. assert result["type"] == "form"
  296. assert "check_in_time" in result["errors"]
  297. # ---------------------------------------------------------------------------
  298. # Options flow — locks step
  299. # ---------------------------------------------------------------------------
  300. async def _reach_locks_step(hass: HomeAssistant, mock_entry):
  301. result = await _options_init(hass, mock_entry)
  302. result = await _select_section(hass, result["flow_id"], "locks")
  303. assert result["step_id"] == "locks"
  304. return result
  305. async def test_options_locks_step_shows_form(hass: HomeAssistant, mock_entry):
  306. result = await _reach_locks_step(hass, mock_entry)
  307. assert result["type"] == "form"
  308. assert result["step_id"] == "locks"
  309. async def test_options_locks_saves_entities_and_slot(hass: HomeAssistant, mock_entry):
  310. result = await _reach_locks_step(hass, mock_entry)
  311. result = await hass.config_entries.options.async_configure(
  312. result["flow_id"],
  313. user_input={CONF_ZWAVE_LOCKS: ["lock.front_door", "lock.back_door"], CONF_ZWAVE_SLOT: 5},
  314. )
  315. assert result["type"] == "create_entry"
  316. assert result["data"][CONF_ZWAVE_LOCKS] == ["lock.front_door", "lock.back_door"]
  317. assert result["data"][CONF_ZWAVE_SLOT] == 5
  318. async def test_options_locks_empty_saves_empty_list(hass: HomeAssistant, mock_entry):
  319. result = await _reach_locks_step(hass, mock_entry)
  320. result = await _submit_locks(hass, result["flow_id"])
  321. assert result["type"] == "create_entry"
  322. assert result["data"][CONF_ZWAVE_LOCKS] == []
  323. # ---------------------------------------------------------------------------
  324. # Cross-section preservation
  325. # ---------------------------------------------------------------------------
  326. async def test_options_editing_defaults_preserves_sync_mode(hass: HomeAssistant, mock_entry):
  327. """Editing defaults alone should not wipe a previously configured sync mode."""
  328. from homeassistant.config_entries import ConfigEntry
  329. existing_options = {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com"}
  330. mock_entry.add_to_hass(hass)
  331. hass.config_entries.async_update_entry(mock_entry, options=existing_options)
  332. result = await hass.config_entries.options.async_init(mock_entry.entry_id)
  333. result = await _select_section(hass, result["flow_id"], "defaults")
  334. result = await hass.config_entries.options.async_configure(
  335. result["flow_id"], user_input=_EMPTY_DEFAULTS
  336. )
  337. assert result["type"] == "create_entry"
  338. assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
  339. assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"