mirror of
https://github.com/home-assistant/core.git
synced 2025-07-21 20:27:08 +00:00
Migrate Mastodon integration to config flow (#122376)
* Migrate to config flow * Fixes & add code owner * Add codeowners * Import within notify module * Fixes from review * Fixes * Remove config schema
This commit is contained in:
parent
64f997718a
commit
cb4a48ca02
@ -839,7 +839,8 @@ build.json @home-assistant/supervisor
|
|||||||
/tests/components/lyric/ @timmo001
|
/tests/components/lyric/ @timmo001
|
||||||
/homeassistant/components/madvr/ @iloveicedgreentea
|
/homeassistant/components/madvr/ @iloveicedgreentea
|
||||||
/tests/components/madvr/ @iloveicedgreentea
|
/tests/components/madvr/ @iloveicedgreentea
|
||||||
/homeassistant/components/mastodon/ @fabaff
|
/homeassistant/components/mastodon/ @fabaff @andrew-codechimp
|
||||||
|
/tests/components/mastodon/ @fabaff @andrew-codechimp
|
||||||
/homeassistant/components/matrix/ @PaarthShah
|
/homeassistant/components/matrix/ @PaarthShah
|
||||||
/tests/components/matrix/ @PaarthShah
|
/tests/components/matrix/ @PaarthShah
|
||||||
/homeassistant/components/matter/ @home-assistant/matter
|
/homeassistant/components/matter/ @home-assistant/matter
|
||||||
|
@ -1 +1,60 @@
|
|||||||
"""The Mastodon integration."""
|
"""The Mastodon integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from mastodon.Mastodon import Mastodon, MastodonError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_CLIENT_ID,
|
||||||
|
CONF_CLIENT_SECRET,
|
||||||
|
CONF_NAME,
|
||||||
|
Platform,
|
||||||
|
)
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers import discovery
|
||||||
|
|
||||||
|
from .const import CONF_BASE_URL, DOMAIN
|
||||||
|
from .utils import create_mastodon_client
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||||
|
"""Set up Mastodon from a config entry."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
client, _, _ = await hass.async_add_executor_job(
|
||||||
|
setup_mastodon,
|
||||||
|
entry,
|
||||||
|
)
|
||||||
|
|
||||||
|
except MastodonError as ex:
|
||||||
|
raise ConfigEntryNotReady("Failed to connect") from ex
|
||||||
|
|
||||||
|
assert entry.unique_id
|
||||||
|
|
||||||
|
await discovery.async_load_platform(
|
||||||
|
hass,
|
||||||
|
Platform.NOTIFY,
|
||||||
|
DOMAIN,
|
||||||
|
{CONF_NAME: entry.title, "client": client},
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def setup_mastodon(entry: ConfigEntry) -> tuple[Mastodon, dict, dict]:
|
||||||
|
"""Get mastodon details."""
|
||||||
|
client = create_mastodon_client(
|
||||||
|
entry.data[CONF_BASE_URL],
|
||||||
|
entry.data[CONF_CLIENT_ID],
|
||||||
|
entry.data[CONF_CLIENT_SECRET],
|
||||||
|
entry.data[CONF_ACCESS_TOKEN],
|
||||||
|
)
|
||||||
|
|
||||||
|
instance = client.instance()
|
||||||
|
account = client.account_verify_credentials()
|
||||||
|
|
||||||
|
return client, instance, account
|
||||||
|
168
homeassistant/components/mastodon/config_flow.py
Normal file
168
homeassistant/components/mastodon/config_flow.py
Normal file
@ -0,0 +1,168 @@
|
|||||||
|
"""Config flow for Mastodon."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
CONF_CLIENT_ID,
|
||||||
|
CONF_CLIENT_SECRET,
|
||||||
|
CONF_NAME,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
TextSelector,
|
||||||
|
TextSelectorConfig,
|
||||||
|
TextSelectorType,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import ConfigType
|
||||||
|
|
||||||
|
from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER
|
||||||
|
from .utils import construct_mastodon_username, create_mastodon_client
|
||||||
|
|
||||||
|
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(
|
||||||
|
CONF_BASE_URL,
|
||||||
|
default=DEFAULT_URL,
|
||||||
|
): TextSelector(TextSelectorConfig(type=TextSelectorType.URL)),
|
||||||
|
vol.Required(
|
||||||
|
CONF_CLIENT_ID,
|
||||||
|
): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)),
|
||||||
|
vol.Required(
|
||||||
|
CONF_CLIENT_SECRET,
|
||||||
|
): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)),
|
||||||
|
vol.Required(
|
||||||
|
CONF_ACCESS_TOKEN,
|
||||||
|
): TextSelector(TextSelectorConfig(type=TextSelectorType.PASSWORD)),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
config_entry: ConfigEntry
|
||||||
|
|
||||||
|
def check_connection(
|
||||||
|
self,
|
||||||
|
base_url: str,
|
||||||
|
client_id: str,
|
||||||
|
client_secret: str,
|
||||||
|
access_token: str,
|
||||||
|
) -> tuple[
|
||||||
|
dict[str, str] | None,
|
||||||
|
dict[str, str] | None,
|
||||||
|
dict[str, str],
|
||||||
|
]:
|
||||||
|
"""Check connection to the Mastodon instance."""
|
||||||
|
try:
|
||||||
|
client = create_mastodon_client(
|
||||||
|
base_url,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
access_token,
|
||||||
|
)
|
||||||
|
instance = client.instance()
|
||||||
|
account = client.account_verify_credentials()
|
||||||
|
|
||||||
|
except MastodonNetworkError:
|
||||||
|
return None, None, {"base": "network_error"}
|
||||||
|
except MastodonUnauthorizedError:
|
||||||
|
return None, None, {"base": "unauthorized_error"}
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
LOGGER.exception("Unexpected error")
|
||||||
|
return None, None, {"base": "unknown"}
|
||||||
|
return instance, account, {}
|
||||||
|
|
||||||
|
def show_user_form(
|
||||||
|
self,
|
||||||
|
user_input: dict[str, Any] | None = None,
|
||||||
|
errors: dict[str, str] | None = None,
|
||||||
|
description_placeholders: dict[str, str] | None = None,
|
||||||
|
step_id: str = "user",
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Show the user form."""
|
||||||
|
if user_input is None:
|
||||||
|
user_input = {}
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id=step_id,
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
STEP_USER_DATA_SCHEMA, user_input
|
||||||
|
),
|
||||||
|
description_placeholders=description_placeholders,
|
||||||
|
errors=errors,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> ConfigFlowResult:
|
||||||
|
"""Handle a flow initialized by the user."""
|
||||||
|
errors: dict[str, str] | None = None
|
||||||
|
if user_input:
|
||||||
|
self._async_abort_entries_match(
|
||||||
|
{CONF_CLIENT_ID: user_input[CONF_CLIENT_ID]}
|
||||||
|
)
|
||||||
|
|
||||||
|
instance, account, errors = await self.hass.async_add_executor_job(
|
||||||
|
self.check_connection,
|
||||||
|
user_input[CONF_BASE_URL],
|
||||||
|
user_input[CONF_CLIENT_ID],
|
||||||
|
user_input[CONF_CLIENT_SECRET],
|
||||||
|
user_input[CONF_ACCESS_TOKEN],
|
||||||
|
)
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
name = construct_mastodon_username(instance, account)
|
||||||
|
await self.async_set_unique_id(user_input[CONF_CLIENT_ID])
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=name,
|
||||||
|
data=user_input,
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.show_user_form(user_input, errors)
|
||||||
|
|
||||||
|
async def async_step_import(self, import_config: ConfigType) -> ConfigFlowResult:
|
||||||
|
"""Import a config entry from configuration.yaml."""
|
||||||
|
errors: dict[str, str] | None = None
|
||||||
|
|
||||||
|
LOGGER.debug("Importing Mastodon from configuration.yaml")
|
||||||
|
|
||||||
|
base_url = str(import_config.get(CONF_BASE_URL, DEFAULT_URL))
|
||||||
|
client_id = str(import_config.get(CONF_CLIENT_ID))
|
||||||
|
client_secret = str(import_config.get(CONF_CLIENT_SECRET))
|
||||||
|
access_token = str(import_config.get(CONF_ACCESS_TOKEN))
|
||||||
|
name = import_config.get(CONF_NAME, None)
|
||||||
|
|
||||||
|
instance, account, errors = await self.hass.async_add_executor_job(
|
||||||
|
self.check_connection,
|
||||||
|
base_url,
|
||||||
|
client_id,
|
||||||
|
client_secret,
|
||||||
|
access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
if not errors:
|
||||||
|
await self.async_set_unique_id(client_id)
|
||||||
|
self._abort_if_unique_id_configured()
|
||||||
|
|
||||||
|
if not name:
|
||||||
|
name = construct_mastodon_username(instance, account)
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=name,
|
||||||
|
data={
|
||||||
|
CONF_BASE_URL: base_url,
|
||||||
|
CONF_CLIENT_ID: client_id,
|
||||||
|
CONF_CLIENT_SECRET: client_secret,
|
||||||
|
CONF_ACCESS_TOKEN: access_token,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
reason = next(iter(errors.items()))[1]
|
||||||
|
return self.async_abort(reason=reason)
|
@ -5,5 +5,14 @@ from typing import Final
|
|||||||
|
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DOMAIN: Final = "mastodon"
|
||||||
|
|
||||||
CONF_BASE_URL: Final = "base_url"
|
CONF_BASE_URL: Final = "base_url"
|
||||||
|
DATA_HASS_CONFIG = "mastodon_hass_config"
|
||||||
DEFAULT_URL: Final = "https://mastodon.social"
|
DEFAULT_URL: Final = "https://mastodon.social"
|
||||||
|
DEFAULT_NAME: Final = "Mastodon"
|
||||||
|
|
||||||
|
INSTANCE_VERSION: Final = "version"
|
||||||
|
INSTANCE_URI: Final = "uri"
|
||||||
|
INSTANCE_DOMAIN: Final = "domain"
|
||||||
|
ACCOUNT_USERNAME: Final = "username"
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
{
|
{
|
||||||
"domain": "mastodon",
|
"domain": "mastodon",
|
||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"codeowners": ["@fabaff"],
|
"codeowners": ["@fabaff", "@andrew-codechimp"],
|
||||||
|
"config_flow": true,
|
||||||
"documentation": "https://www.home-assistant.io/integrations/mastodon",
|
"documentation": "https://www.home-assistant.io/integrations/mastodon",
|
||||||
|
"integration_type": "service",
|
||||||
"iot_class": "cloud_push",
|
"iot_class": "cloud_push",
|
||||||
"loggers": ["mastodon"],
|
"loggers": ["mastodon"],
|
||||||
"requirements": ["Mastodon.py==1.8.1"]
|
"requirements": ["Mastodon.py==1.8.1"]
|
||||||
|
@ -6,7 +6,7 @@ import mimetypes
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from mastodon import Mastodon
|
from mastodon import Mastodon
|
||||||
from mastodon.Mastodon import MastodonAPIError, MastodonUnauthorizedError
|
from mastodon.Mastodon import MastodonAPIError
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.notify import (
|
from homeassistant.components.notify import (
|
||||||
@ -14,12 +14,14 @@ from homeassistant.components.notify import (
|
|||||||
PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
|
PLATFORM_SCHEMA as NOTIFY_PLATFORM_SCHEMA,
|
||||||
BaseNotificationService,
|
BaseNotificationService,
|
||||||
)
|
)
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
from homeassistant.helpers import config_validation as cv, issue_registry as ir
|
||||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||||
|
|
||||||
from .const import CONF_BASE_URL, DEFAULT_URL, LOGGER
|
from .const import CONF_BASE_URL, DEFAULT_URL, DOMAIN, LOGGER
|
||||||
|
|
||||||
ATTR_MEDIA = "media"
|
ATTR_MEDIA = "media"
|
||||||
ATTR_TARGET = "target"
|
ATTR_TARGET = "target"
|
||||||
@ -35,39 +37,78 @@ PLATFORM_SCHEMA = NOTIFY_PLATFORM_SCHEMA.extend(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
INTEGRATION_TITLE = "Mastodon"
|
||||||
|
|
||||||
def get_service(
|
|
||||||
|
async def async_get_service(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
config: ConfigType,
|
config: ConfigType,
|
||||||
discovery_info: DiscoveryInfoType | None = None,
|
discovery_info: DiscoveryInfoType | None = None,
|
||||||
) -> MastodonNotificationService | None:
|
) -> MastodonNotificationService | None:
|
||||||
"""Get the Mastodon notification service."""
|
"""Get the Mastodon notification service."""
|
||||||
client_id = config.get(CONF_CLIENT_ID)
|
|
||||||
client_secret = config.get(CONF_CLIENT_SECRET)
|
|
||||||
access_token = config.get(CONF_ACCESS_TOKEN)
|
|
||||||
base_url = config.get(CONF_BASE_URL)
|
|
||||||
|
|
||||||
try:
|
if not discovery_info:
|
||||||
mastodon = Mastodon(
|
# Import config entry
|
||||||
client_id=client_id,
|
|
||||||
client_secret=client_secret,
|
import_result = await hass.config_entries.flow.async_init(
|
||||||
access_token=access_token,
|
DOMAIN,
|
||||||
api_base_url=base_url,
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=config,
|
||||||
)
|
)
|
||||||
mastodon.account_verify_credentials()
|
|
||||||
except MastodonUnauthorizedError:
|
if (
|
||||||
LOGGER.warning("Authentication failed")
|
import_result["type"] == FlowResultType.ABORT
|
||||||
|
and import_result["reason"] != "already_configured"
|
||||||
|
):
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
DOMAIN,
|
||||||
|
f"deprecated_yaml_import_issue_{import_result["reason"]}",
|
||||||
|
breaks_in_ha_version="2025.2.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key=f"deprecated_yaml_import_issue_{import_result["reason"]}",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": INTEGRATION_TITLE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
ir.async_create_issue(
|
||||||
|
hass,
|
||||||
|
HOMEASSISTANT_DOMAIN,
|
||||||
|
f"deprecated_yaml_{DOMAIN}",
|
||||||
|
breaks_in_ha_version="2025.2.0",
|
||||||
|
is_fixable=False,
|
||||||
|
issue_domain=DOMAIN,
|
||||||
|
severity=ir.IssueSeverity.WARNING,
|
||||||
|
translation_key="deprecated_yaml",
|
||||||
|
translation_placeholders={
|
||||||
|
"domain": DOMAIN,
|
||||||
|
"integration_title": INTEGRATION_TITLE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return MastodonNotificationService(mastodon)
|
client: Mastodon = discovery_info.get("client")
|
||||||
|
|
||||||
|
return MastodonNotificationService(hass, client)
|
||||||
|
|
||||||
|
|
||||||
class MastodonNotificationService(BaseNotificationService):
|
class MastodonNotificationService(BaseNotificationService):
|
||||||
"""Implement the notification service for Mastodon."""
|
"""Implement the notification service for Mastodon."""
|
||||||
|
|
||||||
def __init__(self, api: Mastodon) -> None:
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
client: Mastodon,
|
||||||
|
) -> None:
|
||||||
"""Initialize the service."""
|
"""Initialize the service."""
|
||||||
self._api = api
|
|
||||||
|
self.client = client
|
||||||
|
|
||||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||||
"""Toot a message, with media perhaps."""
|
"""Toot a message, with media perhaps."""
|
||||||
@ -96,7 +137,7 @@ class MastodonNotificationService(BaseNotificationService):
|
|||||||
|
|
||||||
if mediadata:
|
if mediadata:
|
||||||
try:
|
try:
|
||||||
self._api.status_post(
|
self.client.status_post(
|
||||||
message,
|
message,
|
||||||
media_ids=mediadata["id"],
|
media_ids=mediadata["id"],
|
||||||
sensitive=sensitive,
|
sensitive=sensitive,
|
||||||
@ -107,7 +148,7 @@ class MastodonNotificationService(BaseNotificationService):
|
|||||||
LOGGER.error("Unable to send message")
|
LOGGER.error("Unable to send message")
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
self._api.status_post(
|
self.client.status_post(
|
||||||
message, visibility=target, spoiler_text=content_warning
|
message, visibility=target, spoiler_text=content_warning
|
||||||
)
|
)
|
||||||
except MastodonAPIError:
|
except MastodonAPIError:
|
||||||
@ -118,7 +159,7 @@ class MastodonNotificationService(BaseNotificationService):
|
|||||||
with open(media_path, "rb"):
|
with open(media_path, "rb"):
|
||||||
media_type = self._media_type(media_path)
|
media_type = self._media_type(media_path)
|
||||||
try:
|
try:
|
||||||
mediadata = self._api.media_post(media_path, mime_type=media_type)
|
mediadata = self.client.media_post(media_path, mime_type=media_type)
|
||||||
except MastodonAPIError:
|
except MastodonAPIError:
|
||||||
LOGGER.error(f"Unable to upload image {media_path}")
|
LOGGER.error(f"Unable to upload image {media_path}")
|
||||||
|
|
||||||
|
39
homeassistant/components/mastodon/strings.json
Normal file
39
homeassistant/components/mastodon/strings.json
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"base_url": "[%key:common::config_flow::data::url%]",
|
||||||
|
"client_id": "Client Key",
|
||||||
|
"client_secret": "Client Secret",
|
||||||
|
"access_token": "[%key:common::config_flow::data::access_token%]"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"base_url": "The URL of your Mastodon instance."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"unauthorized_error": "The credentials are incorrect.",
|
||||||
|
"network_error": "The Mastodon instance was not found.",
|
||||||
|
"unknown": "Unknown error occured when connecting to the Mastodon instance."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"issues": {
|
||||||
|
"deprecated_yaml_import_issue_unauthorized_error": {
|
||||||
|
"title": "YAML import failed due to an authentication error",
|
||||||
|
"description": "Configuring {integration_title} using YAML is being removed but there was an authentication error while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration."
|
||||||
|
},
|
||||||
|
"deprecated_yaml_import_issue_network_error": {
|
||||||
|
"title": "YAML import failed because the instance was not found",
|
||||||
|
"description": "Configuring {integration_title} using YAML is being removed but no instance was found while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration."
|
||||||
|
},
|
||||||
|
"deprecated_yaml_import_issue_unknown": {
|
||||||
|
"title": "YAML import failed with unknown error",
|
||||||
|
"description": "Configuring {integration_title} using YAML is being removed but there was an unknown error while importing your existing configuration.\nPlease use the UI to configure Mastodon. Don't forget to delete the YAML configuration."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
32
homeassistant/components/mastodon/utils.py
Normal file
32
homeassistant/components/mastodon/utils.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
"""Mastodon util functions."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from mastodon import Mastodon
|
||||||
|
|
||||||
|
from .const import ACCOUNT_USERNAME, DEFAULT_NAME, INSTANCE_DOMAIN, INSTANCE_URI
|
||||||
|
|
||||||
|
|
||||||
|
def create_mastodon_client(
|
||||||
|
base_url: str, client_id: str, client_secret: str, access_token: str
|
||||||
|
) -> Mastodon:
|
||||||
|
"""Create a Mastodon client with the api base url."""
|
||||||
|
return Mastodon(
|
||||||
|
api_base_url=base_url,
|
||||||
|
client_id=client_id,
|
||||||
|
client_secret=client_secret,
|
||||||
|
access_token=access_token,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def construct_mastodon_username(
|
||||||
|
instance: dict[str, str] | None, account: dict[str, str] | None
|
||||||
|
) -> str:
|
||||||
|
"""Construct a mastodon username from the account and instance."""
|
||||||
|
if instance and account:
|
||||||
|
return (
|
||||||
|
f"@{account[ACCOUNT_USERNAME]}@"
|
||||||
|
f"{instance.get(INSTANCE_URI, instance.get(INSTANCE_DOMAIN))}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return DEFAULT_NAME
|
@ -330,6 +330,7 @@ FLOWS = {
|
|||||||
"lyric",
|
"lyric",
|
||||||
"madvr",
|
"madvr",
|
||||||
"mailgun",
|
"mailgun",
|
||||||
|
"mastodon",
|
||||||
"matter",
|
"matter",
|
||||||
"mealie",
|
"mealie",
|
||||||
"meater",
|
"meater",
|
||||||
|
@ -3495,8 +3495,8 @@
|
|||||||
},
|
},
|
||||||
"mastodon": {
|
"mastodon": {
|
||||||
"name": "Mastodon",
|
"name": "Mastodon",
|
||||||
"integration_type": "hub",
|
"integration_type": "service",
|
||||||
"config_flow": false,
|
"config_flow": true,
|
||||||
"iot_class": "cloud_push"
|
"iot_class": "cloud_push"
|
||||||
},
|
},
|
||||||
"matrix": {
|
"matrix": {
|
||||||
|
@ -21,6 +21,9 @@ HAP-python==4.9.1
|
|||||||
# homeassistant.components.tasmota
|
# homeassistant.components.tasmota
|
||||||
HATasmota==0.9.2
|
HATasmota==0.9.2
|
||||||
|
|
||||||
|
# homeassistant.components.mastodon
|
||||||
|
Mastodon.py==1.8.1
|
||||||
|
|
||||||
# homeassistant.components.doods
|
# homeassistant.components.doods
|
||||||
# homeassistant.components.generic
|
# homeassistant.components.generic
|
||||||
# homeassistant.components.image_upload
|
# homeassistant.components.image_upload
|
||||||
|
13
tests/components/mastodon/__init__.py
Normal file
13
tests/components/mastodon/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
"""Tests for the Mastodon integration."""
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) -> None:
|
||||||
|
"""Fixture for setting up the component."""
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
57
tests/components/mastodon/conftest.py
Normal file
57
tests/components/mastodon/conftest.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
"""Mastodon tests configuration."""
|
||||||
|
|
||||||
|
from collections.abc import Generator
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry, load_json_object_fixture
|
||||||
|
from tests.components.smhi.common import AsyncMock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||||
|
"""Override async_setup_entry."""
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.mastodon.async_setup_entry",
|
||||||
|
return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
yield mock_setup_entry
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_mastodon_client() -> Generator[AsyncMock]:
|
||||||
|
"""Mock a Mastodon client."""
|
||||||
|
with (
|
||||||
|
patch(
|
||||||
|
"homeassistant.components.mastodon.utils.Mastodon",
|
||||||
|
autospec=True,
|
||||||
|
) as mock_client,
|
||||||
|
):
|
||||||
|
client = mock_client.return_value
|
||||||
|
client.instance.return_value = load_json_object_fixture("instance.json", DOMAIN)
|
||||||
|
client.account_verify_credentials.return_value = load_json_object_fixture(
|
||||||
|
"account_verify_credentials.json", DOMAIN
|
||||||
|
)
|
||||||
|
client.status_post.return_value = None
|
||||||
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mock_config_entry() -> MockConfigEntry:
|
||||||
|
"""Mock a config entry."""
|
||||||
|
return MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="@trwnh@mastodon.social",
|
||||||
|
data={
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
},
|
||||||
|
entry_id="01J35M4AH9HYRC2V0G6RNVNWJH",
|
||||||
|
unique_id="client_id",
|
||||||
|
)
|
@ -0,0 +1,78 @@
|
|||||||
|
{
|
||||||
|
"id": "14715",
|
||||||
|
"username": "trwnh",
|
||||||
|
"acct": "trwnh",
|
||||||
|
"display_name": "infinite love ⴳ",
|
||||||
|
"locked": false,
|
||||||
|
"bot": false,
|
||||||
|
"created_at": "2016-11-24T10:02:12.085Z",
|
||||||
|
"note": "<p>i have approximate knowledge of many things. perpetual student. (nb/ace/they)</p><p>xmpp/email: a@trwnh.com<br /><a href=\"https://trwnh.com\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">trwnh.com</span><span class=\"invisible\"></span></a><br />help me live: <a href=\"https://liberapay.com/at\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">liberapay.com/at</span><span class=\"invisible\"></span></a> or <a href=\"https://paypal.me/trwnh\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">paypal.me/trwnh</span><span class=\"invisible\"></span></a></p><p>- my triggers are moths and glitter<br />- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise<br />- dm me if i did something wrong, so i can improve<br />- purest person on fedi, do not lewd in my presence<br />- #1 ami cole fan account</p><p>:fatyoshi:</p>",
|
||||||
|
"url": "https://mastodon.social/@trwnh",
|
||||||
|
"avatar": "https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png",
|
||||||
|
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/014/715/original/34aa222f4ae2e0a9.png",
|
||||||
|
"header": "https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg",
|
||||||
|
"header_static": "https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg",
|
||||||
|
"followers_count": 821,
|
||||||
|
"following_count": 178,
|
||||||
|
"statuses_count": 33120,
|
||||||
|
"last_status_at": "2019-11-24T15:49:42.251Z",
|
||||||
|
"source": {
|
||||||
|
"privacy": "public",
|
||||||
|
"sensitive": false,
|
||||||
|
"language": "",
|
||||||
|
"note": "i have approximate knowledge of many things. perpetual student. (nb/ace/they)\r\n\r\nxmpp/email: a@trwnh.com\r\nhttps://trwnh.com\r\nhelp me live: https://liberapay.com/at or https://paypal.me/trwnh\r\n\r\n- my triggers are moths and glitter\r\n- i have all notifs except mentions turned off, so please interact if you wanna be friends! i literally will not notice otherwise\r\n- dm me if i did something wrong, so i can improve\r\n- purest person on fedi, do not lewd in my presence\r\n- #1 ami cole fan account\r\n\r\n:fatyoshi:",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "https://trwnh.com",
|
||||||
|
"verified_at": "2019-08-29T04:14:55.571+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sponsor",
|
||||||
|
"value": "https://liberapay.com/at",
|
||||||
|
"verified_at": "2019-11-15T10:06:15.557+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fan of:",
|
||||||
|
"value": "Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)",
|
||||||
|
"verified_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Main topics:",
|
||||||
|
"value": "systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!",
|
||||||
|
"verified_at": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"follow_requests_count": 0
|
||||||
|
},
|
||||||
|
"emojis": [
|
||||||
|
{
|
||||||
|
"shortcode": "fatyoshi",
|
||||||
|
"url": "https://files.mastodon.social/custom_emojis/images/000/023/920/original/e57ecb623faa0dc9.png",
|
||||||
|
"static_url": "https://files.mastodon.social/custom_emojis/images/000/023/920/static/e57ecb623faa0dc9.png",
|
||||||
|
"visible_in_picker": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Website",
|
||||||
|
"value": "<a href=\"https://trwnh.com\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">trwnh.com</span><span class=\"invisible\"></span></a>",
|
||||||
|
"verified_at": "2019-08-29T04:14:55.571+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Sponsor",
|
||||||
|
"value": "<a href=\"https://liberapay.com/at\" rel=\"me nofollow noopener noreferrer\" target=\"_blank\"><span class=\"invisible\">https://</span><span class=\"\">liberapay.com/at</span><span class=\"invisible\"></span></a>",
|
||||||
|
"verified_at": "2019-11-15T10:06:15.557+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Fan of:",
|
||||||
|
"value": "Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo's Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)",
|
||||||
|
"verified_at": null
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Main topics:",
|
||||||
|
"value": "systemic analysis, design patterns, anticapitalism, info/tech freedom, theory and philosophy, and otherwise being a genuine and decent wholesome poster. i'm just here to hang out and talk to cool people!",
|
||||||
|
"verified_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
147
tests/components/mastodon/fixtures/instance.json
Normal file
147
tests/components/mastodon/fixtures/instance.json
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
{
|
||||||
|
"domain": "mastodon.social",
|
||||||
|
"title": "Mastodon",
|
||||||
|
"version": "4.0.0rc1",
|
||||||
|
"source_url": "https://github.com/mastodon/mastodon",
|
||||||
|
"description": "The original server operated by the Mastodon gGmbH non-profit",
|
||||||
|
"usage": {
|
||||||
|
"users": {
|
||||||
|
"active_month": 123122
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"thumbnail": {
|
||||||
|
"url": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
|
||||||
|
"blurhash": "UeKUpFxuo~R%0nW;WCnhF6RjaJt757oJodS$",
|
||||||
|
"versions": {
|
||||||
|
"@1x": "https://files.mastodon.social/site_uploads/files/000/000/001/@1x/57c12f441d083cde.png",
|
||||||
|
"@2x": "https://files.mastodon.social/site_uploads/files/000/000/001/@2x/57c12f441d083cde.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"languages": ["en"],
|
||||||
|
"configuration": {
|
||||||
|
"urls": {
|
||||||
|
"streaming": "wss://mastodon.social"
|
||||||
|
},
|
||||||
|
"vapid": {
|
||||||
|
"public_key": "BCkMmVdKDnKYwzVCDC99Iuc9GvId-x7-kKtuHnLgfF98ENiZp_aj-UNthbCdI70DqN1zUVis-x0Wrot2sBagkMc="
|
||||||
|
},
|
||||||
|
"accounts": {
|
||||||
|
"max_featured_tags": 10,
|
||||||
|
"max_pinned_statuses": 4
|
||||||
|
},
|
||||||
|
"statuses": {
|
||||||
|
"max_characters": 500,
|
||||||
|
"max_media_attachments": 4,
|
||||||
|
"characters_reserved_per_url": 23
|
||||||
|
},
|
||||||
|
"media_attachments": {
|
||||||
|
"supported_mime_types": [
|
||||||
|
"image/jpeg",
|
||||||
|
"image/png",
|
||||||
|
"image/gif",
|
||||||
|
"image/heic",
|
||||||
|
"image/heif",
|
||||||
|
"image/webp",
|
||||||
|
"video/webm",
|
||||||
|
"video/mp4",
|
||||||
|
"video/quicktime",
|
||||||
|
"video/ogg",
|
||||||
|
"audio/wave",
|
||||||
|
"audio/wav",
|
||||||
|
"audio/x-wav",
|
||||||
|
"audio/x-pn-wave",
|
||||||
|
"audio/vnd.wave",
|
||||||
|
"audio/ogg",
|
||||||
|
"audio/vorbis",
|
||||||
|
"audio/mpeg",
|
||||||
|
"audio/mp3",
|
||||||
|
"audio/webm",
|
||||||
|
"audio/flac",
|
||||||
|
"audio/aac",
|
||||||
|
"audio/m4a",
|
||||||
|
"audio/x-m4a",
|
||||||
|
"audio/mp4",
|
||||||
|
"audio/3gpp",
|
||||||
|
"video/x-ms-asf"
|
||||||
|
],
|
||||||
|
"image_size_limit": 10485760,
|
||||||
|
"image_matrix_limit": 16777216,
|
||||||
|
"video_size_limit": 41943040,
|
||||||
|
"video_frame_rate_limit": 60,
|
||||||
|
"video_matrix_limit": 2304000
|
||||||
|
},
|
||||||
|
"polls": {
|
||||||
|
"max_options": 4,
|
||||||
|
"max_characters_per_option": 50,
|
||||||
|
"min_expiration": 300,
|
||||||
|
"max_expiration": 2629746
|
||||||
|
},
|
||||||
|
"translation": {
|
||||||
|
"enabled": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"registrations": {
|
||||||
|
"enabled": false,
|
||||||
|
"approval_required": false,
|
||||||
|
"message": null
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"email": "staff@mastodon.social",
|
||||||
|
"account": {
|
||||||
|
"id": "1",
|
||||||
|
"username": "Gargron",
|
||||||
|
"acct": "Gargron",
|
||||||
|
"display_name": "Eugen 💀",
|
||||||
|
"locked": false,
|
||||||
|
"bot": false,
|
||||||
|
"discoverable": true,
|
||||||
|
"group": false,
|
||||||
|
"created_at": "2016-03-16T00:00:00.000Z",
|
||||||
|
"note": "<p>Founder, CEO and lead developer <span class=\"h-card\"><a href=\"https://mastodon.social/@Mastodon\" class=\"u-url mention\">@<span>Mastodon</span></a></span>, Germany.</p>",
|
||||||
|
"url": "https://mastodon.social/@Gargron",
|
||||||
|
"avatar": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg",
|
||||||
|
"avatar_static": "https://files.mastodon.social/accounts/avatars/000/000/001/original/dc4286ceb8fab734.jpg",
|
||||||
|
"header": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
|
||||||
|
"header_static": "https://files.mastodon.social/accounts/headers/000/000/001/original/3b91c9965d00888b.jpeg",
|
||||||
|
"followers_count": 133026,
|
||||||
|
"following_count": 311,
|
||||||
|
"statuses_count": 72605,
|
||||||
|
"last_status_at": "2022-10-31",
|
||||||
|
"noindex": false,
|
||||||
|
"emojis": [],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Patreon",
|
||||||
|
"value": "<a href=\"https://www.patreon.com/mastodon\" target=\"_blank\" rel=\"nofollow noopener noreferrer me\"><span class=\"invisible\">https://www.</span><span class=\"\">patreon.com/mastodon</span><span class=\"invisible\"></span></a>",
|
||||||
|
"verified_at": null
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"id": "1",
|
||||||
|
"text": "Sexually explicit or violent media must be marked as sensitive when posting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"text": "No racism, sexism, homophobia, transphobia, xenophobia, or casteism"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "3",
|
||||||
|
"text": "No incitement of violence or promotion of violent ideologies"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "4",
|
||||||
|
"text": "No harassment, dogpiling or doxxing of other users"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "5",
|
||||||
|
"text": "No content illegal in Germany"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "7",
|
||||||
|
"text": "Do not share intentionally false or misleading information"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
33
tests/components/mastodon/snapshots/test_init.ambr
Normal file
33
tests/components/mastodon/snapshots/test_init.ambr
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# serializer version: 1
|
||||||
|
# name: test_device_info
|
||||||
|
DeviceRegistryEntrySnapshot({
|
||||||
|
'area_id': None,
|
||||||
|
'config_entries': <ANY>,
|
||||||
|
'configuration_url': None,
|
||||||
|
'connections': set({
|
||||||
|
}),
|
||||||
|
'disabled_by': None,
|
||||||
|
'entry_type': <DeviceEntryType.SERVICE: 'service'>,
|
||||||
|
'hw_version': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'identifiers': set({
|
||||||
|
tuple(
|
||||||
|
'mastodon',
|
||||||
|
'client_id',
|
||||||
|
),
|
||||||
|
}),
|
||||||
|
'is_new': False,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'manufacturer': 'Mastodon gGmbH',
|
||||||
|
'model': '@trwnh@mastodon.social',
|
||||||
|
'model_id': None,
|
||||||
|
'name': 'Mastodon',
|
||||||
|
'name_by_user': None,
|
||||||
|
'primary_config_entry': <ANY>,
|
||||||
|
'serial_number': None,
|
||||||
|
'suggested_area': None,
|
||||||
|
'sw_version': '4.0.0rc1',
|
||||||
|
'via_device_id': None,
|
||||||
|
})
|
||||||
|
# ---
|
179
tests/components/mastodon/test_config_flow.py
Normal file
179
tests/components/mastodon/test_config_flow.py
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
"""Tests for the Mastodon config flow."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_CLIENT_ID, CONF_CLIENT_SECRET
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_full_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test full flow."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
assert result["title"] == "@trwnh@mastodon.social"
|
||||||
|
assert result["data"] == {
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
}
|
||||||
|
assert result["result"].unique_id == "client_id"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error"),
|
||||||
|
[
|
||||||
|
(MastodonNetworkError, "network_error"),
|
||||||
|
(MastodonUnauthorizedError, "unauthorized_error"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_flow_errors(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
error: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test flow errors."""
|
||||||
|
mock_mastodon_client.account_verify_credentials.side_effect = exception
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["errors"] == {"base": error}
|
||||||
|
|
||||||
|
mock_mastodon_client.account_verify_credentials.side_effect = None
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
async def test_duplicate(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test duplicate flow."""
|
||||||
|
mock_config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "client_id",
|
||||||
|
CONF_CLIENT_SECRET: "client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_flow(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test importing yaml config."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "import_client_id",
|
||||||
|
CONF_CLIENT_SECRET: "import_client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "import_access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "error"),
|
||||||
|
[
|
||||||
|
(MastodonNetworkError, "network_error"),
|
||||||
|
(MastodonUnauthorizedError, "unauthorized_error"),
|
||||||
|
(Exception, "unknown"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_import_flow_abort(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_setup_entry: AsyncMock,
|
||||||
|
exception: Exception,
|
||||||
|
error: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test importing yaml config abort."""
|
||||||
|
mock_mastodon_client.account_verify_credentials.side_effect = exception
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data={
|
||||||
|
CONF_BASE_URL: "https://mastodon.social",
|
||||||
|
CONF_CLIENT_ID: "import_client_id",
|
||||||
|
CONF_CLIENT_SECRET: "import_client_secret",
|
||||||
|
CONF_ACCESS_TOKEN: "import_access_token",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
25
tests/components/mastodon/test_init.py
Normal file
25
tests/components/mastodon/test_init.py
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
"""Tests for the Mastodon integration."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from mastodon.Mastodon import MastodonError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_initialization_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test initialization failure."""
|
||||||
|
mock_mastodon_client.instance.side_effect = MastodonError
|
||||||
|
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
|
38
tests/components/mastodon/test_notify.py
Normal file
38
tests/components/mastodon/test_notify.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
"""Tests for the Mastodon notify platform."""
|
||||||
|
|
||||||
|
from unittest.mock import AsyncMock
|
||||||
|
|
||||||
|
from syrupy.assertion import SnapshotAssertion
|
||||||
|
|
||||||
|
from homeassistant.components.notify import DOMAIN as NOTIFY_DOMAIN
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
from . import setup_integration
|
||||||
|
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_notify(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
entity_registry: er.EntityRegistry,
|
||||||
|
mock_mastodon_client: AsyncMock,
|
||||||
|
mock_config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test sending a message."""
|
||||||
|
await setup_integration(hass, mock_config_entry)
|
||||||
|
|
||||||
|
assert hass.services.has_service(NOTIFY_DOMAIN, "trwnh_mastodon_social")
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
NOTIFY_DOMAIN,
|
||||||
|
"trwnh_mastodon_social",
|
||||||
|
{
|
||||||
|
"message": "test toot",
|
||||||
|
},
|
||||||
|
blocking=True,
|
||||||
|
return_response=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert mock_mastodon_client.status_post.assert_called_once
|
Loading…
x
Reference in New Issue
Block a user