From 543064d7b7740dea10c8f7a5a06303c100f69866 Mon Sep 17 00:00:00 2001 From: G Johansson Date: Thu, 20 Jan 2022 21:45:58 +0100 Subject: [PATCH] Implement config flow dnsip (#62843) --- .coveragerc | 1 + CODEOWNERS | 2 + homeassistant/components/dnsip/__init__.py | 29 ++ homeassistant/components/dnsip/config_flow.py | 170 ++++++++++ homeassistant/components/dnsip/const.py | 17 + homeassistant/components/dnsip/manifest.json | 3 +- homeassistant/components/dnsip/sensor.py | 100 ++++-- homeassistant/components/dnsip/strings.json | 27 ++ .../components/dnsip/translations/en.json | 27 ++ homeassistant/generated/config_flows.py | 1 + tests/components/dnsip/__init__.py | 1 + tests/components/dnsip/test_config_flow.py | 290 ++++++++++++++++++ 12 files changed, 644 insertions(+), 24 deletions(-) create mode 100644 homeassistant/components/dnsip/config_flow.py create mode 100644 homeassistant/components/dnsip/const.py create mode 100644 homeassistant/components/dnsip/strings.json create mode 100644 homeassistant/components/dnsip/translations/en.json create mode 100644 tests/components/dnsip/__init__.py create mode 100644 tests/components/dnsip/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 71ed54e34e9..1ffb47ec832 100644 --- a/.coveragerc +++ b/.coveragerc @@ -217,6 +217,7 @@ omit = homeassistant/components/dlib_face_detect/image_processing.py homeassistant/components/dlib_face_identify/image_processing.py homeassistant/components/dlink/switch.py + homeassistant/components/dnsip/__init__.py homeassistant/components/dnsip/sensor.py homeassistant/components/dominos/* homeassistant/components/doods/* diff --git a/CODEOWNERS b/CODEOWNERS index bb126e04e6b..df77ea8d5b0 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -214,6 +214,8 @@ homeassistant/components/digital_ocean/* @fabaff homeassistant/components/discogs/* @thibmaek homeassistant/components/dlna_dmr/* @StevenLooman @chishm tests/components/dlna_dmr/* @StevenLooman @chishm +homeassistant/components/dnsip/* @gjohansson-ST +tests/components/dnsip/* @gjohansson-ST homeassistant/components/doorbird/* @oblogic7 @bdraco @flacjacket tests/components/doorbird/* @oblogic7 @bdraco @flacjacket homeassistant/components/dsmr/* @Robbie1221 @frenck diff --git a/homeassistant/components/dnsip/__init__.py b/homeassistant/components/dnsip/__init__.py index 603e8403e74..2f20f18580e 100644 --- a/homeassistant/components/dnsip/__init__.py +++ b/homeassistant/components/dnsip/__init__.py @@ -1 +1,30 @@ """The dnsip component.""" +from __future__ import annotations + +import logging + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +from .const import PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up DNS IP from a config entry.""" + + hass.config_entries.async_setup_platforms(entry, PLATFORMS) + entry.async_on_unload(entry.add_update_listener(update_listener)) + return True + + +async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload dnsip config entry.""" + + return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) diff --git a/homeassistant/components/dnsip/config_flow.py b/homeassistant/components/dnsip/config_flow.py new file mode 100644 index 00000000000..e47dc67d58d --- /dev/null +++ b/homeassistant/components/dnsip/config_flow.py @@ -0,0 +1,170 @@ +"""Adds config flow for dnsip integration.""" +from __future__ import annotations + +import asyncio +import contextlib +from typing import Any + +import aiodns +from aiodns.error import DNSError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import callback +from homeassistant.data_entry_flow import FlowResult +import homeassistant.helpers.config_validation as cv + +from .const import ( + CONF_HOSTNAME, + CONF_IPV4, + CONF_IPV6, + CONF_RESOLVER, + CONF_RESOLVER_IPV6, + DEFAULT_HOSTNAME, + DEFAULT_NAME, + DEFAULT_RESOLVER, + DEFAULT_RESOLVER_IPV6, + DOMAIN, +) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, + } +) + + +async def async_validate_hostname( + hostname: str, resolver_ipv4: str, resolver_ipv6: str +) -> dict[str, bool]: + """Validate hostname.""" + + async def async_check(hostname: str, resolver: str, qtype: str) -> bool: + """Return if able to resolve hostname.""" + result = False + with contextlib.suppress(DNSError): + result = bool( + await aiodns.DNSResolver(nameservers=[resolver]).query(hostname, qtype) + ) + return result + + result: dict[str, bool] = {} + + tasks = await asyncio.gather( + async_check(hostname, resolver_ipv4, "A"), + async_check(hostname, resolver_ipv6, "AAAA"), + ) + + result[CONF_IPV4] = tasks[0] + result[CONF_IPV6] = tasks[1] + + return result + + +class DnsIPConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for dnsip integration.""" + + VERSION = 1 + + @staticmethod + @callback + def async_get_options_flow( + config_entry: config_entries.ConfigEntry, + ) -> DnsIPOptionsFlowHandler: + """Return Option handler.""" + return DnsIPOptionsFlowHandler(config_entry) + + async def async_step_import(self, config: dict[str, Any]) -> FlowResult: + """Import a configuration from config.yaml.""" + + hostname = config.get(CONF_HOSTNAME, DEFAULT_HOSTNAME) + self._async_abort_entries_match({CONF_HOSTNAME: hostname}) + config[CONF_HOSTNAME] = hostname + return await self.async_step_user(user_input=config) + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + + errors = {} + + if user_input: + + hostname = user_input[CONF_HOSTNAME] + name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname + resolver = DEFAULT_RESOLVER + resolver_ipv6 = DEFAULT_RESOLVER_IPV6 + + validate = await async_validate_hostname(hostname, resolver, resolver_ipv6) + + if not validate[CONF_IPV4] and not validate[CONF_IPV6]: + errors["base"] = "invalid_hostname" + else: + await self.async_set_unique_id(hostname) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=name, + data={ + CONF_HOSTNAME: hostname, + CONF_NAME: name, + CONF_RESOLVER: resolver, + CONF_RESOLVER_IPV6: resolver_ipv6, + CONF_IPV4: validate[CONF_IPV4], + CONF_IPV6: validate[CONF_IPV6], + }, + ) + + return self.async_show_form( + step_id="user", + data_schema=DATA_SCHEMA, + errors=errors, + ) + + +class DnsIPOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option config flow for dnsip integration.""" + + def __init__(self, entry: config_entries.ConfigEntry) -> None: + """Initialize options flow.""" + self.entry = entry + + async def async_step_init( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Manage the options.""" + errors = {} + if user_input is not None: + validate = await async_validate_hostname( + self.entry.data[CONF_HOSTNAME], + user_input[CONF_RESOLVER], + user_input[CONF_RESOLVER_IPV6], + ) + + if validate[CONF_IPV4] is False and self.entry.data[CONF_IPV4] is True: + errors[CONF_RESOLVER] = "invalid_resolver" + elif validate[CONF_IPV6] is False and self.entry.data[CONF_IPV6] is True: + errors[CONF_RESOLVER_IPV6] = "invalid_resolver" + else: + return self.async_create_entry(title=self.entry.title, data=user_input) + + return self.async_show_form( + step_id="init", + data_schema=vol.Schema( + { + vol.Optional( + CONF_RESOLVER, + default=self.entry.options.get(CONF_RESOLVER, DEFAULT_RESOLVER), + ): cv.string, + vol.Optional( + CONF_RESOLVER_IPV6, + default=self.entry.options.get( + CONF_RESOLVER_IPV6, DEFAULT_RESOLVER_IPV6 + ), + ): cv.string, + } + ), + errors=errors, + ) diff --git a/homeassistant/components/dnsip/const.py b/homeassistant/components/dnsip/const.py new file mode 100644 index 00000000000..4e4d6f472ad --- /dev/null +++ b/homeassistant/components/dnsip/const.py @@ -0,0 +1,17 @@ +"""Constants for dnsip integration.""" +from homeassistant.const import Platform + +DOMAIN = "dnsip" +PLATFORMS = [Platform.SENSOR] + +CONF_HOSTNAME = "hostname" +CONF_RESOLVER = "resolver" +CONF_RESOLVER_IPV6 = "resolver_ipv6" +CONF_IPV4 = "ipv4" +CONF_IPV6 = "ipv6" + +DEFAULT_HOSTNAME = "myip.opendns.com" +DEFAULT_IPV6 = False +DEFAULT_NAME = "myip" +DEFAULT_RESOLVER = "208.67.222.222" +DEFAULT_RESOLVER_IPV6 = "2620:0:ccc::2" diff --git a/homeassistant/components/dnsip/manifest.json b/homeassistant/components/dnsip/manifest.json index 2a277c3ceeb..21934f0d135 100644 --- a/homeassistant/components/dnsip/manifest.json +++ b/homeassistant/components/dnsip/manifest.json @@ -3,6 +3,7 @@ "name": "DNS IP", "documentation": "https://www.home-assistant.io/integrations/dnsip", "requirements": ["aiodns==3.0.0"], - "codeowners": [], + "codeowners": ["@gjohansson-ST"], + "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/dnsip/sensor.py b/homeassistant/components/dnsip/sensor.py index a429d336379..9057f3e8c33 100644 --- a/homeassistant/components/dnsip/sensor.py +++ b/homeassistant/components/dnsip/sensor.py @@ -8,29 +8,37 @@ import aiodns from aiodns.error import DNSError import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, + SensorEntity, +) +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.device_registry import DeviceEntryType +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType +from .const import ( + CONF_HOSTNAME, + CONF_IPV4, + CONF_IPV6, + CONF_RESOLVER, + CONF_RESOLVER_IPV6, + DEFAULT_HOSTNAME, + DEFAULT_IPV6, + DEFAULT_RESOLVER, + DEFAULT_RESOLVER_IPV6, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) -CONF_HOSTNAME = "hostname" -CONF_IPV6 = "ipv6" -CONF_RESOLVER = "resolver" -CONF_RESOLVER_IPV6 = "resolver_ipv6" - -DEFAULT_HOSTNAME = "myip.opendns.com" -DEFAULT_IPV6 = False -DEFAULT_NAME = "myip" -DEFAULT_RESOLVER = "208.67.222.222" -DEFAULT_RESOLVER_IPV6 = "2620:0:ccc::2" - SCAN_INTERVAL = timedelta(seconds=120) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_HOSTNAME, default=DEFAULT_HOSTNAME): cv.string, @@ -48,27 +56,72 @@ async def async_setup_platform( discovery_info: DiscoveryInfoType | None = None, ) -> None: """Set up the DNS IP sensor.""" - hostname = config[CONF_HOSTNAME] - name = config.get(CONF_NAME) - ipv6 = config[CONF_IPV6] + _LOGGER.warning( + "Configuration of the DNS IP platform in YAML is deprecated and will be " + "removed in Home Assistant 2022.4; Your existing configuration " + "has been imported into the UI automatically and can be safely removed " + "from your configuration.yaml file" + ) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=config, + ) + ) - if not name: - name = DEFAULT_NAME if hostname == DEFAULT_HOSTNAME else hostname - resolver = config[CONF_RESOLVER_IPV6] if ipv6 else config[CONF_RESOLVER] - async_add_devices([WanIpSensor(name, hostname, resolver, ipv6)], True) +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up the dnsip sensor entry.""" + + hostname = entry.data[CONF_HOSTNAME] + name = entry.data[CONF_NAME] + + resolver_ipv4 = entry.options.get(CONF_RESOLVER, entry.data[CONF_RESOLVER]) + resolver_ipv6 = entry.options.get( + CONF_RESOLVER_IPV6, entry.data[CONF_RESOLVER_IPV6] + ) + entities = [] + if entry.data[CONF_IPV4]: + entities.append(WanIpSensor(name, hostname, resolver_ipv4, False)) + if entry.data[CONF_IPV6]: + entities.append(WanIpSensor(name, hostname, resolver_ipv6, True)) + + async_add_entities(entities, update_before_add=True) class WanIpSensor(SensorEntity): """Implementation of a DNS IP sensor.""" - def __init__(self, name: str, hostname: str, resolver: str, ipv6: bool) -> None: + _attr_icon = "mdi:web" + + def __init__( + self, + name: str, + hostname: str, + resolver: str, + ipv6: bool, + ) -> None: """Initialize the DNS IP sensor.""" - self._attr_name = name + self._attr_name = f"{name} IPv6" if ipv6 else name + self._attr_unique_id = f"{hostname}_{ipv6}" self.hostname = hostname self.resolver = aiodns.DNSResolver() self.resolver.nameservers = [resolver] self.querytype = "AAAA" if ipv6 else "A" + self._attr_extra_state_attributes = { + "Resolver": resolver, + "Querytype": self.querytype, + } + self._attr_device_info = DeviceInfo( + entry_type=DeviceEntryType.SERVICE, + identifiers={(DOMAIN, f"{hostname}_{ipv6}")}, + manufacturer="DNS", + model=aiodns.__version__, + name=hostname, + ) async def async_update(self) -> None: """Get the current DNS IP address for hostname.""" @@ -80,5 +133,6 @@ class WanIpSensor(SensorEntity): if response: self._attr_native_value = response[0].host + self._attr_available = True else: - self._attr_native_value = None + self._attr_available = False diff --git a/homeassistant/components/dnsip/strings.json b/homeassistant/components/dnsip/strings.json new file mode 100644 index 00000000000..06672e6fb68 --- /dev/null +++ b/homeassistant/components/dnsip/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "data": { + "hostname": "The hostname for which to perform the DNS query" + } + } + }, + "error": { + "invalid_hostname": "Invalid hostname" + } + }, + "options": { + "step": { + "init": { + "data": { + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" + } + } + }, + "error": { + "invalid_resolver": "Invalid IP address for resolver" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/dnsip/translations/en.json b/homeassistant/components/dnsip/translations/en.json new file mode 100644 index 00000000000..06672e6fb68 --- /dev/null +++ b/homeassistant/components/dnsip/translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "step": { + "user": { + "data": { + "hostname": "The hostname for which to perform the DNS query" + } + } + }, + "error": { + "invalid_hostname": "Invalid hostname" + } + }, + "options": { + "step": { + "init": { + "data": { + "resolver": "Resolver for IPV4 lookup", + "resolver_ipv6": "Resolver for IPV6 lookup" + } + } + }, + "error": { + "invalid_resolver": "Invalid IP address for resolver" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e995dd6ad87..60c4e32fad3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -71,6 +71,7 @@ FLOWS = [ "dialogflow", "directv", "dlna_dmr", + "dnsip", "doorbird", "dsmr", "dunehd", diff --git a/tests/components/dnsip/__init__.py b/tests/components/dnsip/__init__.py new file mode 100644 index 00000000000..9fb6f529c5e --- /dev/null +++ b/tests/components/dnsip/__init__.py @@ -0,0 +1 @@ +"""Tests for the dnsip integration.""" diff --git a/tests/components/dnsip/test_config_flow.py b/tests/components/dnsip/test_config_flow.py new file mode 100644 index 00000000000..3ebbdfe91da --- /dev/null +++ b/tests/components/dnsip/test_config_flow.py @@ -0,0 +1,290 @@ +"""Test the dnsip config flow.""" +from __future__ import annotations + +from unittest.mock import patch + +from aiodns.error import DNSError +import pytest + +from homeassistant import config_entries +from homeassistant.components.dnsip.const import ( + CONF_HOSTNAME, + CONF_IPV4, + CONF_IPV6, + CONF_RESOLVER, + CONF_RESOLVER_IPV6, + DOMAIN, +) +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from tests.common import MockConfigEntry + + +class RetrieveDNS: + """Return list of test information.""" + + @staticmethod + async def query(hostname, qtype) -> dict[str, str]: + """Return information.""" + return {"hostname": "1.2.3.4"} + + @property + def nameservers(self) -> list[str]: + """Return nameserver.""" + return ["1.2.3.4"] + + +async def test_form(hass: HomeAssistant) -> None: + """Test we get the form.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTNAME: "home-assistant.io", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "home-assistant.io" + assert result2["data"] == { + "hostname": "home-assistant.io", + "name": "home-assistant.io", + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + "ipv4": True, + "ipv6": True, + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_error(hass: HomeAssistant) -> None: + """Test validate url fails.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + side_effect=DNSError("Did not find"), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTNAME: "home-assistant.io", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_hostname"} + + +@pytest.mark.parametrize( + "p_input,p_output", + [ + ( + {CONF_HOSTNAME: "home-assistant.io"}, + { + "hostname": "home-assistant.io", + "name": "home-assistant.io", + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + "ipv4": True, + "ipv6": True, + }, + ), + ( + {}, + { + "hostname": "myip.opendns.com", + "name": "myip", + "resolver": "208.67.222.222", + "resolver_ipv6": "2620:0:ccc::2", + "ipv4": True, + "ipv6": True, + }, + ), + ], +) +async def test_import_flow_success( + hass: HomeAssistant, p_input: dict[str, str], p_output: dict[str, str] +) -> None: + """Test a successful import of YAML.""" + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=p_input, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == p_output["name"] + assert result2["data"] == p_output + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_already_exist(hass: HomeAssistant) -> None: + """Test flow when unique id already exist.""" + + MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + CONF_IPV4: True, + CONF_IPV6: True, + }, + unique_id="home-assistant.io", + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_HOSTNAME: "home-assistant.io", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_configured" + + +async def test_options_flow(hass: HomeAssistant) -> None: + """Test options config flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + CONF_IPV4: True, + CONF_IPV6: False, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + return_value=RetrieveDNS(), + ), patch( + "homeassistant.components.dnsip.async_setup_entry", + return_value=True, + ): + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_RESOLVER: "8.8.8.8", + CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + }, + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + "resolver": "8.8.8.8", + "resolver_ipv6": "2001:4860:4860::8888", + } + + +@pytest.mark.parametrize( + "p_input", + [ + { + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + CONF_IPV4: True, + CONF_IPV6: False, + }, + { + CONF_HOSTNAME: "home-assistant.io", + CONF_NAME: "home-assistant.io", + CONF_RESOLVER: "208.67.222.222", + CONF_RESOLVER_IPV6: "2620:0:ccc::2", + CONF_IPV4: False, + CONF_IPV6: True, + }, + ], +) +async def test_options_error(hass: HomeAssistant, p_input: dict[str, str]) -> None: + """Test validate url fails in options.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data=p_input, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.options.async_init(entry.entry_id) + + with patch( + "homeassistant.components.dnsip.config_flow.aiodns.DNSResolver", + side_effect=DNSError("Did not find"), + ): + result2 = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_RESOLVER: "192.168.200.34", + CONF_RESOLVER_IPV6: "2001:4860:4860::8888", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_FORM + assert result2["step_id"] == "init" + if p_input[CONF_IPV4]: + assert result2["errors"] == {"resolver": "invalid_resolver"} + if p_input[CONF_IPV6]: + assert result2["errors"] == {"resolver_ipv6": "invalid_resolver"}