mirror of
https://github.com/home-assistant/core.git
synced 2026-05-16 21:41:44 +01:00
f8e5165ec7
Co-authored-by: Erwin Douna <e.douna@gmail.com> Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
901 lines
28 KiB
Python
901 lines
28 KiB
Python
"""Tests for the Mastodon services."""
|
|
|
|
from datetime import timedelta
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
from mastodon.Mastodon import (
|
|
MastodonAPIError,
|
|
MastodonNotFoundError,
|
|
MastodonUnauthorizedError,
|
|
MediaAttachment,
|
|
)
|
|
import pytest
|
|
from syrupy.assertion import SnapshotAssertion
|
|
|
|
from homeassistant.components import camera, image, media_source
|
|
from homeassistant.components.mastodon.const import (
|
|
ATTR_ACCOUNT_NAME,
|
|
ATTR_ATTRIBUTION_DOMAINS,
|
|
ATTR_AVATAR,
|
|
ATTR_AVATAR_MIME_TYPE,
|
|
ATTR_BOT,
|
|
ATTR_CONTENT_WARNING,
|
|
ATTR_DISCOVERABLE,
|
|
ATTR_DISPLAY_NAME,
|
|
ATTR_DURATION,
|
|
ATTR_FIELDS,
|
|
ATTR_HEADER,
|
|
ATTR_HEADER_MIME_TYPE,
|
|
ATTR_HIDE_NOTIFICATIONS,
|
|
ATTR_IDEMPOTENCY_KEY,
|
|
ATTR_LANGUAGE,
|
|
ATTR_LOCKED,
|
|
ATTR_MEDIA,
|
|
ATTR_MEDIA_DESCRIPTION,
|
|
ATTR_NOTE,
|
|
ATTR_QUOTE_APPROVAL_POLICY,
|
|
ATTR_STATUS,
|
|
ATTR_VISIBILITY,
|
|
DOMAIN,
|
|
)
|
|
from homeassistant.components.mastodon.services import (
|
|
SERVICE_GET_ACCOUNT,
|
|
SERVICE_MUTE_ACCOUNT,
|
|
SERVICE_POST,
|
|
SERVICE_UNMUTE_ACCOUNT,
|
|
SERVICE_UPDATE_PROFILE,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntryState
|
|
from homeassistant.const import ATTR_CONFIG_ENTRY_ID
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from . import setup_integration
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
|
async def test_get_account_success(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
snapshot: SnapshotAssertion,
|
|
) -> None:
|
|
"""Test the get_account service successfully returns account data."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
response = await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_GET_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
assert response == snapshot
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
|
|
|
|
async def test_get_account_failure(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the get_account service handles API errors."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
# Test API error (this is the only error type currently caught by the service)
|
|
mock_mastodon_client.account_lookup.side_effect = MastodonAPIError("API error")
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match='Unable to get account "@test@mastodon.social"',
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_GET_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@test@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
(
|
|
"service_data",
|
|
"expected_notifications",
|
|
"expected_duration",
|
|
),
|
|
[
|
|
(
|
|
{ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social"},
|
|
True,
|
|
None,
|
|
),
|
|
(
|
|
{
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
ATTR_HIDE_NOTIFICATIONS: False,
|
|
},
|
|
False,
|
|
None,
|
|
),
|
|
(
|
|
{
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
ATTR_DURATION: timedelta(hours=2),
|
|
},
|
|
True,
|
|
7200,
|
|
),
|
|
(
|
|
{
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
ATTR_DURATION: timedelta(hours=12),
|
|
ATTR_HIDE_NOTIFICATIONS: False,
|
|
},
|
|
False,
|
|
43200,
|
|
),
|
|
],
|
|
)
|
|
async def test_mute_account_success(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
snapshot: SnapshotAssertion,
|
|
service_data: dict[str, str | int | bool],
|
|
expected_notifications: bool,
|
|
expected_duration: int | None,
|
|
) -> None:
|
|
"""Test the mute_account service mutes the target account with all options."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_MUTE_ACCOUNT,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | service_data,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct=service_data[ATTR_ACCOUNT_NAME]
|
|
)
|
|
account = mock_mastodon_client.account_lookup.return_value
|
|
assert mock_mastodon_client.account_mute.call_count == 1
|
|
call_args, call_kwargs = mock_mastodon_client.account_mute.call_args
|
|
|
|
if call_kwargs:
|
|
actual_id = call_kwargs["id"]
|
|
actual_notifications = call_kwargs["notifications"]
|
|
actual_duration = call_kwargs.get("duration")
|
|
else:
|
|
_, positional_args, _ = call_args
|
|
actual_id, actual_notifications, actual_duration = positional_args
|
|
|
|
assert actual_id == account.id
|
|
assert actual_notifications == expected_notifications
|
|
assert actual_duration == expected_duration
|
|
|
|
|
|
async def test_mute_account_duration_too_long(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test mute_account rejects overly long durations."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
with (
|
|
patch("homeassistant.components.mastodon.services.MAX_DURATION_SECONDS", 5),
|
|
pytest.raises(ServiceValidationError) as err,
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_MUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
ATTR_DURATION: timedelta(seconds=10),
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
assert err.value.translation_key == "mute_duration_too_long"
|
|
mock_mastodon_client.account_mute.assert_not_called()
|
|
|
|
|
|
async def test_mute_account_failure_not_found(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test mute_account raises validation when account does not exist."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
mock_mastodon_client.account_lookup.side_effect = MastodonNotFoundError(
|
|
"account not found"
|
|
)
|
|
|
|
with pytest.raises(ServiceValidationError):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_MUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
mock_mastodon_client.account_mute.assert_not_called()
|
|
|
|
|
|
async def test_mute_account_failure_api_error(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test mute_account wraps API errors with translated message."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
mock_mastodon_client.account_mute.side_effect = MastodonAPIError("mute failed")
|
|
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match='Unable to mute account "@trwnh@mastodon.social"',
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_MUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
account = mock_mastodon_client.account_lookup.return_value
|
|
mock_mastodon_client.account_mute.assert_called_once_with(
|
|
id=account.id, notifications=True, duration=None
|
|
)
|
|
|
|
|
|
async def test_unmute_account_success(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the unmute_account service unmutes the target account."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UNMUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
account = mock_mastodon_client.account_lookup.return_value
|
|
mock_mastodon_client.account_unmute.assert_called_once_with(id=account.id)
|
|
|
|
|
|
async def test_unmute_account_failure_not_found(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test unmute_account raises validation when account does not exist."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
mock_mastodon_client.account_lookup.side_effect = MastodonNotFoundError(
|
|
"account not found"
|
|
)
|
|
|
|
with pytest.raises(ServiceValidationError):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UNMUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
mock_mastodon_client.account_unmute.assert_not_called()
|
|
|
|
|
|
async def test_unmute_account_failure_api_error(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test unmute_account wraps API errors with translated message."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
mock_mastodon_client.account_unmute.side_effect = MastodonAPIError("unmute failed")
|
|
|
|
with pytest.raises(
|
|
HomeAssistantError,
|
|
match='Unable to unmute account "@trwnh@mastodon.social"',
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UNMUTE_ACCOUNT,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_ACCOUNT_NAME: "@trwnh@mastodon.social",
|
|
},
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.account_lookup.assert_called_once_with(
|
|
acct="@trwnh@mastodon.social"
|
|
)
|
|
account = mock_mastodon_client.account_lookup.return_value
|
|
mock_mastodon_client.account_unmute.assert_called_once_with(id=account.id)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("payload", "kwargs"),
|
|
[
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": None,
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"language": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{ATTR_STATUS: "test toot", ATTR_VISIBILITY: "private"},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": None,
|
|
"visibility": "private",
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"language": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
ATTR_CONTENT_WARNING: "Spoiler",
|
|
ATTR_VISIBILITY: "private",
|
|
},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": "Spoiler",
|
|
"visibility": "private",
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"language": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
ATTR_CONTENT_WARNING: "Spoiler",
|
|
ATTR_LANGUAGE: "nl",
|
|
ATTR_MEDIA: "/image.jpg",
|
|
},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": "Spoiler",
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"language": "nl",
|
|
"media_ids": "1",
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
ATTR_CONTENT_WARNING: "Spoiler",
|
|
ATTR_LANGUAGE: "en",
|
|
ATTR_MEDIA: "/image.jpg",
|
|
ATTR_MEDIA_DESCRIPTION: "A test image",
|
|
},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": "Spoiler",
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"language": "en",
|
|
"media_ids": "1",
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{ATTR_STATUS: "test toot", ATTR_LANGUAGE: "invalid-lang"},
|
|
{
|
|
"status": "test toot",
|
|
"language": "invalid-lang",
|
|
"spoiler_text": None,
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot\nwith idempotency",
|
|
ATTR_IDEMPOTENCY_KEY: "post_once_only",
|
|
},
|
|
{
|
|
"status": "test toot\nwith idempotency",
|
|
"idempotency_key": "post_once_only",
|
|
"language": None,
|
|
"spoiler_text": None,
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
(
|
|
{ATTR_STATUS: "test toot", ATTR_QUOTE_APPROVAL_POLICY: "followers"},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": None,
|
|
"visibility": None,
|
|
"quote_approval_policy": "followers",
|
|
"idempotency_key": None,
|
|
"language": None,
|
|
"media_ids": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
async def test_service_post(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
payload: dict[str, str],
|
|
kwargs: dict[str, str | None],
|
|
) -> None:
|
|
"""Test the post service."""
|
|
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
with (
|
|
patch.object(hass.config, "is_allowed_path", return_value=True),
|
|
patch.object(
|
|
mock_mastodon_client, "media_post", return_value=MediaAttachment(id="1")
|
|
),
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
}
|
|
| payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
mock_mastodon_client.status_post.assert_called_with(**kwargs)
|
|
|
|
mock_mastodon_client.status_post.reset_mock()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("payload", "kwargs"),
|
|
[
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
},
|
|
{"status": "test toot", "spoiler_text": None, "visibility": None},
|
|
),
|
|
(
|
|
{
|
|
ATTR_STATUS: "test toot",
|
|
ATTR_CONTENT_WARNING: "Spoiler",
|
|
ATTR_MEDIA: "/image.jpg",
|
|
},
|
|
{
|
|
"status": "test toot",
|
|
"spoiler_text": "Spoiler",
|
|
"visibility": None,
|
|
"quote_approval_policy": None,
|
|
"idempotency_key": None,
|
|
"media_ids": "1",
|
|
"media_description": None,
|
|
"sensitive": None,
|
|
},
|
|
),
|
|
],
|
|
)
|
|
async def test_post_service_failed(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
payload: dict[str, str],
|
|
kwargs: dict[str, str | None],
|
|
) -> None:
|
|
"""Test the post service raising an error."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
hass.config.is_allowed_path = Mock(return_value=True)
|
|
mock_mastodon_client.media_post.return_value = MediaAttachment(id="1")
|
|
|
|
mock_mastodon_client.status_post.side_effect = MastodonAPIError
|
|
|
|
with pytest.raises(HomeAssistantError, match="Unable to send message"):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
|
|
async def test_post_media_upload_failed(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the post service raising an error because media upload fails."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
payload = {"status": "test toot", "media": "/fail.jpg"}
|
|
|
|
mock_mastodon_client.media_post.side_effect = MastodonAPIError
|
|
|
|
with (
|
|
patch.object(hass.config, "is_allowed_path", return_value=True),
|
|
pytest.raises(HomeAssistantError, match="Unable to upload image /fail.jpg"),
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
|
|
async def test_post_path_not_whitelisted(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the post service raising an error because the file path is not whitelisted."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
payload = {"status": "test toot", "media": "/fail.jpg"}
|
|
|
|
with pytest.raises(
|
|
HomeAssistantError, match="/fail.jpg is not a whitelisted directory"
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
|
|
async def test_idempotency_key_too_short(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the post service raising an error because the idempotency key is too short."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
payload = {"status": "test toot", "idempotency_key": "abc"}
|
|
|
|
with pytest.raises(
|
|
ServiceValidationError,
|
|
match="Idempotency key must be at least 4 characters long",
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
|
|
|
|
async def test_service_entry_availability(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the services without valid entry."""
|
|
mock_config_entry.add_to_hass(hass)
|
|
mock_config_entry2 = MockConfigEntry(domain=DOMAIN)
|
|
mock_config_entry2.add_to_hass(hass)
|
|
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
payload = {"status": "test toot"}
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
assert err.value.translation_key == "service_config_entry_not_loaded"
|
|
|
|
with pytest.raises(ServiceValidationError) as err:
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_POST,
|
|
{ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload,
|
|
blocking=True,
|
|
return_response=False,
|
|
)
|
|
assert err.value.translation_key == "service_config_entry_not_found"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("payload", "kwargs"),
|
|
[
|
|
(
|
|
{ATTR_DISPLAY_NAME: "Test User"},
|
|
{ATTR_DISPLAY_NAME: "Test User"},
|
|
),
|
|
(
|
|
{ATTR_NOTE: "bio"},
|
|
{ATTR_NOTE: "bio"},
|
|
),
|
|
(
|
|
{ATTR_LOCKED: True},
|
|
{ATTR_LOCKED: True},
|
|
),
|
|
(
|
|
{ATTR_BOT: False},
|
|
{ATTR_BOT: False},
|
|
),
|
|
(
|
|
{ATTR_DISCOVERABLE: True},
|
|
{ATTR_DISCOVERABLE: True},
|
|
),
|
|
(
|
|
{ATTR_FIELDS: [{"name": "Pronouns", "value": "He/Him, They/Them"}]},
|
|
{ATTR_FIELDS: [("Pronouns", "He/Him, They/Them")]},
|
|
),
|
|
(
|
|
{ATTR_ATTRIBUTION_DOMAINS: ["example.com", "test.com"]},
|
|
{ATTR_ATTRIBUTION_DOMAINS: ["example.com", "test.com"]},
|
|
),
|
|
(
|
|
{
|
|
ATTR_AVATAR: {
|
|
"media_content_id": "media-source://camera/camera.demo_camera",
|
|
"media_content_type": "image/jpeg",
|
|
}
|
|
},
|
|
{ATTR_AVATAR: b"I play the sax\n", ATTR_AVATAR_MIME_TYPE: "image/jpeg"},
|
|
),
|
|
(
|
|
{
|
|
ATTR_AVATAR: {
|
|
"media_content_id": "media-source://media_source/local/screenshot.jpg",
|
|
"media_content_type": "image/png",
|
|
}
|
|
},
|
|
{
|
|
ATTR_AVATAR: Path(
|
|
"tests/testing_config/media/screenshot.jpg"
|
|
).resolve(),
|
|
ATTR_AVATAR_MIME_TYPE: "image/jpeg",
|
|
},
|
|
),
|
|
(
|
|
{
|
|
ATTR_AVATAR: {
|
|
"media_content_id": "media-source://image/image.test",
|
|
"media_content_type": "image/png",
|
|
}
|
|
},
|
|
{ATTR_AVATAR: b"\x89PNG", ATTR_AVATAR_MIME_TYPE: "image/png"},
|
|
),
|
|
(
|
|
{
|
|
ATTR_HEADER: {
|
|
"media_content_id": "media-source://camera/camera.demo_camera",
|
|
"media_content_type": "image/jpeg",
|
|
}
|
|
},
|
|
{ATTR_HEADER: b"I play the sax\n", ATTR_HEADER_MIME_TYPE: "image/jpeg"},
|
|
),
|
|
(
|
|
{
|
|
ATTR_HEADER: {
|
|
"media_content_id": "media-source://image/image.test",
|
|
"media_content_type": "image/png",
|
|
}
|
|
},
|
|
{ATTR_HEADER: b"\x89PNG", ATTR_HEADER_MIME_TYPE: "image/png"},
|
|
),
|
|
(
|
|
{
|
|
ATTR_HEADER: {
|
|
"media_content_id": "media-source://media_source/local/screenshot.jpg",
|
|
"media_content_type": "image/png",
|
|
}
|
|
},
|
|
{
|
|
ATTR_HEADER: Path(
|
|
"tests/testing_config/media/screenshot.jpg"
|
|
).resolve(),
|
|
ATTR_HEADER_MIME_TYPE: "image/jpeg",
|
|
},
|
|
),
|
|
],
|
|
)
|
|
async def test_service_update_profile(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
payload: dict[str, str],
|
|
kwargs: dict[str, str | None],
|
|
) -> None:
|
|
"""Test the update profile service."""
|
|
assert await async_setup_component(hass, "media_source", {})
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
with (
|
|
patch(
|
|
"homeassistant.components.camera.async_get_image",
|
|
return_value=camera.Image("image/jpeg", b"I play the sax\n"),
|
|
),
|
|
patch(
|
|
"homeassistant.components.image.async_get_image",
|
|
return_value=image.Image(content_type="image/png", content=b"\x89PNG"),
|
|
),
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UPDATE_PROFILE,
|
|
{ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, **payload},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
mock_mastodon_client.account_update_credentials.assert_called_with(**kwargs)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("exception", "translation_key"),
|
|
[
|
|
(MastodonAPIError, "unable_to_update_profile"),
|
|
(MastodonUnauthorizedError, "auth_failed"),
|
|
],
|
|
)
|
|
async def test_service_update_profile_exceptions(
|
|
hass: HomeAssistant,
|
|
mock_mastodon_client: AsyncMock,
|
|
mock_config_entry: MockConfigEntry,
|
|
exception: type[Exception],
|
|
translation_key: str,
|
|
) -> None:
|
|
"""Test the update profile service exceptions."""
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
mock_mastodon_client.account_update_credentials.side_effect = exception
|
|
|
|
with pytest.raises(HomeAssistantError) as err:
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UPDATE_PROFILE,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_DISPLAY_NAME: "Test User",
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
|
|
assert err.value.translation_key == translation_key
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_mastodon_client")
|
|
async def test_service_update_profile_media_source_not_supported(
|
|
hass: HomeAssistant,
|
|
mock_config_entry: MockConfigEntry,
|
|
) -> None:
|
|
"""Test the update profile service with unsupported media source."""
|
|
assert await async_setup_component(hass, "tts", {})
|
|
await setup_integration(hass, mock_config_entry)
|
|
|
|
assert mock_config_entry.state is ConfigEntryState.LOADED
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.mastodon.services.async_resolve_media",
|
|
return_value=media_source.PlayMedia(
|
|
url="/api/tts_proxy/WDyphPCh3sAoO3koDY87ew.mp3",
|
|
mime_type="audio/mpeg",
|
|
path=None,
|
|
),
|
|
),
|
|
pytest.raises(HomeAssistantError) as err,
|
|
):
|
|
await hass.services.async_call(
|
|
DOMAIN,
|
|
SERVICE_UPDATE_PROFILE,
|
|
{
|
|
ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id,
|
|
ATTR_AVATAR: {
|
|
"media_content_id": "media-source://tts/demo?message=Hello+world%21&language=en",
|
|
"media_content_type": "audio/mp3",
|
|
},
|
|
},
|
|
blocking=True,
|
|
return_response=True,
|
|
)
|
|
assert err.value.translation_key == "media_source_not_supported"
|