mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 05:07:41 +00:00
Add option to login with username/email and password in Habitica integration (#117622)
* add login/password authentication * add advanced config flow * remove unused exception classes, fix errors * update username in init * update tests * update strings * combine steps with menu * remove username from entry * update tests * Revert "update tests" This reverts commit 6ac8ad6a26547b623e217db817ec4d0cf8c91f1d. * Revert "remove username from entry" This reverts commit d9323fb72df3f9d41be0a53bb0cbe16be718d005. * small changes * remove pylint broad-excep * run habitipy init in executor * Add text selectors * changes
This commit is contained in:
parent
20f9b9e412
commit
50577883dc
@ -15,6 +15,7 @@ from homeassistant.const import (
|
|||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_SENSORS,
|
CONF_SENSORS,
|
||||||
CONF_URL,
|
CONF_URL,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
Platform,
|
Platform,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant, ServiceCall
|
from homeassistant.core import HomeAssistant, ServiceCall
|
||||||
@ -125,6 +126,7 @@ async def async_setup_entry(
|
|||||||
name = call.data[ATTR_NAME]
|
name = call.data[ATTR_NAME]
|
||||||
path = call.data[ATTR_PATH]
|
path = call.data[ATTR_PATH]
|
||||||
entries = hass.config_entries.async_entries(DOMAIN)
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
|
||||||
api = None
|
api = None
|
||||||
for entry in entries:
|
for entry in entries:
|
||||||
if entry.data[CONF_NAME] == name:
|
if entry.data[CONF_NAME] == name:
|
||||||
@ -147,18 +149,16 @@ async def async_setup_entry(
|
|||||||
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
|
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
|
||||||
)
|
)
|
||||||
|
|
||||||
websession = async_get_clientsession(hass)
|
websession = async_get_clientsession(
|
||||||
|
hass, verify_ssl=config_entry.data.get(CONF_VERIFY_SSL, True)
|
||||||
url = config_entry.data[CONF_URL]
|
)
|
||||||
username = config_entry.data[CONF_API_USER]
|
|
||||||
password = config_entry.data[CONF_API_KEY]
|
|
||||||
|
|
||||||
api = await hass.async_add_executor_job(
|
api = await hass.async_add_executor_job(
|
||||||
HAHabitipyAsync,
|
HAHabitipyAsync,
|
||||||
{
|
{
|
||||||
"url": url,
|
"url": config_entry.data[CONF_URL],
|
||||||
"login": username,
|
"login": config_entry.data[CONF_API_USER],
|
||||||
"password": password,
|
"password": config_entry.data[CONF_API_KEY],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from http import HTTPStatus
|
||||||
import logging
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
@ -10,48 +11,53 @@ from habitipy.aio import HabitipyAsync
|
|||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_URL
|
from homeassistant.const import (
|
||||||
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
CONF_API_KEY,
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
CONF_PASSWORD,
|
||||||
|
CONF_URL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN
|
||||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
TextSelectorType,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
from .const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
||||||
|
|
||||||
DATA_SCHEMA = vol.Schema(
|
STEP_ADVANCED_DATA_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_API_USER): str,
|
vol.Required(CONF_API_USER): str,
|
||||||
vol.Required(CONF_API_KEY): str,
|
vol.Required(CONF_API_KEY): str,
|
||||||
vol.Optional(CONF_NAME): str,
|
|
||||||
vol.Optional(CONF_URL, default=DEFAULT_URL): str,
|
vol.Optional(CONF_URL, default=DEFAULT_URL): str,
|
||||||
|
vol.Required(CONF_VERIFY_SSL, default=True): bool,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
STEP_LOGIN_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_USERNAME): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.EMAIL,
|
||||||
|
autocomplete="email",
|
||||||
|
)
|
||||||
|
),
|
||||||
|
vol.Required(CONF_PASSWORD): TextSelector(
|
||||||
|
TextSelectorConfig(
|
||||||
|
type=TextSelectorType.PASSWORD,
|
||||||
|
autocomplete="current-password",
|
||||||
|
)
|
||||||
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
async def validate_input(hass: HomeAssistant, data: dict[str, str]) -> dict[str, str]:
|
|
||||||
"""Validate the user input allows us to connect."""
|
|
||||||
|
|
||||||
websession = async_get_clientsession(hass)
|
|
||||||
api = await hass.async_add_executor_job(
|
|
||||||
HabitipyAsync,
|
|
||||||
{
|
|
||||||
"login": data[CONF_API_USER],
|
|
||||||
"password": data[CONF_API_KEY],
|
|
||||||
"url": data[CONF_URL] or DEFAULT_URL,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
await api.user.get(session=websession)
|
|
||||||
return {
|
|
||||||
"title": f"{data.get('name', 'Default username')}",
|
|
||||||
CONF_API_USER: data[CONF_API_USER],
|
|
||||||
}
|
|
||||||
except ClientResponseError as ex:
|
|
||||||
raise InvalidAuth from ex
|
|
||||||
|
|
||||||
|
|
||||||
class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for habitica."""
|
"""Handle a config flow for habitica."""
|
||||||
|
|
||||||
@ -62,24 +68,115 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
"""Handle the initial step."""
|
"""Handle the initial step."""
|
||||||
|
|
||||||
errors = {}
|
return self.async_show_menu(
|
||||||
|
step_id="user",
|
||||||
|
menu_options=["login", "advanced"],
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_login(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Config flow with username/password.
|
||||||
|
|
||||||
|
Simplified configuration setup that retrieves API credentials
|
||||||
|
from Habitica.com by authenticating with login and password.
|
||||||
|
"""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
if user_input is not None:
|
if user_input is not None:
|
||||||
try:
|
try:
|
||||||
info = await validate_input(self.hass, user_input)
|
session = async_get_clientsession(self.hass)
|
||||||
except InvalidAuth:
|
api = await self.hass.async_add_executor_job(
|
||||||
errors = {"base": "invalid_credentials"}
|
HabitipyAsync,
|
||||||
|
{
|
||||||
|
"login": "",
|
||||||
|
"password": "",
|
||||||
|
"url": DEFAULT_URL,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
login_response = await api.user.auth.local.login.post(
|
||||||
|
session=session,
|
||||||
|
username=user_input[CONF_USERNAME],
|
||||||
|
password=user_input[CONF_PASSWORD],
|
||||||
|
)
|
||||||
|
|
||||||
|
except ClientResponseError as ex:
|
||||||
|
if ex.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
else:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
except Exception:
|
except Exception:
|
||||||
_LOGGER.exception("Unexpected exception")
|
_LOGGER.exception("Unexpected exception")
|
||||||
errors = {"base": "unknown"}
|
errors["base"] = "unknown"
|
||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(info[CONF_API_USER])
|
await self.async_set_unique_id(login_response["id"])
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
return self.async_create_entry(title=info["title"], data=user_input)
|
return self.async_create_entry(
|
||||||
|
title=login_response["username"],
|
||||||
|
data={
|
||||||
|
CONF_API_USER: login_response["id"],
|
||||||
|
CONF_API_KEY: login_response["apiToken"],
|
||||||
|
CONF_USERNAME: login_response["username"],
|
||||||
|
CONF_URL: DEFAULT_URL,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_show_form(
|
return self.async_show_form(
|
||||||
step_id="user",
|
step_id="login",
|
||||||
data_schema=DATA_SCHEMA,
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
data_schema=STEP_LOGIN_DATA_SCHEMA, suggested_values=user_input
|
||||||
|
),
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_advanced(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Advanced configuration with User Id and API Token.
|
||||||
|
|
||||||
|
Advanced configuration allows connecting to Habitica instances
|
||||||
|
hosted on different domains or to self-hosted instances.
|
||||||
|
"""
|
||||||
|
errors: dict[str, str] = {}
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
session = async_get_clientsession(
|
||||||
|
self.hass, verify_ssl=user_input.get(CONF_VERIFY_SSL, True)
|
||||||
|
)
|
||||||
|
api = await self.hass.async_add_executor_job(
|
||||||
|
HabitipyAsync,
|
||||||
|
{
|
||||||
|
"login": user_input[CONF_API_USER],
|
||||||
|
"password": user_input[CONF_API_KEY],
|
||||||
|
"url": user_input.get(CONF_URL, DEFAULT_URL),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
api_response = await api.user.get(
|
||||||
|
session=session,
|
||||||
|
userFields="auth",
|
||||||
|
)
|
||||||
|
except ClientResponseError as ex:
|
||||||
|
if ex.status == HTTPStatus.UNAUTHORIZED:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
else:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except Exception:
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
else:
|
||||||
|
await self.async_set_unique_id(user_input[CONF_API_USER])
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
user_input[CONF_USERNAME] = api_response["auth"]["local"]["username"]
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=user_input[CONF_USERNAME], data=user_input
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="advanced",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
data_schema=STEP_ADVANCED_DATA_SCHEMA, suggested_values=user_input
|
||||||
|
),
|
||||||
errors=errors,
|
errors=errors,
|
||||||
description_placeholders={},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||||
@ -98,8 +195,4 @@ class HabiticaConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"integration_title": "Habitica",
|
"integration_title": "Habitica",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
return await self.async_step_user(import_data)
|
return await self.async_step_advanced(import_data)
|
||||||
|
|
||||||
|
|
||||||
class InvalidAuth(HomeAssistantError):
|
|
||||||
"""Error to indicate there is invalid auth."""
|
|
||||||
|
@ -4,18 +4,32 @@
|
|||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
|
||||||
},
|
},
|
||||||
"error": {
|
"error": {
|
||||||
"invalid_credentials": "[%key:common::config_flow::error::invalid_auth%]",
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
},
|
},
|
||||||
"step": {
|
"step": {
|
||||||
"user": {
|
"user": {
|
||||||
|
"menu_options": {
|
||||||
|
"login": "Login to Habitica",
|
||||||
|
"advanced": "Login to other instances"
|
||||||
|
},
|
||||||
|
"description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks."
|
||||||
|
},
|
||||||
|
"login": {
|
||||||
|
"data": {
|
||||||
|
"username": "Email or username (case-sensitive)",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"advanced": {
|
||||||
"data": {
|
"data": {
|
||||||
"url": "[%key:common::config_flow::data::url%]",
|
"url": "[%key:common::config_flow::data::url%]",
|
||||||
"name": "Override for Habitica’s username. Will be used for actions",
|
"api_user": "User ID",
|
||||||
"api_user": "Habitica’s API user ID",
|
"api_key": "API Token",
|
||||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||||
},
|
},
|
||||||
"description": "Connect your Habitica profile to allow monitoring of your user's profile and tasks. Note that api_id and api_key must be gotten from https://habitica.com/user/settings/api"
|
"description": "You can retrieve your `User ID` and `API Token` from **Settings -> Site Data** on Habitica or the instance you want to connect to"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -3,26 +3,152 @@
|
|||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from aiohttp import ClientResponseError
|
from aiohttp import ClientResponseError
|
||||||
|
import pytest
|
||||||
|
|
||||||
from homeassistant import config_entries
|
from homeassistant import config_entries
|
||||||
from homeassistant.components.habitica.const import DEFAULT_URL, DOMAIN
|
from homeassistant.components.habitica.const import CONF_API_USER, DEFAULT_URL, DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_API_KEY,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_URL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
MOCK_DATA_LOGIN_STEP = {
|
||||||
|
CONF_USERNAME: "test-email@example.com",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
}
|
||||||
|
MOCK_DATA_ADVANCED_STEP = {
|
||||||
|
CONF_API_USER: "test-api-user",
|
||||||
|
CONF_API_KEY: "test-api-key",
|
||||||
|
CONF_URL: DEFAULT_URL,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
}
|
||||||
|
|
||||||
async def test_form(hass: HomeAssistant) -> None:
|
|
||||||
|
async def test_form_login(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we get the login form."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.MENU
|
||||||
|
assert "login" in result["menu_options"]
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "login"}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
assert result["step_id"] == "login"
|
||||||
|
|
||||||
|
mock_obj = MagicMock()
|
||||||
|
mock_obj.user.auth.local.login.post = AsyncMock()
|
||||||
|
mock_obj.user.auth.local.login.post.return_value = {
|
||||||
|
"id": "test-api-user",
|
||||||
|
"apiToken": "test-api-key",
|
||||||
|
"username": "test-username",
|
||||||
|
}
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||||
|
return_value=mock_obj,
|
||||||
|
),
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.habitica.async_setup", return_value=True
|
||||||
|
) as mock_setup,
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.habitica.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry,
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_DATA_LOGIN_STEP,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "test-username"
|
||||||
|
assert result["data"] == {
|
||||||
|
**MOCK_DATA_ADVANCED_STEP,
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
}
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("raise_error", "text_error"),
|
||||||
|
[
|
||||||
|
(ClientResponseError(MagicMock(), (), status=400), "cannot_connect"),
|
||||||
|
(ClientResponseError(MagicMock(), (), status=401), "invalid_auth"),
|
||||||
|
(IndexError(), "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_login_errors(hass: HomeAssistant, raise_error, text_error) -> None:
|
||||||
|
"""Test we handle invalid credentials error."""
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.MENU
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "login"}
|
||||||
|
)
|
||||||
|
|
||||||
|
mock_obj = MagicMock()
|
||||||
|
mock_obj.user.auth.local.login.post = AsyncMock(side_effect=raise_error)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||||
|
return_value=mock_obj,
|
||||||
|
):
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input=MOCK_DATA_LOGIN_STEP,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] is FlowResultType.FORM
|
||||||
|
assert result2["errors"] == {"base": text_error}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_advanced(hass: HomeAssistant) -> None:
|
||||||
"""Test we get the form."""
|
"""Test we get the form."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.MENU
|
||||||
|
assert "advanced" in result["menu_options"]
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "advanced"}
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
assert result["step_id"] == "advanced"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "advanced"}
|
||||||
|
)
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["errors"] == {}
|
assert result["errors"] == {}
|
||||||
|
|
||||||
mock_obj = MagicMock()
|
mock_obj = MagicMock()
|
||||||
mock_obj.user.get = AsyncMock()
|
mock_obj.user.get = AsyncMock()
|
||||||
|
mock_obj.user.get.return_value = {"auth": {"local": {"username": "test-username"}}}
|
||||||
|
|
||||||
with (
|
with (
|
||||||
patch(
|
patch(
|
||||||
@ -39,29 +165,46 @@ async def test_form(hass: HomeAssistant) -> None:
|
|||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{"api_user": "test-api-user", "api_key": "test-api-key"},
|
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||||
assert result2["title"] == "Default username"
|
assert result2["title"] == "test-username"
|
||||||
assert result2["data"] == {
|
assert result2["data"] == {
|
||||||
"url": DEFAULT_URL,
|
**MOCK_DATA_ADVANCED_STEP,
|
||||||
"api_user": "test-api-user",
|
CONF_USERNAME: "test-username",
|
||||||
"api_key": "test-api-key",
|
|
||||||
}
|
}
|
||||||
assert len(mock_setup.mock_calls) == 1
|
assert len(mock_setup.mock_calls) == 1
|
||||||
assert len(mock_setup_entry.mock_calls) == 1
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_form_invalid_credentials(hass: HomeAssistant) -> None:
|
@pytest.mark.parametrize(
|
||||||
|
("raise_error", "text_error"),
|
||||||
|
[
|
||||||
|
(ClientResponseError(MagicMock(), (), status=400), "cannot_connect"),
|
||||||
|
(ClientResponseError(MagicMock(), (), status=401), "invalid_auth"),
|
||||||
|
(IndexError(), "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_form_advanced_errors(
|
||||||
|
hass: HomeAssistant, raise_error, text_error
|
||||||
|
) -> None:
|
||||||
"""Test we handle invalid credentials error."""
|
"""Test we handle invalid credentials error."""
|
||||||
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
result = await hass.config_entries.flow.async_init(
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.MENU
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": "advanced"}
|
||||||
|
)
|
||||||
|
|
||||||
mock_obj = MagicMock()
|
mock_obj = MagicMock()
|
||||||
mock_obj.user.get = AsyncMock(side_effect=ClientResponseError(MagicMock(), ()))
|
mock_obj.user.get = AsyncMock(side_effect=raise_error)
|
||||||
|
|
||||||
with patch(
|
with patch(
|
||||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
||||||
@ -69,41 +212,11 @@ async def test_form_invalid_credentials(hass: HomeAssistant) -> None:
|
|||||||
):
|
):
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
result["flow_id"],
|
result["flow_id"],
|
||||||
{
|
user_input=MOCK_DATA_ADVANCED_STEP,
|
||||||
"url": DEFAULT_URL,
|
|
||||||
"api_user": "test-api-user",
|
|
||||||
"api_key": "test-api-key",
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.FORM
|
assert result2["type"] is FlowResultType.FORM
|
||||||
assert result2["errors"] == {"base": "invalid_credentials"}
|
assert result2["errors"] == {"base": text_error}
|
||||||
|
|
||||||
|
|
||||||
async def test_form_unexpected_exception(hass: HomeAssistant) -> None:
|
|
||||||
"""Test we handle unexpected exception error."""
|
|
||||||
result = await hass.config_entries.flow.async_init(
|
|
||||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
||||||
)
|
|
||||||
|
|
||||||
mock_obj = MagicMock()
|
|
||||||
mock_obj.user.get = AsyncMock(side_effect=Exception)
|
|
||||||
|
|
||||||
with patch(
|
|
||||||
"homeassistant.components.habitica.config_flow.HabitipyAsync",
|
|
||||||
return_value=mock_obj,
|
|
||||||
):
|
|
||||||
result2 = await hass.config_entries.flow.async_configure(
|
|
||||||
result["flow_id"],
|
|
||||||
{
|
|
||||||
"url": DEFAULT_URL,
|
|
||||||
"api_user": "test-api-user",
|
|
||||||
"api_key": "test-api-key",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
assert result2["type"] is FlowResultType.FORM
|
|
||||||
assert result2["errors"] == {"base": "unknown"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_manual_flow_config_exist(hass: HomeAssistant) -> None:
|
async def test_manual_flow_config_exist(hass: HomeAssistant) -> None:
|
||||||
@ -119,7 +232,7 @@ async def test_manual_flow_config_exist(hass: HomeAssistant) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert result["type"] is FlowResultType.FORM
|
assert result["type"] is FlowResultType.FORM
|
||||||
assert result["step_id"] == "user"
|
assert result["step_id"] == "advanced"
|
||||||
|
|
||||||
mock_obj = MagicMock()
|
mock_obj = MagicMock()
|
||||||
mock_obj.user.get = AsyncMock(return_value={"api_user": "test-api-user"})
|
mock_obj.user.get = AsyncMock(return_value={"api_user": "test-api-user"})
|
||||||
|
@ -52,6 +52,7 @@ def common_requests(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker:
|
|||||||
"https://habitica.com/api/v3/user",
|
"https://habitica.com/api/v3/user",
|
||||||
json={
|
json={
|
||||||
"data": {
|
"data": {
|
||||||
|
"auth": {"local": {"username": TEST_USER_NAME}},
|
||||||
"api_user": "test-api-user",
|
"api_user": "test-api-user",
|
||||||
"profile": {"name": TEST_USER_NAME},
|
"profile": {"name": TEST_USER_NAME},
|
||||||
"stats": {
|
"stats": {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user