from unittest.mock import AsyncMock, patch import pytest import caldav.lib.error as caldav_error from homeassistant.config_entries import SOURCE_USER 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, 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": "", } # --------------------------------------------------------------------------- # 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 — sync_mode step # --------------------------------------------------------------------------- async def test_options_sync_mode_manual(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) assert result["type"] == "form" 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"] == "form" assert result["step_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_MANUAL async def test_options_sync_mode_caldav_proceeds_to_caldav_step(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) 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): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) 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 test_options_caldav_saves_on_success(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV} ) 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"] == "form" assert result["step_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" assert result["data"][CONF_REFRESH_INTERVAL] == 30 async def test_options_caldav_bad_credentials_shows_error(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV} ) 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): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_CALDAV} ) 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 test_options_api_saves_on_success(hass: HomeAssistant, mock_entry): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API} ) 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"] == "form" assert result["step_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_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 mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API} ) 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): mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_API} ) 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): """Navigate to the defaults step via manual sync mode.""" mock_entry.add_to_hass(hass) result = await hass.config_entries.options.async_init(mock_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={CONF_SYNC_MODE: SYNC_MODE_MANUAL} ) 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"]