"""Tests for the Elmax config flow.""" from unittest.mock import patch from elmax_api.exceptions import ElmaxBadLoginError, ElmaxBadPinError, ElmaxNetworkError from homeassistant import config_entries from homeassistant.components import zeroconf from homeassistant.components.elmax.const import ( CONF_ELMAX_MODE, CONF_ELMAX_MODE_CLOUD, CONF_ELMAX_MODE_DIRECT, CONF_ELMAX_MODE_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL, CONF_ELMAX_MODE_DIRECT_SSL_CERT, CONF_ELMAX_PANEL_ID, CONF_ELMAX_PANEL_NAME, CONF_ELMAX_PANEL_PIN, CONF_ELMAX_PASSWORD, CONF_ELMAX_USERNAME, DOMAIN, ) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from . import ( MOCK_DIRECT_CERT, MOCK_DIRECT_HOST, MOCK_DIRECT_HOST_CHANGED, MOCK_DIRECT_PORT, MOCK_DIRECT_SSL, MOCK_PANEL_ID, MOCK_PANEL_NAME, MOCK_PANEL_PIN, MOCK_PASSWORD, MOCK_USERNAME, MOCK_WRONG_PANEL_PIN, ) from tests.common import MockConfigEntry MOCK_ZEROCONF_DISCOVERY_INFO = zeroconf.ZeroconfServiceInfo( ip_address=MOCK_DIRECT_HOST, ip_addresses=[MOCK_DIRECT_HOST], hostname="VideoBox.local", name="VideoBox", port=443, properties={ "idl": MOCK_PANEL_ID, "idr": MOCK_PANEL_ID, "v1": "PHANTOM64PRO_GSM 11.9.844", "v2": "4.9.13", }, type="_elmax-ssl._tcp", ) MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO = zeroconf.ZeroconfServiceInfo( ip_address=MOCK_DIRECT_HOST_CHANGED, ip_addresses=[MOCK_DIRECT_HOST_CHANGED], hostname="VideoBox.local", name="VideoBox", port=443, properties={ "idl": MOCK_PANEL_ID, "idr": MOCK_PANEL_ID, "v1": "PHANTOM64PRO_GSM 11.9.844", "v2": "4.9.13", }, type="_elmax-ssl._tcp", ) MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED = zeroconf.ZeroconfServiceInfo( ip_address=MOCK_DIRECT_HOST, ip_addresses=[MOCK_DIRECT_HOST], hostname="VideoBox.local", name="VideoBox", port=443, properties={ "idl": MOCK_PANEL_ID, "idr": MOCK_PANEL_ID, "v1": "PHANTOM64PRO_GSM 11.9.844", }, type="_elmax-ssl._tcp", ) CONF_POLLING = "polling" async def test_show_menu(hass: HomeAssistant) -> None: """Test that the form is served with no input.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) assert result["type"] is FlowResultType.MENU assert result["step_id"] == "choose_mode" async def test_direct_setup(hass: HomeAssistant) -> None: """Test the standard direct setup case.""" show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.elmax.async_setup_entry", return_value=True, ): set_mode_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT}, ) result = await hass.config_entries.flow.async_configure( set_mode_result["flow_id"], { CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY async def test_direct_show_form(hass: HomeAssistant) -> None: """Test the standard direct show form case.""" show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.elmax.async_setup_entry", return_value=True, ): set_mode_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], ) result = await hass.config_entries.flow.async_configure( set_mode_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT} ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == CONF_ELMAX_MODE_DIRECT assert result["errors"] is None async def test_cloud_setup(hass: HomeAssistant) -> None: """Test the standard cloud setup case.""" # Setup once. show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) with patch( "homeassistant.components.elmax.async_setup_entry", return_value=True, ): show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) result = await hass.config_entries.flow.async_configure( login_result["flow_id"], { CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_form_setup_api_not_supported(hass: HomeAssistant) -> None: """Test the zeroconf setup case.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO_NOT_SUPPORTED, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "not_supported" async def test_zeroconf_discovery(hass: HomeAssistant) -> None: """Test discovery of Elmax local api panel.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" assert result["errors"] is None async def test_zeroconf_setup_show_form(hass: HomeAssistant) -> None: """Test discovery shows a form when activated.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], ) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "zeroconf_setup" async def test_zeroconf_setup(hass: HomeAssistant) -> None: """Test the successful creation of config entry via discovery flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) result = await hass.config_entries.flow.async_configure( result["flow_id"], { CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, }, ) await hass.async_block_till_done() assert result["type"] is FlowResultType.CREATE_ENTRY async def test_zeroconf_already_configured(hass: HomeAssistant) -> None: """Ensure local discovery aborts when same panel is already added to ha.""" MockConfigEntry( domain=DOMAIN, title=f"Elmax Direct ({MOCK_PANEL_ID})", data={ CONF_ELMAX_MODE: CONF_ELMAX_MODE_DIRECT, CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_MODE_DIRECT_SSL_CERT: MOCK_DIRECT_CERT, }, unique_id=MOCK_PANEL_ID, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_INFO, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_zeroconf_panel_changed_ip(hass: HomeAssistant) -> None: """Ensure local discovery updates the panel data when a the panel changes its IP.""" # Simulate an entry already exists for ip MOCK_DIRECT_HOST. config_entry = MockConfigEntry( domain=DOMAIN, title=f"Elmax Direct ({MOCK_PANEL_ID})", data={ CONF_ELMAX_MODE: CONF_ELMAX_MODE_DIRECT, CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_MODE_DIRECT_SSL_CERT: MOCK_DIRECT_CERT, }, unique_id=MOCK_PANEL_ID, ) config_entry.add_to_hass(hass) # Simulate a MDNS discovery finds the same panel with a different IP (MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO). result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_ZEROCONF}, data=MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO, ) # Expect we abort the configuration as "already configured" assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" # Expect the panel ip has been updated. assert ( hass.config_entries.async_get_entry(config_entry.entry_id).data[ CONF_ELMAX_MODE_DIRECT_HOST ] == MOCK_ZEROCONF_DISCOVERY_CHANGED_INFO.host ) async def test_one_config_allowed_cloud(hass: HomeAssistant) -> None: """Test that only one Elmax configuration is allowed for each panel.""" MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ).add_to_hass(hass) # Attempt to add another instance of the integration for the very same panel, it must fail. show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) user_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( user_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) result = await hass.config_entries.flow.async_configure( login_result["flow_id"], { CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" async def test_cloud_invalid_credentials(hass: HomeAssistant) -> None: """Test that invalid credentials throws an error.""" with patch( "elmax_api.http.Elmax.login", side_effect=ElmaxBadLoginError(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: "wrong_user_name@email.com", CONF_ELMAX_PASSWORD: "incorrect_password", }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "invalid_auth"} async def test_cloud_connection_error(hass: HomeAssistant) -> None: """Test other than invalid credentials throws an error.""" with patch( "elmax_api.http.Elmax.login", side_effect=ElmaxNetworkError(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "network_error"} async def test_direct_connection_error(hass: HomeAssistant) -> None: """Test network error while dealing with direct panel APIs.""" with patch( "elmax_api.http.ElmaxLocal.login", side_effect=ElmaxNetworkError(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) set_mode_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT}, ) result = await hass.config_entries.flow.async_configure( set_mode_result["flow_id"], { CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "network_error"} async def test_direct_wrong_panel_code(hass: HomeAssistant) -> None: """Test wrong code being specified while dealing with direct panel APIs.""" with patch( "elmax_api.http.ElmaxLocal.login", side_effect=ElmaxBadLoginError(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) set_mode_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_DIRECT}, ) result = await hass.config_entries.flow.async_configure( set_mode_result["flow_id"], { CONF_ELMAX_MODE_DIRECT_HOST: MOCK_DIRECT_HOST, CONF_ELMAX_MODE_DIRECT_PORT: MOCK_DIRECT_PORT, CONF_ELMAX_MODE_DIRECT_SSL: MOCK_DIRECT_SSL, CONF_ELMAX_PANEL_PIN: MOCK_WRONG_PANEL_PIN, }, ) assert result["step_id"] == CONF_ELMAX_MODE_DIRECT assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"} async def test_unhandled_error(hass: HomeAssistant) -> None: """Test unhandled exceptions.""" with patch( "elmax_api.http.Elmax.get_panel_status", side_effect=Exception(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) result = await hass.config_entries.flow.async_configure( login_result["flow_id"], { CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) assert result["step_id"] == "panels" assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "unknown"} async def test_invalid_pin(hass: HomeAssistant) -> None: """Test error is thrown when a wrong pin is used to pair a panel.""" # Simulate bad pin response. with patch( "elmax_api.http.Elmax.get_panel_status", side_effect=ElmaxBadPinError(), ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) result = await hass.config_entries.flow.async_configure( login_result["flow_id"], { CONF_ELMAX_PANEL_NAME: MOCK_PANEL_NAME, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, ) assert result["step_id"] == "panels" assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} async def test_no_online_panel(hass: HomeAssistant) -> None: """Test no-online panel is available.""" # Simulate low-level api returns no panels. with patch( "elmax_api.http.Elmax.list_control_panels", return_value=[], ): show_form_result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) show_form_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], {"next_step_id": CONF_ELMAX_MODE_CLOUD}, ) login_result = await hass.config_entries.flow.async_configure( show_form_result["flow_id"], { CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert login_result["step_id"] == CONF_ELMAX_MODE_CLOUD assert login_result["type"] is FlowResultType.FORM assert login_result["errors"] == {"base": "no_panel_online"} async def test_show_reauth(hass: HomeAssistant) -> None: """Test that the reauth form shows.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ) entry.add_to_hass(hass) result = await entry.start_reauth_flow(hass) assert result["type"] is FlowResultType.FORM assert result["step_id"] == "reauth_confirm" async def test_reauth_flow(hass: HomeAssistant) -> None: """Test that the reauth flow works.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ) entry.add_to_hass(hass) # Trigger reauth reauth_result = await entry.start_reauth_flow(hass) with patch( "homeassistant.components.elmax.async_setup_entry", return_value=True, ): result = await hass.config_entries.flow.async_configure( reauth_result["flow_id"], { CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert result["type"] is FlowResultType.ABORT await hass.async_block_till_done() assert result["reason"] == "reauth_successful" async def test_reauth_panel_disappeared(hass: HomeAssistant) -> None: """Test that the case where panel is no longer associated with the user.""" # Simulate a first setup entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ) entry.add_to_hass(hass) # Trigger reauth reauth_result = await entry.start_reauth_flow(hass) with patch( "elmax_api.http.Elmax.list_control_panels", return_value=[], ): result = await hass.config_entries.flow.async_configure( reauth_result["flow_id"], { CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "reauth_panel_disappeared"} async def test_reauth_invalid_pin(hass: HomeAssistant) -> None: """Test that the case where panel is no longer associated with the user.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ) entry.add_to_hass(hass) # Trigger reauth reauth_result = await entry.start_reauth_flow(hass) with patch( "elmax_api.http.Elmax.get_panel_status", side_effect=ElmaxBadPinError(), ): result = await hass.config_entries.flow.async_configure( reauth_result["flow_id"], { CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_pin"} async def test_reauth_bad_login(hass: HomeAssistant) -> None: """Test bad login attempt at reauth time.""" entry = MockConfigEntry( domain=DOMAIN, data={ CONF_ELMAX_PANEL_ID: MOCK_PANEL_ID, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, }, unique_id=MOCK_PANEL_ID, ) entry.add_to_hass(hass) # Trigger reauth reauth_result = await entry.start_reauth_flow(hass) with patch( "elmax_api.http.Elmax.login", side_effect=ElmaxBadLoginError(), ): result = await hass.config_entries.flow.async_configure( reauth_result["flow_id"], { CONF_ELMAX_PANEL_PIN: MOCK_PANEL_PIN, CONF_ELMAX_USERNAME: MOCK_USERNAME, CONF_ELMAX_PASSWORD: MOCK_PASSWORD, }, ) assert result["step_id"] == "reauth_confirm" assert result["type"] is FlowResultType.FORM assert result["errors"] == {"base": "invalid_auth"}