"""Tests for the SolarEdge config flow.""" from unittest.mock import AsyncMock, Mock from aiohttp import ClientError, ClientResponseError import pytest from homeassistant.components.recorder import Recorder from homeassistant.components.solaredge.const import ( CONF_SECTION_API_AUTH, CONF_SECTION_WEB_AUTH, CONF_SITE_ID, DEFAULT_NAME, DOMAIN, ) from homeassistant.config_entries import SOURCE_IGNORE, SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .conftest import API_KEY, PASSWORD, SITE_ID, USERNAME from tests.common import MockConfigEntry NAME = "solaredge site 1 2 3" @pytest.fixture(autouse=True) def solaredge_api_fixture(solaredge_api: Mock) -> None: """Mock the solaredge API.""" @pytest.fixture(autouse=True) def solaredge_web_api_fixture(solaredge_web_api: AsyncMock) -> None: """Mock the solaredge web API.""" async def test_user_api_key( recorder_mock: Recorder, hass: HomeAssistant, solaredge_api: Mock, mock_setup_entry: AsyncMock, ) -> None: """Test user config with API key.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "user" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_NAME: NAME, CONF_SITE_ID: SITE_ID, CONF_SECTION_API_AUTH: {CONF_API_KEY: API_KEY}, }, ) assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") assert data assert data[CONF_SITE_ID] == SITE_ID assert data[CONF_API_KEY] == API_KEY assert CONF_USERNAME not in data assert CONF_PASSWORD not in data assert len(mock_setup_entry.mock_calls) == 1 async def test_user_web_login( recorder_mock: Recorder, hass: HomeAssistant, solaredge_web_api: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test user config with web login.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_NAME: NAME, CONF_SITE_ID: SITE_ID, CONF_SECTION_WEB_AUTH: { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, }, }, ) assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("title") == "solaredge_site_1_2_3" data = result.get("data") assert data assert data[CONF_SITE_ID] == SITE_ID assert data[CONF_USERNAME] == USERNAME assert data[CONF_PASSWORD] == PASSWORD assert CONF_API_KEY not in data assert len(mock_setup_entry.mock_calls) == 1 solaredge_web_api.async_get_equipment.assert_awaited_once() async def test_user_both_auth( recorder_mock: Recorder, hass: HomeAssistant, solaredge_api: Mock, solaredge_web_api: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test user config with both API key and web login.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_NAME: NAME, CONF_SITE_ID: SITE_ID, CONF_SECTION_API_AUTH: {CONF_API_KEY: API_KEY}, CONF_SECTION_WEB_AUTH: { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, }, }, ) assert result.get("type") is FlowResultType.CREATE_ENTRY data = result.get("data") assert data assert data[CONF_SITE_ID] == SITE_ID assert data[CONF_API_KEY] == API_KEY assert data[CONF_USERNAME] == USERNAME assert data[CONF_PASSWORD] == PASSWORD assert len(mock_setup_entry.mock_calls) == 1 async def test_abort_if_already_setup( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test we abort if the site_id is already setup.""" MockConfigEntry( domain=DOMAIN, data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ).add_to_hass(hass) # Should fail, same SITE_ID result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={ CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_SECTION_API_AUTH: {CONF_API_KEY: "test"}, }, ) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: "already_configured"} async def test_ignored_entry_does_not_cause_error( recorder_mock: Recorder, hass: HomeAssistant ) -> None: """Test an ignored entry does not cause and error and we can still create an new entry.""" MockConfigEntry( domain="solaredge", data={CONF_NAME: DEFAULT_NAME, CONF_API_KEY: API_KEY}, source=SOURCE_IGNORE, ).add_to_hass(hass) # Should not fail, same SITE_ID result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={ CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_SECTION_API_AUTH: {CONF_API_KEY: "test"}, }, ) assert result["type"] is FlowResultType.CREATE_ENTRY assert result["title"] == "test" data = result["data"] assert data assert data[CONF_SITE_ID] == SITE_ID assert data[CONF_API_KEY] == "test" async def test_no_auth_provided(recorder_mock: Recorder, hass: HomeAssistant) -> None: """Test error when no authentication method is provided.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], {CONF_NAME: NAME, CONF_SITE_ID: SITE_ID}, ) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "auth_missing"} @pytest.mark.parametrize( ("get_details_setup", "expected_error"), [ (AsyncMock(return_value={"details": {"status": "NOK"}}), "site_not_active"), (AsyncMock(return_value={}), "invalid_api_key"), (AsyncMock(side_effect=TimeoutError()), "cannot_connect"), (AsyncMock(side_effect=ClientError()), "cannot_connect"), ], ) async def test_api_key_errors( recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock, solaredge_api: Mock, get_details_setup: AsyncMock, expected_error: str, ) -> None: """Test API key validation errors.""" solaredge_api.get_details = get_details_setup user_input = { CONF_NAME: NAME, CONF_SITE_ID: SITE_ID, CONF_SECTION_API_AUTH: {CONF_API_KEY: API_KEY}, } result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {CONF_SITE_ID: expected_error} # Make sure the config flow is able to recover from above error solaredge_api.get_details = AsyncMock( return_value={"details": {"status": "active"}} ) result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == {CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY} assert len(mock_setup_entry.mock_calls) == 1 @pytest.mark.parametrize( ("api_exception", "expected_error"), [ (ClientResponseError(None, None, status=401), "invalid_auth"), (ClientResponseError(None, None, status=403), "invalid_auth"), (ClientResponseError(None, None, status=400), "cannot_connect"), (ClientResponseError(None, None, status=500), "cannot_connect"), (TimeoutError(), "cannot_connect"), (ClientError(), "cannot_connect"), ], ) async def test_web_login_errors( recorder_mock: Recorder, hass: HomeAssistant, mock_setup_entry: AsyncMock, solaredge_web_api: AsyncMock, api_exception: Exception, expected_error: str, ) -> None: """Test web login validation errors.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) solaredge_web_api.async_get_equipment.side_effect = api_exception user_input = { CONF_NAME: NAME, CONF_SITE_ID: SITE_ID, CONF_SECTION_WEB_AUTH: { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, }, } result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": expected_error} # Make sure the config flow is able to recover from above error solaredge_web_api.async_get_equipment.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input ) assert result.get("type") is FlowResultType.CREATE_ENTRY assert result.get("data") == { CONF_SITE_ID: SITE_ID, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, } assert len(mock_setup_entry.mock_calls) == 1 async def test_reconfigure_flow_api_key( recorder_mock: Recorder, hass: HomeAssistant, solaredge_api: Mock, mock_setup_entry: AsyncMock, ) -> None: """Test reconfigure flow with API key.""" entry = MockConfigEntry( domain=DOMAIN, title=NAME, data={CONF_SITE_ID: SITE_ID, CONF_API_KEY: "old_api_key"}, ) entry.add_to_hass(hass) result = await entry.start_reconfigure_flow(hass) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reconfigure" result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_SECTION_API_AUTH: {CONF_API_KEY: API_KEY}, }, ) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reconfigure_successful" await hass.async_block_till_done() assert entry.title == NAME assert entry.data[CONF_SITE_ID] == SITE_ID assert entry.data[CONF_API_KEY] == API_KEY assert mock_setup_entry.call_count == 1 async def test_reconfigure_flow_web_login_and_errors( recorder_mock: Recorder, hass: HomeAssistant, solaredge_web_api: AsyncMock, mock_setup_entry: AsyncMock, ) -> None: """Test reconfigure flow with web login and error handling.""" entry = MockConfigEntry( domain=DOMAIN, title=NAME, data={CONF_SITE_ID: SITE_ID, CONF_API_KEY: "old_api_key"}, ) entry.add_to_hass(hass) result = await entry.start_reconfigure_flow(hass) assert result.get("type") is FlowResultType.FORM assert result.get("step_id") == "reconfigure" # Test error solaredge_web_api.async_get_equipment.side_effect = ClientResponseError( None, None, status=401 ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_SECTION_WEB_AUTH: { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, }, }, ) assert result.get("type") is FlowResultType.FORM assert result.get("errors") == {"base": "invalid_auth"} # Test recovery solaredge_web_api.async_get_equipment.side_effect = None result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_SECTION_WEB_AUTH: { CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, }, }, ) assert result.get("type") is FlowResultType.ABORT assert result.get("reason") == "reconfigure_successful" await hass.async_block_till_done() assert entry.title == NAME assert entry.data == { CONF_SITE_ID: SITE_ID, CONF_USERNAME: USERNAME, CONF_PASSWORD: PASSWORD, } assert mock_setup_entry.call_count == 1