From 45262c61145f080673938078aa15a5b31877cfaa Mon Sep 17 00:00:00 2001 From: Michael <35783820+mib1185@users.noreply.github.com> Date: Sun, 26 Mar 2023 21:14:17 +0200 Subject: [PATCH] Implement config flow for nextcloud (#89396) * implement config flow * add tests * fix hassfest and requirements * abort import on connection error * add add_suggested_values_to_schema * mock async_setup_entry * revert code owner change * fix try connect in config flow * add device info * allow multiple instances * fix import in config flow * remove custom scan interval from coordinator * applay suggestions * apply suggestions * take over ownership from @meichthys * cleanup import data before passing to user step * apply suggestions to tests * add untested files to .coveragerc --- .coveragerc | 7 +- CODEOWNERS | 3 +- .../components/nextcloud/__init__.py | 73 ++++++--- .../components/nextcloud/binary_sensor.py | 21 +-- .../components/nextcloud/config_flow.py | 78 +++++++++ .../components/nextcloud/coordinator.py | 11 +- .../components/nextcloud/manifest.json | 3 +- homeassistant/components/nextcloud/sensor.py | 20 +-- .../components/nextcloud/strings.json | 28 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/integrations.json | 2 +- requirements_test_all.txt | 3 + tests/components/nextcloud/__init__.py | 1 + tests/components/nextcloud/conftest.py | 25 +++ .../nextcloud/snapshots/test_config_flow.ambr | 15 ++ .../components/nextcloud/test_config_flow.py | 151 ++++++++++++++++++ 16 files changed, 383 insertions(+), 59 deletions(-) create mode 100644 homeassistant/components/nextcloud/config_flow.py create mode 100644 homeassistant/components/nextcloud/strings.json create mode 100644 tests/components/nextcloud/__init__.py create mode 100644 tests/components/nextcloud/conftest.py create mode 100644 tests/components/nextcloud/snapshots/test_config_flow.ambr create mode 100644 tests/components/nextcloud/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index f3dbc347919..17db4ef9cde 100644 --- a/.coveragerc +++ b/.coveragerc @@ -778,7 +778,12 @@ omit = homeassistant/components/nexia/climate.py homeassistant/components/nexia/entity.py homeassistant/components/nexia/switch.py - homeassistant/components/nextcloud/* + homeassistant/components/nextcloud/__init__.py + homeassistant/components/nextcloud/binary_sensor.py + homeassistant/components/nextcloud/const.py + homeassistant/components/nextcloud/coordinator.py + homeassistant/components/nextcloud/entity.py + homeassistant/components/nextcloud/sensor.py homeassistant/components/nfandroidtv/__init__.py homeassistant/components/nfandroidtv/notify.py homeassistant/components/nibe_heatpump/__init__.py diff --git a/CODEOWNERS b/CODEOWNERS index ff31997ce6f..1acd5f6c9f7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -785,7 +785,8 @@ build.json @home-assistant/supervisor /tests/components/nexia/ @bdraco /homeassistant/components/nextbus/ @vividboarder /tests/components/nextbus/ @vividboarder -/homeassistant/components/nextcloud/ @meichthys +/homeassistant/components/nextcloud/ @mib1185 +/tests/components/nextcloud/ @mib1185 /homeassistant/components/nextdns/ @bieniu /tests/components/nextdns/ @bieniu /homeassistant/components/nfandroidtv/ @tkdrob diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py index 5dffcbf9fba..d2ad3edf1cb 100644 --- a/homeassistant/components/nextcloud/__init__.py +++ b/homeassistant/components/nextcloud/__init__.py @@ -4,6 +4,7 @@ import logging from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_PASSWORD, CONF_SCAN_INTERVAL, @@ -12,42 +13,71 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from .const import DEFAULT_SCAN_INTERVAL, DOMAIN from .coordinator import NextcloudDataUpdateCoordinator _LOGGER = logging.getLogger(__name__) - PLATFORMS = (Platform.SENSOR, Platform.BINARY_SENSOR) # Validate user configuration CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_URL): cv.url, - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Optional( - CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL - ): cv.time_period, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional( + CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL + ): cv.time_period, + }, + ) + }, + ), extra=vol.ALLOW_EXTRA, ) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Nextcloud integration.""" - conf = config[DOMAIN] + if DOMAIN in config: + async_create_issue( + hass, + DOMAIN, + "deprecated_yaml", + breaks_in_ha_version="2023.6.0", + is_fixable=False, + severity=IssueSeverity.WARNING, + translation_key="deprecated_yaml", + ) + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up the Nextcloud integration.""" + + def _connect_nc(): + return NextcloudMonitor( + entry.data[CONF_URL], entry.data[CONF_USERNAME], entry.data[CONF_PASSWORD] + ) try: - ncm = await hass.async_add_executor_job( - NextcloudMonitor, conf[CONF_URL], conf[CONF_USERNAME], conf[CONF_PASSWORD] - ) + ncm = await hass.async_add_executor_job(_connect_nc) except NextcloudMonitorError: _LOGGER.error("Nextcloud setup failed - Check configuration") return False @@ -55,13 +85,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: coordinator = NextcloudDataUpdateCoordinator( hass, ncm, - conf, + entry, ) - hass.data[DOMAIN] = coordinator + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator await coordinator.async_config_entry_first_refresh() - for platform in PLATFORMS: - discovery.load_platform(hass, platform, DOMAIN, {}, config) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py index 52ddb660071..0d960bea8ef 100644 --- a/homeassistant/components/nextcloud/binary_sensor.py +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -2,9 +2,9 @@ from __future__ import annotations from homeassistant.components.binary_sensor import BinarySensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -18,24 +18,17 @@ BINARY_SENSORS = ( ) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: - """Set up the Nextcloud sensors.""" - if discovery_info is None: - return - coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN] - - add_entities( + """Set up the Nextcloud binary sensors.""" + coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( [ NextcloudBinarySensor(coordinator, name) for name in coordinator.data if name in BINARY_SENSORS - ], - True, + ] ) diff --git a/homeassistant/components/nextcloud/config_flow.py b/homeassistant/components/nextcloud/config_flow.py new file mode 100644 index 00000000000..e297a6893a7 --- /dev/null +++ b/homeassistant/components/nextcloud/config_flow.py @@ -0,0 +1,78 @@ +"""Config flow to configure the Nextcloud integration.""" +from __future__ import annotations + +import logging +from typing import Any + +from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError +import voluptuous as vol + +from homeassistant.config_entries import ConfigFlow +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN + +DATA_SCHEMA_USER = vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) +_LOGGER = logging.getLogger(__name__) + + +class NextcloudConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a Nextcloud config flow.""" + + VERSION = 1 + + def _try_connect_nc(self, user_input: dict) -> NextcloudMonitor: + """Try to connect to nextcloud server.""" + return NextcloudMonitor( + user_input[CONF_URL], + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + ) + + async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: + """Handle a flow initiated by configuration file.""" + self._async_abort_entries_match({CONF_URL: user_input.get(CONF_URL)}) + try: + await self.hass.async_add_executor_job(self._try_connect_nc, user_input) + except NextcloudMonitorError: + _LOGGER.error( + "Connection error during import of yaml configuration, import aborted" + ) + return self.async_abort(reason="connection_error_during_import") + return await self.async_step_user( + { + CONF_URL: user_input[CONF_URL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_USERNAME: user_input[CONF_USERNAME], + } + ) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initialized by the user.""" + errors = {} + + if user_input is not None: + self._async_abort_entries_match({CONF_URL: user_input.get(CONF_URL)}) + try: + await self.hass.async_add_executor_job(self._try_connect_nc, user_input) + except NextcloudMonitorError: + errors["base"] = "connection_error" + else: + return self.async_create_entry( + title=user_input[CONF_URL], + data=user_input, + ) + + data_schema = self.add_suggested_values_to_schema(DATA_SCHEMA_USER, user_input) + return self.async_show_form( + step_id="user", data_schema=data_schema, errors=errors + ) diff --git a/homeassistant/components/nextcloud/coordinator.py b/homeassistant/components/nextcloud/coordinator.py index 07dc76d41dd..73a07a77e23 100644 --- a/homeassistant/components/nextcloud/coordinator.py +++ b/homeassistant/components/nextcloud/coordinator.py @@ -5,9 +5,9 @@ from typing import Any from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError -from homeassistant.const import CONF_SCAN_INTERVAL, CONF_URL +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant -from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DEFAULT_SCAN_INTERVAL, DOMAIN @@ -19,18 +19,17 @@ class NextcloudDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]): """Nextcloud data update coordinator.""" def __init__( - self, hass: HomeAssistant, ncm: NextcloudMonitor, config: ConfigType + self, hass: HomeAssistant, ncm: NextcloudMonitor, entry: ConfigEntry ) -> None: """Initialize the Nextcloud coordinator.""" - self.config = config self.ncm = ncm - self.url = config[CONF_URL] + self.url = entry.data[CONF_URL] super().__init__( hass, _LOGGER, name=self.url, - update_interval=config.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL), + update_interval=DEFAULT_SCAN_INTERVAL, ) # Use recursion to create list of sensors & values based on nextcloud api data diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json index 366c6eeb564..72e992277c6 100644 --- a/homeassistant/components/nextcloud/manifest.json +++ b/homeassistant/components/nextcloud/manifest.json @@ -1,7 +1,8 @@ { "domain": "nextcloud", "name": "Nextcloud", - "codeowners": ["@meichthys"], + "codeowners": ["@mib1185"], + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/nextcloud", "iot_class": "cloud_polling", "requirements": ["nextcloudmonitor==1.1.0"] diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py index 459f22d30eb..eb6043e4bc6 100644 --- a/homeassistant/components/nextcloud/sensor.py +++ b/homeassistant/components/nextcloud/sensor.py @@ -2,9 +2,10 @@ from __future__ import annotations from homeassistant.components.sensor import SensorEntity +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType +from homeassistant.helpers.typing import StateType from .const import DOMAIN from .coordinator import NextcloudDataUpdateCoordinator @@ -57,24 +58,17 @@ SENSORS = ( ) -def setup_platform( - hass: HomeAssistant, - config: ConfigType, - add_entities: AddEntitiesCallback, - discovery_info: DiscoveryInfoType | None = None, +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback ) -> None: """Set up the Nextcloud sensors.""" - if discovery_info is None: - return - coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN] - - add_entities( + coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + async_add_entities( [ NextcloudSensor(coordinator, name) for name in coordinator.data if name in SENSORS - ], - True, + ] ) diff --git a/homeassistant/components/nextcloud/strings.json b/homeassistant/components/nextcloud/strings.json new file mode 100644 index 00000000000..9ae7ed24a60 --- /dev/null +++ b/homeassistant/components/nextcloud/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "flow_title": "Nextcloud", + "step": { + "user": { + "description": "Enter your Nextcloud information.", + "data": { + "url": "[%key:common::config_flow::data::url%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "connection_error_during_import": "Connection error occured during yaml configuration import" + }, + "error": { + "connection_error": "[%key:common::config_flow::error::cannot_connect%]" + } + }, + "issues": { + "deprecated_yaml": { + "title": "The Netxcloud YAML configuration has been deprecated", + "description": "Configuring Netxcloud using YAML has been deprecated.\n\nYour existing YAML configuration has been imported into the UI automatically.\n\nRemove the `nextcloud` YAML configuration from your configuration.yaml file and restart Home Assistant to fix this issue." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 6656972f8b0..37480904f9e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -282,6 +282,7 @@ FLOWS = { "netatmo", "netgear", "nexia", + "nextcloud", "nextdns", "nfandroidtv", "nibe_heatpump", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 843b8ed006d..7340980f137 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -3616,7 +3616,7 @@ "nextcloud": { "name": "Nextcloud", "integration_type": "hub", - "config_flow": false, + "config_flow": true, "iot_class": "cloud_polling" }, "nextdns": { diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2cdff72fc82..6573d81e550 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -887,6 +887,9 @@ nettigo-air-monitor==2.1.0 # homeassistant.components.nexia nexia==2.0.6 +# homeassistant.components.nextcloud +nextcloudmonitor==1.1.0 + # homeassistant.components.discord nextcord==2.0.0a8 diff --git a/tests/components/nextcloud/__init__.py b/tests/components/nextcloud/__init__.py new file mode 100644 index 00000000000..e2102ed8c25 --- /dev/null +++ b/tests/components/nextcloud/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nextcloud integration.""" diff --git a/tests/components/nextcloud/conftest.py b/tests/components/nextcloud/conftest.py new file mode 100644 index 00000000000..0ea281abb49 --- /dev/null +++ b/tests/components/nextcloud/conftest.py @@ -0,0 +1,25 @@ +"""Fixtrues for the Nextcloud integration tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, Mock, patch + +import pytest + + +@pytest.fixture +def mock_nextcloud_monitor() -> Mock: + """Mock of NextcloudMonitor.""" + ncm = Mock( + update=Mock(return_value=True), + ) + + return ncm + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.nextcloud.async_setup_entry", return_value=True + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/nextcloud/snapshots/test_config_flow.ambr b/tests/components/nextcloud/snapshots/test_config_flow.ambr new file mode 100644 index 00000000000..0c9df1238cf --- /dev/null +++ b/tests/components/nextcloud/snapshots/test_config_flow.ambr @@ -0,0 +1,15 @@ +# serializer version: 1 +# name: test_import + dict({ + 'password': 'nc_pass', + 'url': 'nc_url', + 'username': 'nc_user', + }) +# --- +# name: test_user_create_entry + dict({ + 'password': 'nc_pass', + 'url': 'nc_url', + 'username': 'nc_user', + }) +# --- diff --git a/tests/components/nextcloud/test_config_flow.py b/tests/components/nextcloud/test_config_flow.py new file mode 100644 index 00000000000..118d8fef0da --- /dev/null +++ b/tests/components/nextcloud/test_config_flow.py @@ -0,0 +1,151 @@ +"""Tests for the Nextcloud config flow.""" +from unittest.mock import Mock, patch + +from nextcloudmonitor import NextcloudMonitorError +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.nextcloud import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + +from tests.common import MockConfigEntry + +pytestmark = pytest.mark.usefixtures("mock_setup_entry") + +VALID_CONFIG = {CONF_URL: "nc_url", CONF_USERNAME: "nc_user", CONF_PASSWORD: "nc_pass"} + + +async def test_user_create_entry( + hass: HomeAssistant, mock_nextcloud_monitor: Mock, snapshot: SnapshotAssertion +) -> None: + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorError, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "connection_error"} + + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + return_value=mock_nextcloud_monitor, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "nc_url" + assert result["data"] == snapshot + + +async def test_user_already_configured( + hass: HomeAssistant, mock_nextcloud_monitor: Mock +) -> None: + """Test that errors are shown when duplicates are added.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="nc_url", + unique_id="nc_url", + data=VALID_CONFIG, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + return_value=mock_nextcloud_monitor, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import( + hass: HomeAssistant, mock_nextcloud_monitor: Mock, snapshot: SnapshotAssertion +) -> None: + """Test that the import step works.""" + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + return_value=mock_nextcloud_monitor, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + assert result["type"] == FlowResultType.CREATE_ENTRY + assert result["title"] == "nc_url" + assert result["data"] == snapshot + + +async def test_import_already_configured( + hass: HomeAssistant, mock_nextcloud_monitor: Mock +) -> None: + """Test that import step is aborted when duplicates are added.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="nc_url", + unique_id="nc_url", + data=VALID_CONFIG, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + return_value=mock_nextcloud_monitor, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "already_configured" + + +async def test_import_connection_error(hass: HomeAssistant) -> None: + """Test that import step is aborted on connection error.""" + with patch( + "homeassistant.components.nextcloud.config_flow.NextcloudMonitor", + side_effect=NextcloudMonitorError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=VALID_CONFIG, + ) + await hass.async_block_till_done() + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "connection_error_during_import"