Add config flow for cloudflare (#41167)

* add config flow for cloudflare

* Create const.py

* work on flow.

* remove const.

* lint.

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update config_flows.py

* Update homeassistant/components/cloudflare/strings.json

* Apply suggestions from code review

* Apply suggestions from code review

* Apply suggestions from code review

* Update strings.json

* Apply suggestions from code review

* Update __init__.py

* Update __init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Chris Talkington 2020-10-21 09:09:00 -05:00 committed by GitHub
parent e203896638
commit d8577a1550
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 692 additions and 49 deletions

View file

@ -131,7 +131,6 @@ omit =
homeassistant/components/clickatell/notify.py homeassistant/components/clickatell/notify.py
homeassistant/components/clicksend/notify.py homeassistant/components/clicksend/notify.py
homeassistant/components/clicksend_tts/notify.py homeassistant/components/clicksend_tts/notify.py
homeassistant/components/cloudflare/*
homeassistant/components/cmus/media_player.py homeassistant/components/cmus/media_player.py
homeassistant/components/co2signal/* homeassistant/components/co2signal/*
homeassistant/components/coinbase/* homeassistant/components/coinbase/*

View file

@ -75,7 +75,7 @@ homeassistant/components/cisco_ios/* @fbradyirl
homeassistant/components/cisco_mobility_express/* @fbradyirl homeassistant/components/cisco_mobility_express/* @fbradyirl
homeassistant/components/cisco_webex_teams/* @fbradyirl homeassistant/components/cisco_webex_teams/* @fbradyirl
homeassistant/components/cloud/* @home-assistant/cloud homeassistant/components/cloud/* @home-assistant/cloud
homeassistant/components/cloudflare/* @ludeeus homeassistant/components/cloudflare/* @ludeeus @ctalkington
homeassistant/components/comfoconnect/* @michaelarnauts homeassistant/components/comfoconnect/* @michaelarnauts
homeassistant/components/config/* @home-assistant/core homeassistant/components/config/* @home-assistant/core
homeassistant/components/configurator/* @home-assistant/core homeassistant/components/configurator/* @home-assistant/core

View file

@ -1,74 +1,130 @@
"""Update the IP addresses of your Cloudflare DNS records.""" """Update the IP addresses of your Cloudflare DNS records."""
from datetime import timedelta from datetime import timedelta
import logging import logging
from typing import Dict
from pycfdns import CloudflareUpdater from pycfdns import CloudflareUpdater
from pycfdns.exceptions import (
CloudflareAuthenticationException,
CloudflareConnectionException,
CloudflareException,
)
import voluptuous as vol import voluptuous as vol
from homeassistant.const import CONF_API_KEY, CONF_EMAIL, CONF_ZONE from homeassistant.components import persistent_notification
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, CONF_EMAIL, CONF_ZONE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.event import track_time_interval from homeassistant.helpers.event import async_track_time_interval
from .const import (
CONF_RECORDS,
DATA_UNDO_UPDATE_INTERVAL,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
SERVICE_UPDATE_RECORDS,
)
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
CONF_RECORDS = "records"
DOMAIN = "cloudflare"
INTERVAL = timedelta(minutes=60)
CONFIG_SCHEMA = vol.Schema( CONFIG_SCHEMA = vol.Schema(
{ {
DOMAIN: vol.Schema( DOMAIN: vol.All(
{ cv.deprecated(CONF_EMAIL, invalidation_version="0.119"),
vol.Required(CONF_EMAIL): cv.string, cv.deprecated(CONF_API_KEY, invalidation_version="0.119"),
vol.Required(CONF_API_KEY): cv.string, cv.deprecated(CONF_ZONE, invalidation_version="0.119"),
vol.Required(CONF_ZONE): cv.string, cv.deprecated(CONF_RECORDS, invalidation_version="0.119"),
vol.Required(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]), vol.Schema(
} {
vol.Optional(CONF_EMAIL): cv.string,
vol.Optional(CONF_API_KEY): cv.string,
vol.Optional(CONF_ZONE): cv.string,
vol.Optional(CONF_RECORDS): vol.All(cv.ensure_list, [cv.string]),
}
),
) )
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
def setup(hass, config): async def async_setup(hass: HomeAssistant, config: Dict) -> bool:
"""Set up the Cloudflare component.""" """Set up the component."""
hass.data.setdefault(DOMAIN, {})
cfupdate = CloudflareUpdater() if len(hass.config_entries.async_entries(DOMAIN)) > 0:
email = config[DOMAIN][CONF_EMAIL] return True
key = config[DOMAIN][CONF_API_KEY]
zone = config[DOMAIN][CONF_ZONE]
records = config[DOMAIN][CONF_RECORDS]
def update_records_interval(now): if DOMAIN in config and CONF_API_KEY in config[DOMAIN]:
"""Set up recurring update.""" persistent_notification.async_create(
_update_cloudflare(cfupdate, email, key, zone, records) hass,
"Cloudflare integration now requires an API Token. Please go to the integrations page to setup.",
"Cloudflare Setup",
"cloudflare_setup",
)
def update_records_service(now):
"""Set up service for manual trigger."""
_update_cloudflare(cfupdate, email, key, zone, records)
track_time_interval(hass, update_records_interval, INTERVAL)
hass.services.register(DOMAIN, "update_records", update_records_service)
return True return True
def _update_cloudflare(cfupdate, email, key, zone, records): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Update DNS records for a given zone.""" """Set up Cloudflare from a config entry."""
_LOGGER.debug("Starting update for zone %s", zone) cfupdate = CloudflareUpdater(
async_get_clientsession(hass),
entry.data[CONF_API_TOKEN],
entry.data[CONF_ZONE],
entry.data[CONF_RECORDS],
)
headers = cfupdate.set_header(email, key) try:
_LOGGER.debug("Header data defined as: %s", headers) zone_id = await cfupdate.get_zone_id()
except CloudflareAuthenticationException:
_LOGGER.error("API access forbidden. Please reauthenticate")
return False
except CloudflareConnectionException as error:
raise ConfigEntryNotReady from error
zoneid = cfupdate.get_zoneID(headers, zone) async def update_records(now):
_LOGGER.debug("Zone ID is set to: %s", zoneid) """Set up recurring update."""
try:
await _async_update_cloudflare(cfupdate, zone_id)
except CloudflareException as error:
_LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error)
update_records = cfupdate.get_recordInfo(headers, zoneid, zone, records) async def update_records_service(call):
_LOGGER.debug("Records: %s", update_records) """Set up service for manual trigger."""
try:
await _async_update_cloudflare(cfupdate, zone_id)
except CloudflareException as error:
_LOGGER.error("Error updating zone %s: %s", entry.data[CONF_ZONE], error)
result = cfupdate.update_records(headers, zoneid, update_records) update_interval = timedelta(minutes=DEFAULT_UPDATE_INTERVAL)
_LOGGER.debug("Update for zone %s is complete", zone) undo_interval = async_track_time_interval(hass, update_records, update_interval)
if result is not True: hass.data[DOMAIN][entry.entry_id] = {
_LOGGER.warning(result) DATA_UNDO_UPDATE_INTERVAL: undo_interval,
}
hass.services.async_register(DOMAIN, SERVICE_UPDATE_RECORDS, update_records_service)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload Cloudflare config entry."""
hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_INTERVAL]()
hass.data[DOMAIN].pop(entry.entry_id)
return True
async def _async_update_cloudflare(cfupdate: CloudflareUpdater, zone_id: str):
_LOGGER.debug("Starting update for zone %s", cfupdate.zone)
records = await cfupdate.get_record_info(zone_id)
_LOGGER.debug("Records: %s", records)
await cfupdate.update_records(zone_id, records)
_LOGGER.debug("Update for zone %s is complete", cfupdate.zone)

View file

@ -0,0 +1,180 @@
"""Config flow for Cloudflare integration."""
import logging
from typing import Dict, List, Optional
from pycfdns import CloudflareUpdater
from pycfdns.exceptions import (
CloudflareAuthenticationException,
CloudflareConnectionException,
CloudflareZoneException,
)
import voluptuous as vol
from homeassistant.components import persistent_notification
from homeassistant.config_entries import CONN_CLASS_CLOUD_PUSH, ConfigFlow
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_RECORDS
from .const import DOMAIN # pylint:disable=unused-import
_LOGGER = logging.getLogger(__name__)
DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_API_TOKEN): str,
}
)
def _zone_schema(zones: Optional[List] = None):
"""Zone selection schema."""
zones_list = []
if zones is not None:
zones_list = zones
return vol.Schema({vol.Required(CONF_ZONE): vol.In(zones_list)})
def _records_schema(records: Optional[List] = None):
"""Zone records selection schema."""
records_dict = {}
if records:
records_dict = {name: name for name in records}
return vol.Schema({vol.Required(CONF_RECORDS): cv.multi_select(records_dict)})
async def validate_input(hass: HomeAssistant, data: Dict):
"""Validate the user input allows us to connect.
Data has the keys from DATA_SCHEMA with values provided by the user.
"""
zone = data.get(CONF_ZONE)
records = None
cfupdate = CloudflareUpdater(
async_get_clientsession(hass),
data[CONF_API_TOKEN],
zone,
[],
)
try:
zones = await cfupdate.get_zones()
if zone:
zone_id = await cfupdate.get_zone_id()
records = await cfupdate.get_zone_records(zone_id, "A")
except CloudflareConnectionException as error:
raise CannotConnect from error
except CloudflareAuthenticationException as error:
raise InvalidAuth from error
except CloudflareZoneException as error:
raise InvalidZone from error
return {"zones": zones, "records": records}
class CloudflareConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Cloudflare."""
VERSION = 1
CONNECTION_CLASS = CONN_CLASS_CLOUD_PUSH
def __init__(self):
"""Initialize the Cloudflare config flow."""
self.cloudflare_config = {}
self.zones = None
self.records = None
async def async_step_user(self, user_input: Optional[Dict] = None):
"""Handle a flow initiated by the user."""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
assert self.hass
persistent_notification.async_dismiss(self.hass, "cloudflare_setup")
errors = {}
if user_input is not None:
info, errors = await self._async_validate_or_error(user_input)
if not errors:
self.cloudflare_config.update(user_input)
self.zones = info["zones"]
return await self.async_step_zone()
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
async def async_step_zone(self, user_input: Optional[Dict] = None):
"""Handle the picking the zone."""
errors = {}
if user_input is not None:
self.cloudflare_config.update(user_input)
info, errors = await self._async_validate_or_error(self.cloudflare_config)
if not errors:
await self.async_set_unique_id(user_input[CONF_ZONE])
self.records = info["records"]
return await self.async_step_records()
return self.async_show_form(
step_id="zone",
data_schema=_zone_schema(self.zones),
errors=errors,
)
async def async_step_records(self, user_input: Optional[Dict] = None):
"""Handle the picking the zone records."""
errors = {}
if user_input is not None:
self.cloudflare_config.update(user_input)
title = self.cloudflare_config[CONF_ZONE]
return self.async_create_entry(title=title, data=self.cloudflare_config)
return self.async_show_form(
step_id="records",
data_schema=_records_schema(self.records),
errors=errors,
)
async def _async_validate_or_error(self, config):
errors = {}
info = {}
try:
info = await validate_input(self.hass, config)
except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
errors["base"] = "invalid_auth"
except InvalidZone:
errors["base"] = "invalid_zone"
except Exception: # pylint: disable=broad-except
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return info, errors
class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""
class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
class InvalidZone(HomeAssistantError):
"""Error to indicate we cannot validate zone exists in account."""

View file

@ -0,0 +1,15 @@
"""Constants for Cloudflare."""
DOMAIN = "cloudflare"
# Config
CONF_RECORDS = "records"
# Data
DATA_UNDO_UPDATE_INTERVAL = "undo_update_interval"
# Defaults
DEFAULT_UPDATE_INTERVAL = 60 # in minutes
# Services
SERVICE_UPDATE_RECORDS = "update_records"

View file

@ -2,6 +2,7 @@
"domain": "cloudflare", "domain": "cloudflare",
"name": "Cloudflare", "name": "Cloudflare",
"documentation": "https://www.home-assistant.io/integrations/cloudflare", "documentation": "https://www.home-assistant.io/integrations/cloudflare",
"requirements": ["pycfdns==0.0.1"], "requirements": ["pycfdns==1.1.1"],
"codeowners": ["@ludeeus"] "codeowners": ["@ludeeus", "@ctalkington"],
"config_flow": true
} }

View file

@ -0,0 +1,35 @@
{
"config": {
"flow_title": "Cloudflare: {name}",
"step": {
"user": {
"title": "Connect to Cloudflare",
"description": "This integration requires an API Token created with Zone:Zone:Read and Zone:DNS:Edit permissions for all zones in your account.",
"data": {
"api_token": "[%key:common::config_flow::data::api_token%]"
}
},
"zone": {
"title": "Choose the Zone to Update",
"data": {
"zone": "Zone"
}
},
"records": {
"title": "Choose the Records to Update",
"data": {
"records": "Records"
}
}
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_zone": "Invalid zone"
},
"abort": {
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
"unknown": "[%key:common::config_flow::error::unknown%]"
}
}
}

View file

@ -35,6 +35,7 @@ FLOWS = [
"canary", "canary",
"cast", "cast",
"cert_expiry", "cert_expiry",
"cloudflare",
"control4", "control4",
"coolmaster", "coolmaster",
"coronavirus", "coronavirus",

View file

@ -1288,7 +1288,7 @@ pybotvac==0.0.17
pycarwings2==2.9 pycarwings2==2.9
# homeassistant.components.cloudflare # homeassistant.components.cloudflare
pycfdns==0.0.1 pycfdns==1.1.1
# homeassistant.components.channels # homeassistant.components.channels
pychannels==1.0.0 pychannels==1.0.0

View file

@ -635,6 +635,9 @@ pyblackbird==0.5
# homeassistant.components.neato # homeassistant.components.neato
pybotvac==0.0.17 pybotvac==0.0.17
# homeassistant.components.cloudflare
pycfdns==1.1.1
# homeassistant.components.cast # homeassistant.components.cast
pychromecast==7.5.1 pychromecast==7.5.1

View file

@ -0,0 +1,101 @@
"""Tests for the Cloudflare integration."""
from typing import List
from pycfdns import CFRecord
from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN
from homeassistant.const import CONF_API_TOKEN, CONF_ZONE
from tests.async_mock import AsyncMock, patch
from tests.common import MockConfigEntry
ENTRY_CONFIG = {
CONF_API_TOKEN: "mock-api-token",
CONF_ZONE: "mock.com",
CONF_RECORDS: ["ha.mock.com", "homeassistant.mock.com"],
}
ENTRY_OPTIONS = {}
USER_INPUT = {
CONF_API_TOKEN: "mock-api-token",
}
USER_INPUT_ZONE = {CONF_ZONE: "mock.com"}
USER_INPUT_RECORDS = {CONF_RECORDS: ["ha.mock.com", "homeassistant.mock.com"]}
MOCK_ZONE = "mock.com"
MOCK_ZONE_ID = "mock-zone-id"
MOCK_ZONE_RECORDS = [
{
"id": "zone-record-id",
"type": "A",
"name": "ha.mock.com",
"proxied": True,
"content": "127.0.0.1",
},
{
"id": "zone-record-id-2",
"type": "A",
"name": "homeassistant.mock.com",
"proxied": True,
"content": "127.0.0.1",
},
{
"id": "zone-record-id-3",
"type": "A",
"name": "mock.com",
"proxied": True,
"content": "127.0.0.1",
},
]
async def init_integration(
hass,
*,
data: dict = ENTRY_CONFIG,
options: dict = ENTRY_OPTIONS,
) -> MockConfigEntry:
"""Set up the Cloudflare integration in Home Assistant."""
entry = MockConfigEntry(domain=DOMAIN, data=data, options=options)
entry.add_to_hass(hass)
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
return entry
def _get_mock_cfupdate(
zone: str = MOCK_ZONE,
zone_id: str = MOCK_ZONE_ID,
records: List = MOCK_ZONE_RECORDS,
):
client = AsyncMock()
zone_records = [record["name"] for record in records]
cf_records = [CFRecord(record) for record in records]
client.get_zones = AsyncMock(return_value=[zone])
client.get_zone_records = AsyncMock(return_value=zone_records)
client.get_record_info = AsyncMock(return_value=cf_records)
client.get_zone_id = AsyncMock(return_value=zone_id)
client.update_records = AsyncMock(return_value=None)
return client
def _patch_async_setup(return_value=True):
return patch(
"homeassistant.components.cloudflare.async_setup",
return_value=return_value,
)
def _patch_async_setup_entry(return_value=True):
return patch(
"homeassistant.components.cloudflare.async_setup_entry",
return_value=return_value,
)

View file

@ -0,0 +1,28 @@
"""Define fixtures available for all tests."""
from pytest import fixture
from . import _get_mock_cfupdate
from tests.async_mock import patch
@fixture
def cfupdate(hass):
"""Mock the CloudflareUpdater for easier testing."""
mock_cfupdate = _get_mock_cfupdate()
with patch(
"homeassistant.components.cloudflare.CloudflareUpdater",
return_value=mock_cfupdate,
) as mock_api:
yield mock_api
@fixture
def cfupdate_flow(hass):
"""Mock the CloudflareUpdater for easier config flow testing."""
mock_cfupdate = _get_mock_cfupdate()
with patch(
"homeassistant.components.cloudflare.config_flow.CloudflareUpdater",
return_value=mock_cfupdate,
) as mock_api:
yield mock_api

View file

@ -0,0 +1,166 @@
"""Test the Cloudflare config flow."""
from pycfdns.exceptions import (
CloudflareAuthenticationException,
CloudflareConnectionException,
CloudflareZoneException,
)
from homeassistant.components.cloudflare.const import CONF_RECORDS, DOMAIN
from homeassistant.config_entries import SOURCE_USER
from homeassistant.const import CONF_API_TOKEN, CONF_SOURCE, CONF_ZONE
from homeassistant.data_entry_flow import (
RESULT_TYPE_ABORT,
RESULT_TYPE_CREATE_ENTRY,
RESULT_TYPE_FORM,
)
from homeassistant.setup import async_setup_component
from . import (
ENTRY_CONFIG,
USER_INPUT,
USER_INPUT_RECORDS,
USER_INPUT_ZONE,
_patch_async_setup,
_patch_async_setup_entry,
)
from tests.common import MockConfigEntry
async def test_user_form(hass, cfupdate_flow):
"""Test we get the user initiated form."""
await async_setup_component(hass, "persistent_notification", {})
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "user"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "zone"
assert result["errors"] == {}
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT_ZONE,
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_FORM
assert result["step_id"] == "records"
assert result["errors"] == {}
with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT_RECORDS,
)
await hass.async_block_till_done()
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
assert result["title"] == USER_INPUT_ZONE[CONF_ZONE]
assert result["data"]
assert result["data"][CONF_API_TOKEN] == USER_INPUT[CONF_API_TOKEN]
assert result["data"][CONF_ZONE] == USER_INPUT_ZONE[CONF_ZONE]
assert result["data"][CONF_RECORDS] == USER_INPUT_RECORDS[CONF_RECORDS]
assert result["result"]
assert result["result"].unique_id == USER_INPUT_ZONE[CONF_ZONE]
assert len(mock_setup.mock_calls) == 1
assert len(mock_setup_entry.mock_calls) == 1
async def test_user_form_cannot_connect(hass, cfupdate_flow):
"""Test we handle cannot connect error."""
instance = cfupdate_flow.return_value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
instance.get_zones.side_effect = CloudflareConnectionException()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "cannot_connect"}
async def test_user_form_invalid_auth(hass, cfupdate_flow):
"""Test we handle invalid auth error."""
instance = cfupdate_flow.return_value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
instance.get_zones.side_effect = CloudflareAuthenticationException()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_auth"}
async def test_user_form_invalid_zone(hass, cfupdate_flow):
"""Test we handle invalid zone error."""
instance = cfupdate_flow.return_value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
instance.get_zones.side_effect = CloudflareZoneException()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "invalid_zone"}
async def test_user_form_unexpected_exception(hass, cfupdate_flow):
"""Test we handle unexpected exception."""
instance = cfupdate_flow.return_value
result = await hass.config_entries.flow.async_init(
DOMAIN, context={CONF_SOURCE: SOURCE_USER}
)
instance.get_zones.side_effect = Exception()
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
USER_INPUT,
)
assert result["type"] == RESULT_TYPE_FORM
assert result["errors"] == {"base": "unknown"}
async def test_user_form_single_instance_allowed(hass):
"""Test that configuring more than one instance is rejected."""
entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
entry.add_to_hass(hass)
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={CONF_SOURCE: SOURCE_USER},
data=USER_INPUT,
)
assert result["type"] == RESULT_TYPE_ABORT
assert result["reason"] == "single_instance_allowed"

View file

@ -0,0 +1,58 @@
"""Test the Cloudflare integration."""
from pycfdns.exceptions import CloudflareConnectionException
from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS
from homeassistant.config_entries import (
ENTRY_STATE_LOADED,
ENTRY_STATE_NOT_LOADED,
ENTRY_STATE_SETUP_RETRY,
)
from . import ENTRY_CONFIG, init_integration
from tests.common import MockConfigEntry
async def test_unload_entry(hass, cfupdate):
"""Test successful unload of entry."""
entry = await init_integration(hass)
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert entry.state == ENTRY_STATE_LOADED
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
assert entry.state == ENTRY_STATE_NOT_LOADED
assert not hass.data.get(DOMAIN)
async def test_async_setup_raises_entry_not_ready(hass, cfupdate):
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
instance = cfupdate.return_value
entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
entry.add_to_hass(hass)
instance.get_zone_id.side_effect = CloudflareConnectionException()
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state == ENTRY_STATE_SETUP_RETRY
async def test_integration_services(hass, cfupdate):
"""Test integration services."""
instance = cfupdate.return_value
entry = await init_integration(hass)
assert entry.state == ENTRY_STATE_LOADED
await hass.services.async_call(
DOMAIN,
SERVICE_UPDATE_RECORDS,
{},
blocking=True,
)
await hass.async_block_till_done()
instance.update_records.assert_called_once()