Fix endpoint deprecation warning in Mastodon (#151275)

This commit is contained in:
Andrew Jackson
2025-08-28 10:59:37 +01:00
committed by GitHub
parent da65c52f2d
commit 5fbb99a79a
8 changed files with 205 additions and 11 deletions

View File

@@ -2,7 +2,14 @@
from __future__ import annotations from __future__ import annotations
from mastodon.Mastodon import Account, Instance, InstanceV2, Mastodon, MastodonError from mastodon.Mastodon import (
Account,
Instance,
InstanceV2,
Mastodon,
MastodonError,
MastodonNotFoundError,
)
from homeassistant.const import ( from homeassistant.const import (
CONF_ACCESS_TOKEN, CONF_ACCESS_TOKEN,
@@ -105,7 +112,11 @@ def setup_mastodon(
entry.data[CONF_ACCESS_TOKEN], entry.data[CONF_ACCESS_TOKEN],
) )
instance = client.instance() try:
instance = client.instance_v2()
except MastodonNotFoundError:
instance = client.instance_v1()
account = client.account_verify_credentials() account = client.account_verify_credentials()
return client, instance, account return client, instance, account

View File

@@ -7,7 +7,9 @@ from typing import Any
from mastodon.Mastodon import ( from mastodon.Mastodon import (
Account, Account,
Instance, Instance,
InstanceV2,
MastodonNetworkError, MastodonNetworkError,
MastodonNotFoundError,
MastodonUnauthorizedError, MastodonUnauthorizedError,
) )
import voluptuous as vol import voluptuous as vol
@@ -61,7 +63,7 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
client_secret: str, client_secret: str,
access_token: str, access_token: str,
) -> tuple[ ) -> tuple[
Instance | None, InstanceV2 | Instance | None,
Account | None, Account | None,
dict[str, str], dict[str, str],
]: ]:
@@ -73,7 +75,10 @@ class MastodonConfigFlow(ConfigFlow, domain=DOMAIN):
client_secret, client_secret,
access_token, access_token,
) )
instance = client.instance() try:
instance = client.instance_v2()
except MastodonNotFoundError:
instance = client.instance_v1()
account = client.account_verify_credentials() account = client.account_verify_credentials()
except MastodonNetworkError: except MastodonNetworkError:

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from mastodon.Mastodon import Account, Instance from mastodon.Mastodon import Account, Instance, InstanceV2, MastodonNotFoundError
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -27,11 +27,16 @@ async def async_get_config_entry_diagnostics(
} }
def get_diagnostics(config_entry: MastodonConfigEntry) -> tuple[Instance, Account]: def get_diagnostics(
config_entry: MastodonConfigEntry,
) -> tuple[InstanceV2 | Instance, Account]:
"""Get mastodon diagnostics.""" """Get mastodon diagnostics."""
client = config_entry.runtime_data.client client = config_entry.runtime_data.client
instance = client.instance() try:
instance = client.instance_v2()
except MastodonNotFoundError:
instance = client.instance_v1()
account = client.account_verify_credentials() account = client.account_verify_credentials()
return instance, account return instance, account

View File

@@ -32,12 +32,16 @@ def mock_mastodon_client() -> Generator[AsyncMock]:
) as mock_client, ) as mock_client,
): ):
client = mock_client.return_value client = mock_client.return_value
client.instance.return_value = InstanceV2.from_json( client.instance_v1.return_value = InstanceV2.from_json(
load_fixture("instance.json", DOMAIN)
)
client.instance_v2.return_value = InstanceV2.from_json(
load_fixture("instance.json", DOMAIN) load_fixture("instance.json", DOMAIN)
) )
client.account_verify_credentials.return_value = Account.from_json( client.account_verify_credentials.return_value = Account.from_json(
load_fixture("account_verify_credentials.json", DOMAIN) load_fixture("account_verify_credentials.json", DOMAIN)
) )
client.mastodon_api_version = 2
client.status_post.return_value = None client.status_post.return_value = None
yield client yield client

View File

@@ -83,3 +83,87 @@
}), }),
}) })
# --- # ---
# name: test_entry_diagnostics_fallback_to_instance_v1
dict({
'account': dict({
'acct': 'trwnh',
'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
'bot': True,
'created_at': '2016-11-24T00:00:00+00:00',
'discoverable': True,
'display_name': 'infinite love ⴳ',
'emojis': list([
]),
'fields': list([
dict({
'name': 'Website',
'value': '<a href="https://trwnh.com" target="_blank" rel="nofollow noopener me" translate="no"><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',
}),
dict({
'name': 'Portfolio',
'value': '<a href="https://abdullahtarawneh.com" target="_blank" rel="nofollow noopener me" translate="no"><span class="invisible">https://</span><span class="">abdullahtarawneh.com</span><span class="invisible"></span></a>',
'verified_at': '2021-02-11T20:34:13.574+00:00',
}),
dict({
'name': 'Fan of:',
'value': 'Punk-rock and post-hardcore (Circa Survive, letlive., La Dispute, THE FEVER 333)Manga (Yu-Gi-Oh!, One Piece, JoJo&#39;s Bizarre Adventure, Death Note, Shaman King)Platformers and RPGs (Banjo-Kazooie, Boktai, Final Fantasy Crystal Chronicles)',
'verified_at': None,
}),
dict({
'name': 'What to expect:',
'value': 'talking about various things i find interesting, and otherwise being a genuine and decent wholesome poster. i&#39;m just here to hang out and talk to cool people! and to spill my thoughts.',
'verified_at': None,
}),
]),
'followers_count': 3169,
'following_count': 328,
'group': False,
'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',
'hide_collections': True,
'id': '14715',
'indexable': False,
'last_status_at': '2025-03-04T00:00:00',
'limited': None,
'locked': False,
'memorial': None,
'moved': None,
'moved_to_account': None,
'mute_expires_at': None,
'noindex': False,
'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" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="">trwnh.com</span><span class="invisible"></span></a><br />help me live:<br />- <a href="https://donate.stripe.com/4gwcPCaMpcQ19RC4gg" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="ellipsis">donate.stripe.com/4gwcPCaMpcQ1</span><span class="invisible">9RC4gg</span></a><br />- <a href="https://liberapay.com/trwnh" target="_blank" rel="nofollow noopener" translate="no"><span class="invisible">https://</span><span class="">liberapay.com/trwnh</span><span class="invisible"></span></a></p><p>notes:<br />- 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</p>',
'role': None,
'roles': list([
]),
'source': None,
'statuses_count': 69523,
'suspended': None,
'uri': 'https://mastodon.social/users/trwnh',
'url': 'https://mastodon.social/@trwnh',
'username': 'trwnh',
}),
'instance': dict({
'api_versions': None,
'configuration': None,
'contact': None,
'description': 'The original server operated by the Mastodon gGmbH non-profit',
'domain': 'mastodon.social',
'icon': None,
'languages': None,
'registrations': None,
'rules': None,
'source_url': 'https://github.com/mastodon/mastodon',
'thumbnail': None,
'title': 'Mastodon',
'uri': 'mastodon.social',
'usage': dict({
'users': dict({
'active_month': 380143,
}),
}),
'version': '4.4.0-nightly.2025-02-07',
}),
})
# ---

View File

@@ -2,7 +2,11 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError from mastodon.Mastodon import (
MastodonNetworkError,
MastodonNotFoundError,
MastodonUnauthorizedError,
)
import pytest import pytest
from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN
@@ -80,6 +84,46 @@ async def test_full_flow_with_path(
assert result["result"].unique_id == "trwnh_mastodon_social" assert result["result"].unique_id == "trwnh_mastodon_social"
async def test_full_flow_fallback_to_instance_v1(
hass: HomeAssistant,
mock_mastodon_client: AsyncMock,
mock_setup_entry: AsyncMock,
) -> None:
"""Test full flow where instance_v2 fails and falls back to instance_v1."""
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
"Instance API v2 not found"
)
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 == "trwnh_mastodon_social"
mock_mastodon_client.instance_v2.assert_called_once()
mock_mastodon_client.instance_v1.assert_called_once()
@pytest.mark.parametrize( @pytest.mark.parametrize(
("exception", "error"), ("exception", "error"),
[ [

View File

@@ -2,6 +2,7 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from mastodon.Mastodon import MastodonNotFoundError
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
@@ -26,3 +27,26 @@ async def test_entry_diagnostics(
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry) await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
== snapshot == snapshot
) )
async def test_entry_diagnostics_fallback_to_instance_v1(
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
mock_mastodon_client: AsyncMock,
mock_config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
) -> None:
"""Test config entry diagnostics with fallback to instance_v1 when instance_v2 raises MastodonNotFoundError."""
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
"Instance v2 not found"
)
await setup_integration(hass, mock_config_entry)
diagnostics_result = await get_diagnostics_for_config_entry(
hass, hass_client, mock_config_entry
)
mock_mastodon_client.instance_v1.assert_called()
assert diagnostics_result == snapshot

View File

@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from mastodon.Mastodon import MastodonError from mastodon.Mastodon import MastodonNotFoundError
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
from homeassistant.components.mastodon.config_flow import MastodonConfigFlow from homeassistant.components.mastodon.config_flow import MastodonConfigFlow
@@ -39,13 +39,30 @@ async def test_initialization_failure(
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
) -> None: ) -> None:
"""Test initialization failure.""" """Test initialization failure."""
mock_mastodon_client.instance.side_effect = MastodonError mock_mastodon_client.instance_v1.side_effect = MastodonNotFoundError
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_setup_integration_fallback_to_instance_v1(
hass: HomeAssistant,
mock_mastodon_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test full flow where instance_v2 fails and falls back to instance_v1."""
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
"Instance API v2 not found"
)
await setup_integration(hass, mock_config_entry)
mock_mastodon_client.instance_v2.assert_called_once()
mock_mastodon_client.instance_v1.assert_called_once()
async def test_migrate( async def test_migrate(
hass: HomeAssistant, hass: HomeAssistant,
mock_mastodon_client: AsyncMock, mock_mastodon_client: AsyncMock,