| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435 |
- from unittest.mock import AsyncMock, patch
- import pytest
- import caldav.lib.error as caldav_error
- from homeassistant.config_entries import SOURCE_USER
- from pytest_homeassistant_custom_component.common import MockConfigEntry
- from homeassistant.core import HomeAssistant
- from custom_components.room_cal_ha.const import (
- CONF_API_BASE_URL,
- CONF_API_TOKEN,
- CONF_CALDAV_URL,
- CONF_CALENDAR_NAME,
- CONF_PASSWORD,
- CONF_REFRESH_INTERVAL,
- CONF_RESERVATION_DEFAULTS,
- CONF_ROOM_NAME,
- CONF_SYNC_MODE,
- CONF_USERNAME,
- CONF_ZLOCK_ENTRIES,
- CONF_ZWAVE_SLOT,
- DEFAULT_REFRESH_INTERVAL,
- DOMAIN,
- SYNC_MODE_API,
- SYNC_MODE_CALDAV,
- SYNC_MODE_MANUAL,
- )
- _EMPTY_DEFAULTS = {
- "guest_name": "",
- "check_in_time": "",
- "check_out_time": "",
- "wifi_ssid": "",
- "wifi_key": "",
- "door_code": "",
- }
- _EMPTY_LOCKS = {CONF_ZLOCK_ENTRIES: [], CONF_ZWAVE_SLOT: 1}
- async def _options_init(hass: HomeAssistant, mock_entry):
- """Start an options flow; returns the init-step result."""
- mock_entry.add_to_hass(hass)
- result = await hass.config_entries.options.async_init(mock_entry.entry_id)
- assert result["type"] == "menu"
- assert result["step_id"] == "configure"
- return result
- async def _select_section(hass: HomeAssistant, flow_id: str, step_id: str):
- """Choose a section from the init menu."""
- return await hass.config_entries.options.async_configure(
- flow_id, user_input={"next_step_id": step_id}
- )
- async def _submit_locks(hass: HomeAssistant, flow_id: str, user_input: dict | None = None):
- return await hass.config_entries.options.async_configure(
- flow_id, user_input=user_input if user_input is not None else _EMPTY_LOCKS
- )
- # ---------------------------------------------------------------------------
- # Config flow — user step
- # ---------------------------------------------------------------------------
- async def test_user_step_shows_form(hass: HomeAssistant):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- assert result["type"] == "form"
- assert result["step_id"] == "user"
- async def test_user_step_creates_entry_with_room_name(hass: HomeAssistant):
- result = await hass.config_entries.flow.async_init(
- DOMAIN, context={"source": SOURCE_USER}
- )
- result = await hass.config_entries.flow.async_configure(
- result["flow_id"],
- user_input={CONF_ROOM_NAME: "Beach House"},
- )
- assert result["type"] == "create_entry"
- assert result["title"] == "Beach House"
- assert result["data"][CONF_ROOM_NAME] == "Beach House"
- # ---------------------------------------------------------------------------
- # Options flow — init (menu) step
- # ---------------------------------------------------------------------------
- async def test_options_init_shows_menu(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- assert result["type"] == "menu"
- assert result["step_id"] == "configure"
- async def test_options_init_sync_leads_to_sync_mode(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- assert result["type"] == "form"
- assert result["step_id"] == "sync_mode"
- async def test_options_init_defaults_leads_to_defaults(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "defaults")
- assert result["type"] == "form"
- assert result["step_id"] == "defaults"
- async def test_options_init_locks_leads_to_locks(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "locks")
- assert result["type"] == "form"
- assert result["step_id"] == "locks"
- # ---------------------------------------------------------------------------
- # Options flow — sync section
- # ---------------------------------------------------------------------------
- async def test_options_sync_mode_manual_saves(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- assert result["step_id"] == "sync_mode"
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL},
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_MANUAL
- async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV},
- )
- assert result["type"] == "form"
- assert result["step_id"] == "caldav"
- async def test_options_sync_mode_api_proceeds_to_api_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={CONF_SYNC_MODE: SYNC_MODE_API},
- )
- assert result["type"] == "form"
- assert result["step_id"] == "api"
- # ---------------------------------------------------------------------------
- # Options flow — CalDAV step
- # ---------------------------------------------------------------------------
- async def _reach_caldav_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV}
- )
- assert result["step_id"] == "caldav"
- return result
- async def test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry):
- result = await _reach_caldav_step(hass, mock_entry)
- with patch("custom_components.room_cal_ha.config_flow._test_caldav_auth", return_value=None):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_CALDAV_URL: "https://cal.example.com",
- CONF_CALENDAR_NAME: "Room",
- CONF_USERNAME: "user",
- CONF_PASSWORD: "pass",
- CONF_REFRESH_INTERVAL: 30,
- },
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
- assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"
- assert result["data"][CONF_REFRESH_INTERVAL] == 30
- async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, mock_entry):
- result = await _reach_caldav_step(hass, mock_entry)
- with patch(
- "custom_components.room_cal_ha.config_flow._test_caldav_auth",
- side_effect=caldav_error.AuthorizationError,
- ):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_CALDAV_URL: "https://cal.example.com",
- CONF_CALENDAR_NAME: "Room",
- CONF_USERNAME: "user",
- CONF_PASSWORD: "bad",
- CONF_REFRESH_INTERVAL: 15,
- },
- )
- assert result["type"] == "form"
- assert result["errors"]["base"] == "invalid_auth"
- async def test_options_caldav_connect_error_shows_error(hass: HomeAssistant, mock_entry):
- result = await _reach_caldav_step(hass, mock_entry)
- with patch(
- "custom_components.room_cal_ha.config_flow._test_caldav_auth",
- side_effect=Exception("timeout"),
- ):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_CALDAV_URL: "https://cal.example.com",
- CONF_CALENDAR_NAME: "Room",
- CONF_USERNAME: "user",
- CONF_PASSWORD: "pass",
- CONF_REFRESH_INTERVAL: 15,
- },
- )
- assert result["type"] == "form"
- assert result["errors"]["base"] == "cannot_connect"
- # ---------------------------------------------------------------------------
- # Options flow — API step
- # ---------------------------------------------------------------------------
- async def _reach_api_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "sync_mode")
- result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API}
- )
- assert result["step_id"] == "api"
- return result
- async def test_options_api_saves_on_success(hass: HomeAssistant, mock_entry):
- result = await _reach_api_step(hass, mock_entry)
- with patch(
- "custom_components.room_cal_ha.config_flow._test_api_connection",
- new=AsyncMock(return_value=None),
- ):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_API_BASE_URL: "https://api.example.com",
- CONF_API_TOKEN: "tok-secret",
- CONF_REFRESH_INTERVAL: 10,
- },
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_API
- assert result["data"][CONF_API_BASE_URL] == "https://api.example.com"
- assert result["data"][CONF_REFRESH_INTERVAL] == 10
- async def test_options_api_bad_token_shows_error(hass: HomeAssistant, mock_entry):
- from custom_components.room_cal_ha.config_flow import _ApiAuthError
- result = await _reach_api_step(hass, mock_entry)
- with patch(
- "custom_components.room_cal_ha.config_flow._test_api_connection",
- new=AsyncMock(side_effect=_ApiAuthError),
- ):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_API_BASE_URL: "https://api.example.com",
- CONF_API_TOKEN: "bad-token",
- CONF_REFRESH_INTERVAL: 15,
- },
- )
- assert result["type"] == "form"
- assert result["errors"]["base"] == "invalid_auth"
- async def test_options_api_connect_error_shows_error(hass: HomeAssistant, mock_entry):
- result = await _reach_api_step(hass, mock_entry)
- with patch(
- "custom_components.room_cal_ha.config_flow._test_api_connection",
- new=AsyncMock(side_effect=Exception("timeout")),
- ):
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- CONF_API_BASE_URL: "https://api.example.com",
- CONF_API_TOKEN: "tok",
- CONF_REFRESH_INTERVAL: 15,
- },
- )
- assert result["type"] == "form"
- assert result["errors"]["base"] == "cannot_connect"
- # ---------------------------------------------------------------------------
- # Options flow — defaults step
- # ---------------------------------------------------------------------------
- async def _reach_defaults_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "defaults")
- assert result["step_id"] == "defaults"
- return result
- async def test_options_defaults_saves_values(hass: HomeAssistant, mock_entry):
- result = await _reach_defaults_step(hass, mock_entry)
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={
- "guest_name": "Default Guest",
- "check_in_time": "15:00",
- "check_out_time": "11:00",
- "wifi_ssid": "HomeNet",
- "wifi_key": "secret",
- "door_code": "1234",
- },
- )
- assert result["type"] == "create_entry"
- defaults = result["data"][CONF_RESERVATION_DEFAULTS]
- assert defaults["guest_name"] == "Default Guest"
- assert defaults["check_in_time"] == "15:00"
- assert defaults["check_out_time"] == "11:00"
- assert defaults["wifi_ssid"] == "HomeNet"
- assert defaults["wifi_key"] == "secret"
- assert defaults["door_code"] == "1234"
- async def test_options_defaults_empty_strings_become_none(hass: HomeAssistant, mock_entry):
- result = await _reach_defaults_step(hass, mock_entry)
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input=_EMPTY_DEFAULTS,
- )
- assert result["type"] == "create_entry"
- defaults = result["data"][CONF_RESERVATION_DEFAULTS]
- assert defaults["guest_name"] is None
- assert defaults["check_in_time"] is None
- assert defaults["wifi_ssid"] is None
- async def test_options_defaults_invalid_time_shows_error(hass: HomeAssistant, mock_entry):
- result = await _reach_defaults_step(hass, mock_entry)
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={**_EMPTY_DEFAULTS, "check_in_time": "not-a-time"},
- )
- assert result["type"] == "form"
- assert "check_in_time" in result["errors"]
- # ---------------------------------------------------------------------------
- # Options flow — locks step
- # ---------------------------------------------------------------------------
- async def _reach_locks_step(hass: HomeAssistant, mock_entry):
- result = await _options_init(hass, mock_entry)
- result = await _select_section(hass, result["flow_id"], "locks")
- assert result["step_id"] == "locks"
- return result
- async def test_options_locks_step_shows_form(hass: HomeAssistant, mock_entry):
- result = await _reach_locks_step(hass, mock_entry)
- assert result["type"] == "form"
- assert result["step_id"] == "locks"
- async def test_options_locks_saves_entities_and_slot(hass: HomeAssistant, mock_entry):
- z1 = MockConfigEntry(domain="zlock_slots", title="Front Lock", data={"lock_entity_ids": ["lock.front"]})
- z2 = MockConfigEntry(domain="zlock_slots", title="Back Lock", data={"lock_entity_ids": ["lock.back"]})
- z1.add_to_hass(hass)
- z2.add_to_hass(hass)
- result = await _reach_locks_step(hass, mock_entry)
- result = await hass.config_entries.options.async_configure(
- result["flow_id"],
- user_input={CONF_ZLOCK_ENTRIES: [z1.entry_id, z2.entry_id], CONF_ZWAVE_SLOT: 5},
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_ZLOCK_ENTRIES] == [z1.entry_id, z2.entry_id]
- assert result["data"][CONF_ZWAVE_SLOT] == 5
- async def test_options_locks_empty_saves_empty_list(hass: HomeAssistant, mock_entry):
- result = await _reach_locks_step(hass, mock_entry)
- result = await _submit_locks(hass, result["flow_id"])
- assert result["type"] == "create_entry"
- assert result["data"][CONF_ZLOCK_ENTRIES] == []
- # ---------------------------------------------------------------------------
- # Cross-section preservation
- # ---------------------------------------------------------------------------
- async def test_options_editing_defaults_preserves_sync_mode(hass: HomeAssistant, mock_entry):
- """Editing defaults alone should not wipe a previously configured sync mode."""
- from homeassistant.config_entries import ConfigEntry
- existing_options = {CONF_SYNC_MODE: SYNC_MODE_CALDAV, CONF_CALDAV_URL: "https://cal.example.com"}
- mock_entry.add_to_hass(hass)
- hass.config_entries.async_update_entry(mock_entry, options=existing_options)
- result = await hass.config_entries.options.async_init(mock_entry.entry_id)
- result = await _select_section(hass, result["flow_id"], "defaults")
- result = await hass.config_entries.options.async_configure(
- result["flow_id"], user_input=_EMPTY_DEFAULTS
- )
- assert result["type"] == "create_entry"
- assert result["data"][CONF_SYNC_MODE] == SYNC_MODE_CALDAV
- assert result["data"][CONF_CALDAV_URL] == "https://cal.example.com"
|