test_config_flow.py 14 KB

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