test_config_flow.py 15 KB

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