Skip to content

Commit 5fbb99a

Browse files
Fix endpoint deprecation warning in Mastodon (home-assistant#151275)
1 parent da65c52 commit 5fbb99a

File tree

8 files changed

+205
-11
lines changed

8 files changed

+205
-11
lines changed

homeassistant/components/mastodon/__init__.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@
22

33
from __future__ import annotations
44

5-
from mastodon.Mastodon import Account, Instance, InstanceV2, Mastodon, MastodonError
5+
from mastodon.Mastodon import (
6+
Account,
7+
Instance,
8+
InstanceV2,
9+
Mastodon,
10+
MastodonError,
11+
MastodonNotFoundError,
12+
)
613

714
from homeassistant.const import (
815
CONF_ACCESS_TOKEN,
@@ -105,7 +112,11 @@ def setup_mastodon(
105112
entry.data[CONF_ACCESS_TOKEN],
106113
)
107114

108-
instance = client.instance()
115+
try:
116+
instance = client.instance_v2()
117+
except MastodonNotFoundError:
118+
instance = client.instance_v1()
119+
109120
account = client.account_verify_credentials()
110121

111122
return client, instance, account

homeassistant/components/mastodon/config_flow.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
from mastodon.Mastodon import (
88
Account,
99
Instance,
10+
InstanceV2,
1011
MastodonNetworkError,
12+
MastodonNotFoundError,
1113
MastodonUnauthorizedError,
1214
)
1315
import voluptuous as vol
@@ -61,7 +63,7 @@ def check_connection(
6163
client_secret: str,
6264
access_token: str,
6365
) -> tuple[
64-
Instance | None,
66+
InstanceV2 | Instance | None,
6567
Account | None,
6668
dict[str, str],
6769
]:
@@ -73,7 +75,10 @@ def check_connection(
7375
client_secret,
7476
access_token,
7577
)
76-
instance = client.instance()
78+
try:
79+
instance = client.instance_v2()
80+
except MastodonNotFoundError:
81+
instance = client.instance_v1()
7782
account = client.account_verify_credentials()
7883

7984
except MastodonNetworkError:

homeassistant/components/mastodon/diagnostics.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from typing import Any
66

7-
from mastodon.Mastodon import Account, Instance
7+
from mastodon.Mastodon import Account, Instance, InstanceV2, MastodonNotFoundError
88

99
from homeassistant.core import HomeAssistant
1010

@@ -27,11 +27,16 @@ async def async_get_config_entry_diagnostics(
2727
}
2828

2929

30-
def get_diagnostics(config_entry: MastodonConfigEntry) -> tuple[Instance, Account]:
30+
def get_diagnostics(
31+
config_entry: MastodonConfigEntry,
32+
) -> tuple[InstanceV2 | Instance, Account]:
3133
"""Get mastodon diagnostics."""
3234
client = config_entry.runtime_data.client
3335

34-
instance = client.instance()
36+
try:
37+
instance = client.instance_v2()
38+
except MastodonNotFoundError:
39+
instance = client.instance_v1()
3540
account = client.account_verify_credentials()
3641

3742
return instance, account

tests/components/mastodon/conftest.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ def mock_mastodon_client() -> Generator[AsyncMock]:
3232
) as mock_client,
3333
):
3434
client = mock_client.return_value
35-
client.instance.return_value = InstanceV2.from_json(
35+
client.instance_v1.return_value = InstanceV2.from_json(
36+
load_fixture("instance.json", DOMAIN)
37+
)
38+
client.instance_v2.return_value = InstanceV2.from_json(
3639
load_fixture("instance.json", DOMAIN)
3740
)
3841
client.account_verify_credentials.return_value = Account.from_json(
3942
load_fixture("account_verify_credentials.json", DOMAIN)
4043
)
44+
client.mastodon_api_version = 2
4145
client.status_post.return_value = None
4246
yield client
4347

tests/components/mastodon/snapshots/test_diagnostics.ambr

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,87 @@
8383
}),
8484
})
8585
# ---
86+
# name: test_entry_diagnostics_fallback_to_instance_v1
87+
dict({
88+
'account': dict({
89+
'acct': 'trwnh',
90+
'avatar': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
91+
'avatar_static': 'https://files.mastodon.social/accounts/avatars/000/014/715/original/051c958388818705.png',
92+
'bot': True,
93+
'created_at': '2016-11-24T00:00:00+00:00',
94+
'discoverable': True,
95+
'display_name': 'infinite love ⴳ',
96+
'emojis': list([
97+
]),
98+
'fields': list([
99+
dict({
100+
'name': 'Website',
101+
'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>',
102+
'verified_at': '2019-08-29T04:14:55.571+00:00',
103+
}),
104+
dict({
105+
'name': 'Portfolio',
106+
'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>',
107+
'verified_at': '2021-02-11T20:34:13.574+00:00',
108+
}),
109+
dict({
110+
'name': 'Fan of:',
111+
'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)',
112+
'verified_at': None,
113+
}),
114+
dict({
115+
'name': 'What to expect:',
116+
'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.',
117+
'verified_at': None,
118+
}),
119+
]),
120+
'followers_count': 3169,
121+
'following_count': 328,
122+
'group': False,
123+
'header': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
124+
'header_static': 'https://files.mastodon.social/accounts/headers/000/014/715/original/5c6fc24edb3bb873.jpg',
125+
'hide_collections': True,
126+
'id': '14715',
127+
'indexable': False,
128+
'last_status_at': '2025-03-04T00:00:00',
129+
'limited': None,
130+
'locked': False,
131+
'memorial': None,
132+
'moved': None,
133+
'moved_to_account': None,
134+
'mute_expires_at': None,
135+
'noindex': False,
136+
'note': '<p>i have approximate knowledge of many things. perpetual student. (nb/ace/they)</p><p>xmpp/email: [email protected]<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>',
137+
'role': None,
138+
'roles': list([
139+
]),
140+
'source': None,
141+
'statuses_count': 69523,
142+
'suspended': None,
143+
'uri': 'https://mastodon.social/users/trwnh',
144+
'url': 'https://mastodon.social/@trwnh',
145+
'username': 'trwnh',
146+
}),
147+
'instance': dict({
148+
'api_versions': None,
149+
'configuration': None,
150+
'contact': None,
151+
'description': 'The original server operated by the Mastodon gGmbH non-profit',
152+
'domain': 'mastodon.social',
153+
'icon': None,
154+
'languages': None,
155+
'registrations': None,
156+
'rules': None,
157+
'source_url': 'https://github.com/mastodon/mastodon',
158+
'thumbnail': None,
159+
'title': 'Mastodon',
160+
'uri': 'mastodon.social',
161+
'usage': dict({
162+
'users': dict({
163+
'active_month': 380143,
164+
}),
165+
}),
166+
'version': '4.4.0-nightly.2025-02-07',
167+
}),
168+
})
169+
# ---

tests/components/mastodon/test_config_flow.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
from unittest.mock import AsyncMock
44

5-
from mastodon.Mastodon import MastodonNetworkError, MastodonUnauthorizedError
5+
from mastodon.Mastodon import (
6+
MastodonNetworkError,
7+
MastodonNotFoundError,
8+
MastodonUnauthorizedError,
9+
)
610
import pytest
711

812
from homeassistant.components.mastodon.const import CONF_BASE_URL, DOMAIN
@@ -80,6 +84,46 @@ async def test_full_flow_with_path(
8084
assert result["result"].unique_id == "trwnh_mastodon_social"
8185

8286

87+
async def test_full_flow_fallback_to_instance_v1(
88+
hass: HomeAssistant,
89+
mock_mastodon_client: AsyncMock,
90+
mock_setup_entry: AsyncMock,
91+
) -> None:
92+
"""Test full flow where instance_v2 fails and falls back to instance_v1."""
93+
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
94+
"Instance API v2 not found"
95+
)
96+
97+
result = await hass.config_entries.flow.async_init(
98+
DOMAIN,
99+
context={"source": SOURCE_USER},
100+
)
101+
assert result["type"] is FlowResultType.FORM
102+
assert result["step_id"] == "user"
103+
104+
result = await hass.config_entries.flow.async_configure(
105+
result["flow_id"],
106+
{
107+
CONF_BASE_URL: "https://mastodon.social",
108+
CONF_CLIENT_ID: "client_id",
109+
CONF_CLIENT_SECRET: "client_secret",
110+
CONF_ACCESS_TOKEN: "access_token",
111+
},
112+
)
113+
assert result["type"] is FlowResultType.CREATE_ENTRY
114+
assert result["title"] == "@[email protected]"
115+
assert result["data"] == {
116+
CONF_BASE_URL: "https://mastodon.social",
117+
CONF_CLIENT_ID: "client_id",
118+
CONF_CLIENT_SECRET: "client_secret",
119+
CONF_ACCESS_TOKEN: "access_token",
120+
}
121+
assert result["result"].unique_id == "trwnh_mastodon_social"
122+
123+
mock_mastodon_client.instance_v2.assert_called_once()
124+
mock_mastodon_client.instance_v1.assert_called_once()
125+
126+
83127
@pytest.mark.parametrize(
84128
("exception", "error"),
85129
[

tests/components/mastodon/test_diagnostics.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from unittest.mock import AsyncMock
44

5+
from mastodon.Mastodon import MastodonNotFoundError
56
from syrupy.assertion import SnapshotAssertion
67

78
from homeassistant.core import HomeAssistant
@@ -26,3 +27,26 @@ async def test_entry_diagnostics(
2627
await get_diagnostics_for_config_entry(hass, hass_client, mock_config_entry)
2728
== snapshot
2829
)
30+
31+
32+
async def test_entry_diagnostics_fallback_to_instance_v1(
33+
hass: HomeAssistant,
34+
hass_client: ClientSessionGenerator,
35+
mock_mastodon_client: AsyncMock,
36+
mock_config_entry: MockConfigEntry,
37+
snapshot: SnapshotAssertion,
38+
) -> None:
39+
"""Test config entry diagnostics with fallback to instance_v1 when instance_v2 raises MastodonNotFoundError."""
40+
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
41+
"Instance v2 not found"
42+
)
43+
44+
await setup_integration(hass, mock_config_entry)
45+
46+
diagnostics_result = await get_diagnostics_for_config_entry(
47+
hass, hass_client, mock_config_entry
48+
)
49+
50+
mock_mastodon_client.instance_v1.assert_called()
51+
52+
assert diagnostics_result == snapshot

tests/components/mastodon/test_init.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from unittest.mock import AsyncMock
44

5-
from mastodon.Mastodon import MastodonError
5+
from mastodon.Mastodon import MastodonNotFoundError
66
from syrupy.assertion import SnapshotAssertion
77

88
from homeassistant.components.mastodon.config_flow import MastodonConfigFlow
@@ -39,13 +39,30 @@ async def test_initialization_failure(
3939
mock_config_entry: MockConfigEntry,
4040
) -> None:
4141
"""Test initialization failure."""
42-
mock_mastodon_client.instance.side_effect = MastodonError
42+
mock_mastodon_client.instance_v1.side_effect = MastodonNotFoundError
43+
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError
4344

4445
await setup_integration(hass, mock_config_entry)
4546

4647
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
4748

4849

50+
async def test_setup_integration_fallback_to_instance_v1(
51+
hass: HomeAssistant,
52+
mock_mastodon_client: AsyncMock,
53+
mock_config_entry: MockConfigEntry,
54+
) -> None:
55+
"""Test full flow where instance_v2 fails and falls back to instance_v1."""
56+
mock_mastodon_client.instance_v2.side_effect = MastodonNotFoundError(
57+
"Instance API v2 not found"
58+
)
59+
60+
await setup_integration(hass, mock_config_entry)
61+
62+
mock_mastodon_client.instance_v2.assert_called_once()
63+
mock_mastodon_client.instance_v1.assert_called_once()
64+
65+
4966
async def test_migrate(
5067
hass: HomeAssistant,
5168
mock_mastodon_client: AsyncMock,

0 commit comments

Comments
 (0)