diff --git a/homeassistant/components/owntracks/__init__.py b/homeassistant/components/owntracks/__init__.py index 7e65ff3d51d..d30e667f368 100644 --- a/homeassistant/components/owntracks/__init__.py +++ b/homeassistant/components/owntracks/__init__.py @@ -14,12 +14,12 @@ from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.setup import async_when_setup +from .const import DOMAIN from .config_flow import CONF_SECRET from .messages import async_handle_message _LOGGER = logging.getLogger(__name__) -DOMAIN = "owntracks" CONF_MAX_GPS_ACCURACY = "max_gps_accuracy" CONF_WAYPOINT_IMPORT = "waypoints" CONF_WAYPOINT_WHITELIST = "waypoint_whitelist" diff --git a/homeassistant/components/owntracks/config_flow.py b/homeassistant/components/owntracks/config_flow.py index 343a6d90b52..a59cd869c74 100644 --- a/homeassistant/components/owntracks/config_flow.py +++ b/homeassistant/components/owntracks/config_flow.py @@ -3,6 +3,8 @@ from homeassistant import config_entries from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.auth.util import generate_secret +from .const import DOMAIN # noqa pylint: disable=unused-import + CONF_SECRET = "secret" CONF_CLOUDHOOK = "cloudhook" @@ -17,8 +19,7 @@ def supports_encryption(): return False -@config_entries.HANDLERS.register("owntracks") -class OwnTracksFlow(config_entries.ConfigFlow): +class OwnTracksFlow(config_entries.ConfigFlow, domain=DOMAIN): """Set up OwnTracks.""" VERSION = 1 @@ -36,14 +37,9 @@ class OwnTracksFlow(config_entries.ConfigFlow): secret = generate_secret(16) if supports_encryption(): - secret_desc = ( - "The encryption key is {} " - "(on Android under preferences -> advanced)".format(secret) - ) + secret_desc = f"The encryption key is {secret} (on Android under preferences -> advanced)" else: - secret_desc = ( - "Encryption is not supported because libsodium is not " "installed." - ) + secret_desc = "Encryption is not supported because nacl is not installed." return self.async_create_entry( title="OwnTracks", @@ -55,8 +51,7 @@ class OwnTracksFlow(config_entries.ConfigFlow): description_placeholders={ "secret": secret_desc, "webhook_url": webhook_url, - "android_url": "https://play.google.com/store/apps/details?" - "id=org.owntracks.android", + "android_url": "https://play.google.com/store/apps/details?id=org.owntracks.android", "ios_url": "https://itunes.apple.com/us/app/owntracks/id692424691?mt=8", "docs_url": "https://www.home-assistant.io/integrations/owntracks/", }, @@ -64,6 +59,8 @@ class OwnTracksFlow(config_entries.ConfigFlow): async def async_step_import(self, user_input): """Import a config flow from configuration.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") webhook_id, _webhook_url, cloudhook = await self._get_webhook_id() secret = generate_secret(16) return self.async_create_entry( diff --git a/homeassistant/components/owntracks/const.py b/homeassistant/components/owntracks/const.py new file mode 100644 index 00000000000..c7caa201ca3 --- /dev/null +++ b/homeassistant/components/owntracks/const.py @@ -0,0 +1,3 @@ +"""Constants for OwnTracks.""" + +DOMAIN = "owntracks" diff --git a/homeassistant/components/owntracks/messages.py b/homeassistant/components/owntracks/messages.py index 465d2762f74..0cb65c774b5 100644 --- a/homeassistant/components/owntracks/messages.py +++ b/homeassistant/components/owntracks/messages.py @@ -107,7 +107,7 @@ def _decrypt_payload(secret, topic, ciphertext): try: keylen, decrypt = get_cipher() except OSError: - _LOGGER.warning("Ignoring encrypted payload because libsodium not installed") + _LOGGER.warning("Ignoring encrypted payload because nacl not installed") return None if isinstance(secret, dict): @@ -117,8 +117,7 @@ def _decrypt_payload(secret, topic, ciphertext): if key is None: _LOGGER.warning( - "Ignoring encrypted payload because no decryption key known " - "for topic %s", + "Ignoring encrypted payload because no decryption key known for topic %s", topic, ) return None @@ -134,8 +133,7 @@ def _decrypt_payload(secret, topic, ciphertext): return message except ValueError: _LOGGER.warning( - "Ignoring encrypted payload because unable to decrypt using " - "key for topic %s", + "Ignoring encrypted payload because unable to decrypt using key for topic %s", topic, ) return None diff --git a/tests/components/owntracks/test_config_flow.py b/tests/components/owntracks/test_config_flow.py index c4e2a54f69a..54e33a1ab61 100644 --- a/tests/components/owntracks/test_config_flow.py +++ b/tests/components/owntracks/test_config_flow.py @@ -1,25 +1,135 @@ """Tests for OwnTracks config flow.""" -from unittest.mock import patch +from unittest.mock import Mock, patch +import pytest +from homeassistant import data_entry_flow +from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.components.owntracks import config_flow +from homeassistant.components.owntracks.config_flow import CONF_CLOUDHOOK, CONF_SECRET +from homeassistant.components.owntracks.const import DOMAIN from homeassistant.setup import async_setup_component -from tests.common import mock_coro + +from tests.common import mock_coro, MockConfigEntry -async def test_config_flow_import(hass): +CONF_WEBHOOK_URL = "webhook_url" + +BASE_URL = "http://example.com" +CLOUDHOOK = False +SECRET = "secret" +WEBHOOK_ID = "webhook_id" +WEBHOOK_URL = f"{BASE_URL}/api/webhook/webhook_id" + + +@pytest.fixture(name="webhook_id") +def mock_webhook_id(): + """Mock webhook_id.""" + with patch( + "homeassistant.components.webhook.async_generate_id", return_value=WEBHOOK_ID + ): + yield + + +@pytest.fixture(name="secret") +def mock_secret(): + """Mock secret.""" + with patch("binascii.hexlify", return_value=str.encode(SECRET)): + yield + + +@pytest.fixture(name="not_supports_encryption") +def mock_not_supports_encryption(): + """Mock non successful nacl import.""" + with patch( + "homeassistant.components.owntracks.config_flow.supports_encryption", + return_value=False, + ): + yield + + +def init_config_flow(hass): + """Init a configuration flow.""" + hass.config.api = Mock(base_url=BASE_URL) + flow = config_flow.OwnTracksFlow() + flow.hass = hass + return flow + + +async def test_user(hass, webhook_id, secret): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"][CONF_WEBHOOK_URL] == WEBHOOK_URL + + +async def test_import(hass, webhook_id, secret): + """Test import step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "OwnTracks" + assert result["data"][CONF_WEBHOOK_ID] == WEBHOOK_ID + assert result["data"][CONF_SECRET] == SECRET + assert result["data"][CONF_CLOUDHOOK] == CLOUDHOOK + assert result["description_placeholders"] is None + + +async def test_import_setup(hass): """Test that we automatically create a config flow.""" - assert not hass.config_entries.async_entries("owntracks") - assert await async_setup_component(hass, "owntracks", {"owntracks": {}}) + assert not hass.config_entries.async_entries(DOMAIN) + assert await async_setup_component(hass, DOMAIN, {"owntracks": {}}) await hass.async_block_till_done() - assert hass.config_entries.async_entries("owntracks") + assert hass.config_entries.async_entries(DOMAIN) -async def test_config_flow_unload(hass): +async def test_abort_if_already_setup(hass): + """Test that we can't add more than one instance.""" + flow = init_config_flow(hass) + + MockConfigEntry(domain=DOMAIN, data={}).add_to_hass(hass) + assert hass.config_entries.async_entries(DOMAIN) + + # Should fail, already setup (import) + result = await flow.async_step_import({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + # Should fail, already setup (flow) + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "one_instance_allowed" + + +async def test_user_not_supports_encryption(hass, not_supports_encryption): + """Test user step.""" + flow = init_config_flow(hass) + + result = await flow.async_step_user({}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + result["description_placeholders"]["secret"] + == "Encryption is not supported because nacl is not installed." + ) + + +async def test_unload(hass): """Test unloading a config flow.""" with patch( "homeassistant.config_entries.ConfigEntries" ".async_forward_entry_setup" ) as mock_forward: result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "import"}, data={} + DOMAIN, context={"source": "import"}, data={} ) assert len(mock_forward.mock_calls) == 1 @@ -51,7 +161,7 @@ async def test_with_cloud_sub(hass): return_value=mock_coro("https://hooks.nabu.casa/ABCD"), ): result = await hass.config_entries.flow.async_init( - "owntracks", context={"source": "user"}, data={} + DOMAIN, context={"source": "user"}, data={} ) entry = result["result"]