diff --git a/.coveragerc b/.coveragerc index 5198d8c34b23..3f19e3a79e24 100644 --- a/.coveragerc +++ b/.coveragerc @@ -839,6 +839,7 @@ omit = homeassistant/components/pvoutput/sensor.py homeassistant/components/pyload/sensor.py homeassistant/components/qbittorrent/sensor.py + homeassistant/components/qbittorrent/client.py homeassistant/components/qnap/sensor.py homeassistant/components/qrcode/image_processing.py homeassistant/components/quantum_gateway/device_tracker.py diff --git a/homeassistant/components/qbittorrent/__init__.py b/homeassistant/components/qbittorrent/__init__.py index a5274f7a5a92..61dc863660ef 100644 --- a/homeassistant/components/qbittorrent/__init__.py +++ b/homeassistant/components/qbittorrent/__init__.py @@ -1 +1,85 @@ """The qbittorrent component.""" +import asyncio +import logging + +from qbittorrent.client import LoginRequired +from requests.exceptions import RequestException + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_per_platform + +from .client import create_client +from .const import DATA_KEY_CLIENT, DATA_KEY_NAME, DOMAIN + +PLATFORMS = ["sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Qbittorrent component.""" + hass.data.setdefault(DOMAIN, {}) + + # Import configuration from sensor platform + config_platform = config_per_platform(config, "sensor") + for p_type, p_config in config_platform: + if p_type != DOMAIN: + continue + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=p_config, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Qbittorrent from a config entry.""" + name = "Qbittorrent" + try: + client = await hass.async_add_executor_job( + create_client, + entry.data[CONF_URL], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + ) + except LoginRequired: + _LOGGER.error("Invalid authentication") + return False + + except RequestException as err: + _LOGGER.error("Connection failed") + raise ConfigEntryNotReady from err + + hass.data[DOMAIN][entry.data[CONF_URL]] = { + DATA_KEY_CLIENT: client, + DATA_KEY_NAME: name, + } + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry): + """Unload Qbittorrent Entry from config_entry.""" + + unload_ok = all( + await asyncio.gather( + *( + hass.config_entries.async_forward_entry_unload(config_entry, platform) + for platform in PLATFORMS + ) + ) + ) + + return unload_ok diff --git a/homeassistant/components/qbittorrent/client.py b/homeassistant/components/qbittorrent/client.py new file mode 100644 index 000000000000..940453ab9c14 --- /dev/null +++ b/homeassistant/components/qbittorrent/client.py @@ -0,0 +1,19 @@ +"""Some common functions used in the qbittorrent components.""" +from qbittorrent.client import Client + + +def get_main_data_client(client: Client): + """Get the main data from the Qtorrent client.""" + return client.sync_main_data() + + +def create_client(url, username, password): + """Create the Qtorrent client.""" + client = Client(url) + client.login(username, password) + return client + + +def retrieve_torrentdata(client: Client, torrentfilter): + """Retrieve torrent data from the Qtorrent client with specific filters.""" + return client.torrents(filter=torrentfilter) diff --git a/homeassistant/components/qbittorrent/config_flow.py b/homeassistant/components/qbittorrent/config_flow.py new file mode 100644 index 000000000000..47cf2a918eeb --- /dev/null +++ b/homeassistant/components/qbittorrent/config_flow.py @@ -0,0 +1,116 @@ +"""Config flow for qBittorrent integration.""" +import logging + +from qbittorrent.client import LoginRequired +from requests.exceptions import RequestException +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME + +from .client import create_client +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass, data): + """Validate user or import input.""" + errors = {} + try: + await hass.async_add_executor_job( + create_client, + data[CONF_URL], + data[CONF_USERNAME], + data[CONF_PASSWORD], + ) + except LoginRequired: + errors["base"] = "invalid_auth" + + except RequestException as err: + errors["base"] = "cannot_connect" + _LOGGER.error("Connection failed - %s", err) + return errors + + +class QBittorrentConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for qbittorrent.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + def __init__(self): + """Initialize qbittorrent config flow.""" + self._data = None + self._title = None + + async def async_step_import(self, device_config): + """Import a configuration.yaml config.""" + data = {} + + data[CONF_URL] = device_config.get(CONF_URL) + data[CONF_USERNAME] = device_config.get(CONF_USERNAME) + data[CONF_PASSWORD] = device_config.get(CONF_PASSWORD) + + errors = await validate_input(self.hass, data) + if not errors: + self._data = data + self._title = data[CONF_URL] + return await self.async_step_import_confirm() + + return self.async_abort(reason=errors["base"]) + + async def async_step_import_confirm(self, user_input=None): + """Confirm the user wants to import the config entry.""" + if user_input is None: + return self.async_show_form( + step_id="import_confirm", + description_placeholders={"id": self._title}, + ) + + await self.async_set_unique_id(self._data[CONF_URL]) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=self._data[CONF_URL], + data={ + CONF_USERNAME: self._data[CONF_USERNAME], + CONF_PASSWORD: self._data[CONF_PASSWORD], + CONF_URL: self._data[CONF_URL], + }, + ) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + await self.async_set_unique_id(user_input[CONF_URL]) + self._abort_if_unique_id_configured() + + _LOGGER.debug( + "Configuring user: %s - Password hidden", user_input[CONF_USERNAME] + ) + + errors = await validate_input(self.hass, user_input) + + if not errors: + return self.async_create_entry( + title=user_input[CONF_URL], + data={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_URL: user_input[CONF_URL], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_URL): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/qbittorrent/const.py b/homeassistant/components/qbittorrent/const.py new file mode 100644 index 000000000000..e61e31368f58 --- /dev/null +++ b/homeassistant/components/qbittorrent/const.py @@ -0,0 +1,25 @@ +"""Added constants for the qbittorrent component.""" + +SENSOR_TYPE_CURRENT_STATUS = "current_status" +SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed" +SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" +SENSOR_TYPE_TOTAL_TORRENTS = "total_torrents" +SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents" +SENSOR_TYPE_INACTIVE_TORRENTS = "inactive_torrents" +SENSOR_TYPE_DOWNLOADING_TORRENTS = "downloading_torrents" +SENSOR_TYPE_SEEDING_TORRENTS = "seeding_torrents" +SENSOR_TYPE_RESUMED_TORRENTS = "resumed_torrents" +SENSOR_TYPE_PAUSED_TORRENTS = "paused_torrents" +SENSOR_TYPE_COMPLETED_TORRENTS = "completed_torrents" + +DEFAULT_NAME = "qbittorrent" +TRIM_SIZE = 35 +CONF_CATEGORIES = "categories" + +DOMAIN = DEFAULT_NAME + +DATA_KEY_CLIENT = "client" +DATA_KEY_NAME = "name" + +SERVICE_ADD_DOWNLOAD = "add_download" +SERVICE_REMOVE_DOWNLOAD = "remove_download" diff --git a/homeassistant/components/qbittorrent/manifest.json b/homeassistant/components/qbittorrent/manifest.json index 241b9a5cff9c..52e4e8064c94 100644 --- a/homeassistant/components/qbittorrent/manifest.json +++ b/homeassistant/components/qbittorrent/manifest.json @@ -1,6 +1,7 @@ { "domain": "qbittorrent", "name": "qBittorrent", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/qbittorrent", "requirements": ["python-qbittorrent==0.4.2"], "codeowners": ["@geoffreylagaisse"], diff --git a/homeassistant/components/qbittorrent/sensor.py b/homeassistant/components/qbittorrent/sensor.py index 6dd52af5631e..1d026eb59bb3 100644 --- a/homeassistant/components/qbittorrent/sensor.py +++ b/homeassistant/components/qbittorrent/sensor.py @@ -3,7 +3,7 @@ import logging -from qbittorrent.client import Client, LoginRequired +from qbittorrent.client import LoginRequired from requests.exceptions import RequestException import voluptuous as vol @@ -20,21 +20,23 @@ DATA_RATE_KIBIBYTES_PER_SECOND, STATE_IDLE, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) - -SENSOR_TYPE_CURRENT_STATUS = "current_status" -SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed" -SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" +from .client import get_main_data_client +from .const import ( + DATA_KEY_CLIENT, + DATA_KEY_NAME, + DOMAIN, + SENSOR_TYPE_CURRENT_STATUS, + SENSOR_TYPE_DOWNLOAD_SPEED, + SENSOR_TYPE_UPLOAD_SPEED, +) -DEFAULT_NAME = "qBittorrent" +_LOGGER = logging.getLogger(__name__) SENSOR_TYPES: tuple[SensorEntityDescription, ...] = ( SensorEntityDescription( - key=SENSOR_TYPE_CURRENT_STATUS, - name="Status", + key=SENSOR_TYPE_CURRENT_STATUS, name="Status", native_unit_of_measurement=None ), SensorEntityDescription( key=SENSOR_TYPE_DOWNLOAD_SPEED, @@ -53,32 +55,27 @@ vol.Required(CONF_URL): cv.url, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_NAME, default=DOMAIN): cv.string, } ) -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the qBittorrent sensors.""" - - try: - client = Client(config[CONF_URL]) - client.login(config[CONF_USERNAME], config[CONF_PASSWORD]) - except LoginRequired: - _LOGGER.error("Invalid authentication") - return - except RequestException as err: - _LOGGER.error("Connection failed") - raise PlatformNotReady from err - - name = config.get(CONF_NAME) - - entities = [ - QBittorrentSensor(description, client, name, LoginRequired) - for description in SENSOR_TYPES +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the qBittorrent sensor.""" + + qbit_data = hass.data[DOMAIN][entry.data[CONF_URL]] + name = qbit_data[DATA_KEY_NAME] + sensors = [ + QBittorrentSensor( + qbit_data[DATA_KEY_CLIENT], + name, + LoginRequired, + entry.entry_id, + sensordiscription, + ) + for sensordiscription in SENSOR_TYPES ] - - add_entities(entities, True) + async_add_entities(sensors, True) def format_speed(speed): @@ -92,30 +89,77 @@ class QBittorrentSensor(SensorEntity): def __init__( self, - description: SensorEntityDescription, qbittorrent_client, client_name, exception, + server_unique_id, + description: SensorEntityDescription, ): """Initialize the qBittorrent sensor.""" self.entity_description = description self.client = qbittorrent_client self._exception = exception - + self._server_unique_id = server_unique_id self._attr_name = f"{client_name} {description.name}" self._attr_available = False - def update(self): + @property + def name(self): + """Return the name of the sensor.""" + return f"{self._attr_name}" + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + return f"{self._server_unique_id}/{self._attr_name}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._attr_native_value + + @property + def available(self): + """Return true if device is available.""" + return self._attr_available + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self.entity_description.native_unit_of_measurement + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return "mdi:cloud-download" + + @property + def device_info(self): + """Return the device information of the entity.""" + return { + "identifiers": {(DOMAIN, self._server_unique_id)}, + "name": DOMAIN, + "model": DOMAIN, + "manufacturer": DOMAIN, + } + + async def async_update(self): """Get the latest data from qBittorrent and updates the state.""" try: - data = self.client.sync_main_data() + data = await self.hass.async_add_executor_job( + get_main_data_client, self.client + ) + if not self._attr_available: + _LOGGER.info("Reconnected with QBittorent server") + self._attr_available = True except RequestException: - _LOGGER.error("Connection lost") - self._attr_available = False - return + if self._attr_available: + _LOGGER.error("Connection lost") + self._attr_available = False except self._exception: _LOGGER.error("Invalid authentication") + self._attr_available = False return if data is None: diff --git a/homeassistant/components/qbittorrent/strings.json b/homeassistant/components/qbittorrent/strings.json new file mode 100644 index 000000000000..24ea1be79e6e --- /dev/null +++ b/homeassistant/components/qbittorrent/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "title": "qBitTorrent", + "description": "[%key:common::config_flow::description::confirm_setup%]", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + "url": "[%key:common::config_flow::data::url%]" + } + }, + "import_confirm": { + "title": "Import qbittorret sensor", + "description": "A {id} sensor has been discovered in configuration.yaml. This flow will allow you to import it into a config entry." + } + }, + "error": { + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "config_cannot_be_imported": "Config from configuration.yaml can not be imported" + } + } + } diff --git a/homeassistant/components/qbittorrent/translations/en.json b/homeassistant/components/qbittorrent/translations/en.json new file mode 100644 index 000000000000..af3f249fc340 --- /dev/null +++ b/homeassistant/components/qbittorrent/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured", + "config_cannot_be_imported": "Config from configuration.yaml can not be imported" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication" + }, + "step": { + "import_confirm": { + "description": "A {id} sensor has been discovered in configuration.yaml. This flow will allow you to import it into a config entry.", + "title": "Import qbittorret sensor" + }, + "user": { + "data": { + "password": "Password", + "url": "URL", + "username": "Username" + }, + "description": "Do you want to start set up?", + "title": "qBitTorrent" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 397194022395..31a92ae84e2e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -228,6 +228,7 @@ "prosegur", "ps4", "pvpc_hourly_pricing", + "qbittorrent", "rachio", "rainforest_eagle", "rainmachine", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8636bf478893..202d9e4c2ea0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1120,6 +1120,9 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 # homeassistant.components.picnic python-picnic-api==1.1.0 +# homeassistant.components.qbittorrent +python-qbittorrent==0.4.2 + # homeassistant.components.smarttub python-smarttub==0.0.27 diff --git a/tests/components/qbittorrent/__init__.py b/tests/components/qbittorrent/__init__.py new file mode 100644 index 000000000000..ceddae42ffed --- /dev/null +++ b/tests/components/qbittorrent/__init__.py @@ -0,0 +1 @@ +"""Tests for the Qbittorrent integration.""" diff --git a/tests/components/qbittorrent/test_config_flow.py b/tests/components/qbittorrent/test_config_flow.py new file mode 100644 index 000000000000..305d6a9d5616 --- /dev/null +++ b/tests/components/qbittorrent/test_config_flow.py @@ -0,0 +1,204 @@ +"""Test the Qbittorrent Flow.""" + +from unittest.mock import MagicMock, patch + +from qbittorrent.client import LoginRequired +from requests.exceptions import RequestException + +from homeassistant import data_entry_flow +from homeassistant.components.qbittorrent.const import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM + +test_url = "http://testurl.org" +test_username = "test-username" +test_password = "test-password" + +config_input = { + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, +} + + +async def test_show_form(hass): + """Test that the form is served with no input.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + + +async def test_invalid_server(hass): + """Test handle invalid credentials.""" + mocked_client = _create_mocked_client(True, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + _flow_next(hass, result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_invalid_credentials(hass): + """Test handle invalid credentials.""" + mocked_client = _create_mocked_client(False, True) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + _flow_next(hass, result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_imported_credentials(hass): + """Test handle immported configuration credentials.""" + + mocked_client = _create_mocked_client(False, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_input, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "import_confirm" + + _flow_next(hass, result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == test_url + assert result["data"][CONF_URL] == test_url + assert result["data"][CONF_USERNAME] == test_username + assert result["data"][CONF_PASSWORD] == test_password + + +async def test_invalid_imported_credentials(hass): + """Test handle invalid imported credentials.""" + mocked_client = _create_mocked_client(False, True) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_input, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_auth" + + +async def test_invalid_imported_server(hass): + """Test handle invalid imported credentials.""" + mocked_client = _create_mocked_client(True, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config_input, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_valid_credentials(hass): + """Test handle valid credentials.""" + mocked_client = _create_mocked_client(False, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + _flow_next(hass, result["flow_id"]) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == test_url + assert result["data"][CONF_URL] == test_url + assert result["data"][CONF_USERNAME] == test_username + assert result["data"][CONF_PASSWORD] == test_password + + +def _create_mocked_client(raise_request_exception=False, raise_login_exception=False): + mocked_client = MagicMock() + if raise_request_exception: + mocked_client.login.side_effect = RequestException("Mocked Exception") + if raise_login_exception: + mocked_client.login.side_effect = LoginRequired() + return mocked_client + + +def _flow_next(hass, flow_id): + return next( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == flow_id + ) diff --git a/tests/components/qbittorrent/test_init.py b/tests/components/qbittorrent/test_init.py new file mode 100644 index 000000000000..bb102424157c --- /dev/null +++ b/tests/components/qbittorrent/test_init.py @@ -0,0 +1,123 @@ +"""Test the Qbittorrent Init.""" +from unittest.mock import MagicMock, patch + +from qbittorrent.client import LoginRequired +from requests.exceptions import RequestException + +from homeassistant import setup +from homeassistant.components import qbittorrent +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_PLATFORM, CONF_URL, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +test_url = "http://testurl.org" +test_username = "test-username" +test_password = "test-password" + +MOCK_ENTRY = MockConfigEntry( + domain=qbittorrent.DOMAIN, + data={ + qbittorrent.CONF_URL: test_url, + qbittorrent.CONF_USERNAME: test_username, + qbittorrent.CONF_PASSWORD: test_password, + }, + unique_id=test_url, +) + + +def _create_mocked_client(raise_request_exception=False, raise_login_exception=False): + mocked_client = MagicMock() + if raise_request_exception: + mocked_client.login.side_effect = RequestException("Mocked Exception") + if raise_login_exception: + mocked_client.login.side_effect = LoginRequired + return mocked_client + + +async def test_import_old_config_sensor(hass: HomeAssistant): + """Test import of old sensor platform config.""" + config = { + "sensor": [ + { + CONF_PLATFORM: qbittorrent.DOMAIN, + CONF_URL: test_url, + CONF_USERNAME: test_username, + CONF_PASSWORD: test_password, + } + ], + } + mocked_client = _create_mocked_client(False, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + with patch("homeassistant.core.ServiceRegistry.async_call", return_value=True): + assert await setup.async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + confflow_entries = hass.config_entries.flow.async_progress(True) + + assert len(confflow_entries) == 1 + + +async def test_import_faulty_config_sensor(hass: HomeAssistant): + """Test import of old sensor platform config.""" + config = { + "sensor": [ + { + CONF_PLATFORM: qbittorrent.DATA_KEY_NAME, + } + ], + } + mocked_client = _create_mocked_client(False, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", + return_value=mocked_client, + ): + with patch("homeassistant.core.ServiceRegistry.async_call", return_value=True): + assert await setup.async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + confflow_entries = hass.config_entries.flow.async_progress(True) + + assert len(confflow_entries) == 0 + + +async def test_unload_entry(hass: HomeAssistant): + """Test removing Qbittorrent client.""" + mocked_client = _create_mocked_client(False, False) + with patch( + "homeassistant.components.qbittorrent.client.Client", return_value=mocked_client + ): + entry = MOCK_ENTRY + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + + +async def test_setup_entry_invalid_imported_server(hass: HomeAssistant): + """Test setup invalid imported server.""" + # Create a mock entry so we don't have to go through config flow + config_entry = MOCK_ENTRY + config_entry.add_to_hass(hass) + mocked_client = _create_mocked_client(True, False) + + with patch( + "homeassistant.components.qbittorrent.client.Client", return_value=mocked_client + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(qbittorrent.DOMAIN)) == 1 + assert config_entry.state is ConfigEntryState.SETUP_RETRY + assert not hass.data.get(qbittorrent.DOMAIN)