Skip to content

Commit 0c12146

Browse files
pantherale0zweckj
andauthored
Validate devices connected to Nintendo Parental Controls accounts (#154873)
Co-authored-by: Josef Zweck <josef@zweck.dev>
1 parent af27765 commit 0c12146

File tree

7 files changed

+152
-10
lines changed

7 files changed

+152
-10
lines changed

homeassistant/components/nintendo_parental_controls/config_flow.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,15 @@
77
from typing import TYPE_CHECKING, Any
88

99
from pynintendoparental import Authenticator
10+
from pynintendoparental.api import Api
1011
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
1112
import voluptuous as vol
1213

1314
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
1415
from homeassistant.const import CONF_API_TOKEN
1516
from homeassistant.helpers.aiohttp_client import async_get_clientsession
1617

17-
from .const import CONF_SESSION_TOKEN, DOMAIN
18+
from .const import APP_SETUP_URL, CONF_SESSION_TOKEN, DOMAIN
1819

1920
_LOGGER = logging.getLogger(__name__)
2021

@@ -37,6 +38,9 @@ async def async_step_user(
3738
)
3839

3940
if user_input is not None:
41+
nintendo_api = Api(
42+
self.auth, self.hass.config.time_zone, self.hass.config.language
43+
)
4044
try:
4145
await self.auth.complete_login(
4246
self.auth, user_input[CONF_API_TOKEN], False
@@ -48,12 +52,24 @@ async def async_step_user(
4852
assert self.auth.account_id
4953
await self.async_set_unique_id(self.auth.account_id)
5054
self._abort_if_unique_id_configured()
51-
return self.async_create_entry(
52-
title=self.auth.account_id,
53-
data={
54-
CONF_SESSION_TOKEN: self.auth.get_session_token,
55-
},
56-
)
55+
try:
56+
if "base" not in errors:
57+
await nintendo_api.async_get_account_devices()
58+
except HttpException as err:
59+
if err.status_code == 404:
60+
return self.async_abort(
61+
reason="no_devices_found",
62+
description_placeholders={"more_info_url": APP_SETUP_URL},
63+
)
64+
errors["base"] = "cannot_connect"
65+
else:
66+
if "base" not in errors:
67+
return self.async_create_entry(
68+
title=self.auth.account_id,
69+
data={
70+
CONF_SESSION_TOKEN: self.auth.get_session_token,
71+
},
72+
)
5773
return self.async_show_form(
5874
step_id="user",
5975
description_placeholders={"link": self.auth.login_url},

homeassistant/components/nintendo_parental_controls/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,7 @@
88
BEDTIME_ALARM_MAX = "23:00"
99
BEDTIME_ALARM_DISABLE = "00:00"
1010

11+
APP_SETUP_URL = (
12+
"https://www.nintendo.com/my/support/switch/parentalcontrols/app/setup.html"
13+
)
1114
ATTR_BONUS_TIME = "bonus_time"

homeassistant/components/nintendo_parental_controls/coordinator.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
import logging
77

88
from pynintendoparental import Authenticator, NintendoParental
9-
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
9+
from pynintendoparental.exceptions import (
10+
InvalidOAuthConfigurationException,
11+
NoDevicesFoundException,
12+
)
1013

1114
from homeassistant.config_entries import ConfigEntry
1215
from homeassistant.core import HomeAssistant
@@ -24,6 +27,8 @@
2427
class NintendoUpdateCoordinator(DataUpdateCoordinator[None]):
2528
"""Nintendo data update coordinator."""
2629

30+
config_entry: NintendoParentalControlsConfigEntry
31+
2732
def __init__(
2833
self,
2934
hass: HomeAssistant,
@@ -50,3 +55,8 @@ async def _async_update_data(self) -> None:
5055
raise ConfigEntryError(
5156
err, translation_domain=DOMAIN, translation_key="invalid_auth"
5257
) from err
58+
except NoDevicesFoundException as err:
59+
raise ConfigEntryError(
60+
translation_domain=DOMAIN,
61+
translation_key="no_devices_found",
62+
) from err

homeassistant/components/nintendo_parental_controls/strings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
"config": {
33
"abort": {
44
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
5+
"no_devices_found": "There are no devices paired with this Nintendo account, go to [Nintendo Support]({more_info_url}) for further assistance.",
56
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
67
},
78
"error": {
@@ -67,6 +68,9 @@
6768
},
6869
"device_not_found": {
6970
"message": "Device not found."
71+
},
72+
"no_devices_found": {
73+
"message": "No Nintendo devices found for this account."
7074
}
7175
},
7276
"services": {

tests/components/nintendo_parental_controls/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,20 @@ def mock_nintendo_authenticator() -> Generator[MagicMock]:
7676
yield mock_auth
7777

7878

79+
@pytest.fixture
80+
def mock_nintendo_api() -> Generator[AsyncMock]:
81+
"""Mock Nintendo API."""
82+
with patch(
83+
"homeassistant.components.nintendo_parental_controls.config_flow.Api",
84+
autospec=True,
85+
) as mock_api_class:
86+
mock_api_instance = MagicMock()
87+
# patch async_get_account_devices as an AsyncMock
88+
mock_api_instance.async_get_account_devices = AsyncMock()
89+
mock_api_class.return_value = mock_api_instance
90+
yield mock_api_instance
91+
92+
7993
@pytest.fixture
8094
def mock_failed_nintendo_authenticator() -> Generator[MagicMock]:
8195
"""Mock a failed Nintendo Authenticator."""

tests/components/nintendo_parental_controls/test_config_flow.py

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

33
from unittest.mock import AsyncMock
44

5-
from pynintendoparental.exceptions import InvalidSessionTokenException
5+
from pynintendoparental.exceptions import HttpException, InvalidSessionTokenException
66

77
from homeassistant import config_entries
88
from homeassistant.components.nintendo_parental_controls.const import (
@@ -22,6 +22,7 @@ async def test_full_flow(
2222
hass: HomeAssistant,
2323
mock_setup_entry: AsyncMock,
2424
mock_nintendo_authenticator: AsyncMock,
25+
mock_nintendo_api: AsyncMock,
2526
) -> None:
2627
"""Test a full and successful config flow."""
2728
result = await hass.config_entries.flow.async_init(
@@ -47,6 +48,7 @@ async def test_already_configured(
4748
hass: HomeAssistant,
4849
mock_config_entry: MockConfigEntry,
4950
mock_nintendo_authenticator: AsyncMock,
51+
mock_nintendo_api: AsyncMock,
5052
) -> None:
5153
"""Test that the flow aborts if the account is already configured."""
5254
mock_config_entry.add_to_hass(hass)
@@ -68,6 +70,7 @@ async def test_already_configured(
6870
async def test_invalid_auth(
6971
hass: HomeAssistant,
7072
mock_nintendo_authenticator: AsyncMock,
73+
mock_nintendo_api: AsyncMock,
7174
) -> None:
7275
"""Test handling of invalid authentication."""
7376
result = await hass.config_entries.flow.async_init(
@@ -104,6 +107,75 @@ async def test_invalid_auth(
104107
assert result["result"].unique_id == ACCOUNT_ID
105108

106109

110+
async def test_missing_devices(
111+
hass: HomeAssistant,
112+
mock_nintendo_authenticator: AsyncMock,
113+
mock_nintendo_api: AsyncMock,
114+
) -> None:
115+
"""Test handling of no devices found."""
116+
result = await hass.config_entries.flow.async_init(
117+
DOMAIN, context={"source": config_entries.SOURCE_USER}
118+
)
119+
assert result is not None
120+
assert result["type"] is FlowResultType.FORM
121+
assert result["step_id"] == "user"
122+
assert "link" in result["description_placeholders"]
123+
124+
mock_nintendo_authenticator.complete_login.side_effect = None
125+
126+
mock_nintendo_api.async_get_account_devices.side_effect = HttpException(
127+
status_code=404, message="TEST"
128+
)
129+
130+
result = await hass.config_entries.flow.async_configure(
131+
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
132+
)
133+
134+
assert result["type"] is FlowResultType.ABORT
135+
assert result["reason"] == "no_devices_found"
136+
137+
138+
async def test_cannot_connect(
139+
hass: HomeAssistant,
140+
mock_nintendo_authenticator: AsyncMock,
141+
mock_nintendo_api: AsyncMock,
142+
) -> None:
143+
"""Test handling of connection errors during device discovery."""
144+
result = await hass.config_entries.flow.async_init(
145+
DOMAIN, context={"source": config_entries.SOURCE_USER}
146+
)
147+
assert result is not None
148+
assert result["type"] is FlowResultType.FORM
149+
assert result["step_id"] == "user"
150+
assert "link" in result["description_placeholders"]
151+
152+
mock_nintendo_authenticator.complete_login.side_effect = None
153+
154+
mock_nintendo_api.async_get_account_devices.side_effect = HttpException(
155+
status_code=500, message="TEST"
156+
)
157+
158+
result = await hass.config_entries.flow.async_configure(
159+
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
160+
)
161+
162+
assert result["type"] is FlowResultType.FORM
163+
assert result["step_id"] == "user"
164+
assert result["errors"] == {"base": "cannot_connect"}
165+
166+
# Test we can recover from the error
167+
mock_nintendo_api.async_get_account_devices.side_effect = None
168+
169+
result = await hass.config_entries.flow.async_configure(
170+
result["flow_id"], user_input={CONF_API_TOKEN: API_TOKEN}
171+
)
172+
173+
assert result["type"] is FlowResultType.CREATE_ENTRY
174+
assert result["title"] == ACCOUNT_ID
175+
assert result["data"][CONF_SESSION_TOKEN] == API_TOKEN
176+
assert result["result"].unique_id == ACCOUNT_ID
177+
178+
107179
async def test_reauthentication_success(
108180
hass: HomeAssistant,
109181
mock_config_entry: MockConfigEntry,

tests/components/nintendo_parental_controls/test_coordinator.py

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

33
from unittest.mock import AsyncMock
44

5-
from pynintendoparental.exceptions import InvalidOAuthConfigurationException
5+
from pynintendoparental.exceptions import (
6+
InvalidOAuthConfigurationException,
7+
NoDevicesFoundException,
8+
)
69

710
from homeassistant.config_entries import ConfigEntryState
811
from homeassistant.core import HomeAssistant
@@ -33,3 +36,23 @@ async def test_invalid_authentication(
3336
assert len(entries) == 0
3437
# Ensure the config entry is marked as error
3538
assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR
39+
40+
41+
async def test_no_devices(
42+
hass: HomeAssistant,
43+
mock_config_entry: MockConfigEntry,
44+
mock_nintendo_client: AsyncMock,
45+
entity_registry: er.EntityRegistry,
46+
) -> None:
47+
"""Test handling of invalid authentication."""
48+
mock_nintendo_client.update.side_effect = NoDevicesFoundException()
49+
50+
await setup_integration(hass, mock_config_entry)
51+
52+
# Ensure no entities are created
53+
entries = er.async_entries_for_config_entry(
54+
entity_registry, mock_config_entry.entry_id
55+
)
56+
assert len(entries) == 0
57+
# Ensure the config entry is marked as error
58+
assert mock_config_entry.state == ConfigEntryState.SETUP_ERROR

0 commit comments

Comments
 (0)