mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Refactor Steam integration config flow and tests (#174504)
Co-authored-by: Erwin Douna <e.douna@gmail.com>
This commit is contained in:
@@ -3,14 +3,13 @@
|
||||
from collections.abc import Iterator, Mapping
|
||||
from typing import Any, override
|
||||
|
||||
import steam
|
||||
import steam.api
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_REAUTH,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
OptionsFlowWithReload,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import callback
|
||||
@@ -22,6 +21,14 @@ from .coordinator import SteamConfigEntry
|
||||
# To avoid too long request URIs, the amount of ids to request is limited
|
||||
MAX_IDS_TO_REQUEST = 275
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Required(CONF_ACCOUNT): str,
|
||||
}
|
||||
)
|
||||
STEP_REAUTH_DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
|
||||
|
||||
|
||||
def validate_input(user_input: dict[str, str]) -> dict[str, str | int]:
|
||||
"""Handle common flow input validation."""
|
||||
@@ -49,29 +56,23 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
if user_input is None and self.source == SOURCE_REAUTH:
|
||||
user_input = {CONF_ACCOUNT: self._get_reauth_entry().data[CONF_ACCOUNT]}
|
||||
elif user_input is not None:
|
||||
if user_input is not None:
|
||||
await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||
self._abort_if_unique_id_configured()
|
||||
try:
|
||||
res = await self.hass.async_add_executor_job(validate_input, user_input)
|
||||
if res is not None:
|
||||
name = str(res["personaname"])
|
||||
else:
|
||||
errors["base"] = "invalid_account"
|
||||
except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex:
|
||||
errors["base"] = "cannot_connect"
|
||||
if "403" in str(ex):
|
||||
errors["base"] = "invalid_auth"
|
||||
except steam.api.HTTPError as ex:
|
||||
errors["base"] = (
|
||||
"invalid_auth" if "403" in str(ex) else "cannot_connect"
|
||||
)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
LOGGER.exception("Unknown exception: %s", ex)
|
||||
errors["base"] = "unknown"
|
||||
if not errors:
|
||||
entry = await self.async_set_unique_id(user_input[CONF_ACCOUNT])
|
||||
if entry and self.source == SOURCE_REAUTH:
|
||||
self.hass.config_entries.async_update_entry(entry, data=user_input)
|
||||
await self.hass.config_entries.async_reload(entry.entry_id)
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=name,
|
||||
data=user_input,
|
||||
@@ -80,15 +81,8 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_API_KEY, default=user_input.get(CONF_API_KEY) or ""
|
||||
): str,
|
||||
vol.Required(
|
||||
CONF_ACCOUNT, default=user_input.get(CONF_ACCOUNT) or ""
|
||||
): str,
|
||||
}
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_USER_DATA_SCHEMA, suggested_values=user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=PLACEHOLDERS,
|
||||
@@ -104,12 +98,34 @@ class SteamFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauth dialog."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_user()
|
||||
errors: dict[str, str] = {}
|
||||
entry = self._get_reauth_entry()
|
||||
|
||||
self._set_confirm_only()
|
||||
if user_input is not None:
|
||||
try:
|
||||
if not await self.hass.async_add_executor_job(
|
||||
validate_input, {**entry.data, **user_input}
|
||||
):
|
||||
errors["base"] = "invalid_account"
|
||||
except steam.api.HTTPError as ex:
|
||||
errors["base"] = (
|
||||
"invalid_auth" if "403" in str(ex) else "cannot_connect"
|
||||
)
|
||||
except Exception as ex: # noqa: BLE001
|
||||
LOGGER.exception("Unknown exception: %s", ex)
|
||||
errors["base"] = "unknown"
|
||||
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
entry, data_updates=user_input
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm", description_placeholders=PLACEHOLDERS
|
||||
step_id="reauth_confirm",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
data_schema=STEP_REAUTH_DATA_SCHEMA, suggested_values=user_input
|
||||
),
|
||||
errors=errors,
|
||||
description_placeholders=PLACEHOLDERS,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,7 +134,7 @@ def _batch_ids(ids: list[str]) -> Iterator[list[str]]:
|
||||
yield ids[i : i + MAX_IDS_TO_REQUEST]
|
||||
|
||||
|
||||
class SteamOptionsFlowHandler(OptionsFlow):
|
||||
class SteamOptionsFlowHandler(OptionsFlowWithReload):
|
||||
"""Handle Steam client options."""
|
||||
|
||||
def __init__(self, entry: SteamConfigEntry) -> None:
|
||||
@@ -145,7 +161,6 @@ class SteamOptionsFlowHandler(OptionsFlow):
|
||||
if _id in user_input[CONF_ACCOUNTS]
|
||||
}
|
||||
}
|
||||
await self.hass.config_entries.async_reload(self.config_entry.entry_id)
|
||||
return self.async_create_entry(title="", data=channel_data)
|
||||
error = None
|
||||
try:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from datetime import timedelta
|
||||
from typing import override
|
||||
|
||||
import steam
|
||||
import steam.api
|
||||
from steam.api import _interface_method as INTMethod
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -70,7 +70,7 @@ class SteamDataUpdateCoordinator(
|
||||
try:
|
||||
return await self.hass.async_add_executor_job(self._update)
|
||||
|
||||
except (steam.api.HTTPError, steam.api.HTTPTimeoutError) as ex:
|
||||
except steam.api.HTTPError as ex:
|
||||
if "401" in str(ex):
|
||||
raise ConfigEntryAuthFailed from ex
|
||||
raise UpdateFailed(ex) from ex
|
||||
|
||||
@@ -12,7 +12,13 @@
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Steam integration needs to be manually re-authenticated\n\nYou can find your Steam Web API key [**here**]({api_key_url}).",
|
||||
"data": {
|
||||
"api_key": "[%key:component::steam_online::config::step::user::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::steam_online::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "The Steam integration requires re-authentication.\n\nYou can find your Steam Web API key [**here**]({api_key_url}).",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"user": {
|
||||
|
||||
@@ -1,21 +1,7 @@
|
||||
"""Tests for Steam integration."""
|
||||
|
||||
import random
|
||||
import string
|
||||
from unittest.mock import patch
|
||||
import urllib.parse
|
||||
|
||||
import steam
|
||||
|
||||
from homeassistant.components.steam_online.const import (
|
||||
CONF_ACCOUNT,
|
||||
CONF_ACCOUNTS,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.components.steam_online.const import CONF_ACCOUNT, CONF_ACCOUNTS
|
||||
from homeassistant.const import CONF_API_KEY
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
API_KEY = "abc123"
|
||||
ACCOUNT_1 = "12345678901234567"
|
||||
@@ -36,127 +22,3 @@ CONF_OPTIONS_2 = {
|
||||
ACCOUNT_2: ACCOUNT_NAME_2,
|
||||
}
|
||||
}
|
||||
|
||||
MAX_LENGTH_STEAM_IDS = 30
|
||||
|
||||
|
||||
def create_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Add config entry in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONF_DATA,
|
||||
options=CONF_OPTIONS,
|
||||
unique_id=ACCOUNT_1,
|
||||
)
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
||||
|
||||
|
||||
class MockedUserInterfaceNull:
|
||||
"""Mocked user interface returning no players."""
|
||||
|
||||
def GetPlayerSummaries(self, steamids: str) -> dict:
|
||||
"""Get player summaries."""
|
||||
return {"response": {"players": {"player": [None]}}}
|
||||
|
||||
|
||||
class MockedInterface(dict):
|
||||
"""Mocked interface."""
|
||||
|
||||
def IPlayerService(self) -> None:
|
||||
"""Mock iplayerservice."""
|
||||
|
||||
def ISteamUser(self) -> None:
|
||||
"""Mock iSteamUser."""
|
||||
|
||||
def GetFriendList(self, steamid: str) -> dict:
|
||||
"""Get friend list."""
|
||||
fake_friends = [{"steamid": ACCOUNT_2}]
|
||||
fake_friends.extend(
|
||||
{"steamid": "".join(random.choices(string.digits, k=len(ACCOUNT_1)))}
|
||||
for _ in range(4)
|
||||
)
|
||||
return {"friendslist": {"friends": fake_friends}}
|
||||
|
||||
def GetPlayerSummaries(self, steamids: str | list[str]) -> dict:
|
||||
"""Get player summaries."""
|
||||
assert len(urllib.parse.quote(str(steamids))) <= MAX_LENGTH_STEAM_IDS
|
||||
return {
|
||||
"response": {
|
||||
"players": {
|
||||
"player": [
|
||||
{
|
||||
"steamid": ACCOUNT_1,
|
||||
"communityvisibilitystate": 1,
|
||||
"profilestate": 1,
|
||||
"personaname": ACCOUNT_NAME_1,
|
||||
"profileurl": "https://steamcommunity.com/profiles/123456789/",
|
||||
"avatar": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb.jpg",
|
||||
"avatarmedium": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_medium.jpg",
|
||||
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg",
|
||||
"avatarhash": "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb",
|
||||
"lastlogoff": 1775409487,
|
||||
"personastate": 1,
|
||||
"realname": "John Dough",
|
||||
"personastateflags": 0,
|
||||
"gameextrainfo": "The Witcher: Enhanced Edition",
|
||||
"gameid": "20900",
|
||||
},
|
||||
{
|
||||
"steamid": ACCOUNT_2,
|
||||
"communityvisibilitystate": 1,
|
||||
"profilestate": 1,
|
||||
"personaname": ACCOUNT_NAME_2,
|
||||
"profileurl": "https://steamcommunity.com/profiles/987654321/",
|
||||
"avatar": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb.jpg",
|
||||
"avatarmedium": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_medium.jpg",
|
||||
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg",
|
||||
"avatarhash": "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb",
|
||||
"lastlogoff": 1775409487,
|
||||
"personastate": 2,
|
||||
"personastateflags": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def GetOwnedGames(self, steamid: str, include_appinfo: int) -> dict:
|
||||
"""Get owned games."""
|
||||
return {
|
||||
"response": {"game_count": 1},
|
||||
"games": [
|
||||
{"appid": 1, "img_icon_url": "1234567890"},
|
||||
{
|
||||
"appid": 20900,
|
||||
"img_icon_url": "746d1cd48fb2e57d579b05b6e9eccba95859e549",
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
def GetSteamLevel(self, steamid: str) -> dict:
|
||||
"""Get steam level."""
|
||||
return {"response": {"player_level": 10}}
|
||||
|
||||
|
||||
class MockedInterfacePrivate(MockedInterface):
|
||||
"""Mocked interface for private friends list."""
|
||||
|
||||
def GetFriendList(self, steamid: str) -> None:
|
||||
"""Get friend list."""
|
||||
raise steam.api.HTTPError
|
||||
|
||||
|
||||
def patch_interface() -> MockedInterface:
|
||||
"""Patch interface."""
|
||||
return patch("steam.api.interface", return_value=MockedInterface())
|
||||
|
||||
|
||||
def patch_interface_private() -> MockedInterfacePrivate:
|
||||
"""Patch interface for private friends list."""
|
||||
return patch("steam.api.interface", return_value=MockedInterfacePrivate())
|
||||
|
||||
|
||||
def patch_user_interface_null() -> MockedUserInterfaceNull:
|
||||
"""Patch player interface with no players."""
|
||||
return patch("steam.api.interface", return_value=MockedUserInterfaceNull())
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
"""Common fixtures for Steam integration."""
|
||||
"""Common fixtures for the Steam integration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -6,7 +9,7 @@ from homeassistant.components.steam_online.const import DOMAIN
|
||||
|
||||
from . import ACCOUNT_1, CONF_DATA, CONF_OPTIONS
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, load_json_object_fixture, patch
|
||||
|
||||
|
||||
@pytest.fixture(name="config_entry")
|
||||
@@ -18,3 +21,44 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
options=CONF_OPTIONS,
|
||||
unique_id=ACCOUNT_1,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.steam_online.async_setup_entry", return_value=True
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
|
||||
|
||||
@pytest.fixture(name="steam_api")
|
||||
def mock_steam_api() -> Generator[MagicMock]:
|
||||
"""Mock Steam API."""
|
||||
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.steam_online.config_flow.steam.api.interface"
|
||||
) as mock_client,
|
||||
patch("homeassistant.components.steam_online.config_flow.steam.api.key.set"),
|
||||
patch(
|
||||
"homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST", 2
|
||||
),
|
||||
):
|
||||
client = MagicMock()
|
||||
mock_client.return_value = client
|
||||
|
||||
client.GetFriendList.return_value = load_json_object_fixture(
|
||||
"GetFriendList.json", DOMAIN
|
||||
)
|
||||
client.GetSteamLevel.return_value = load_json_object_fixture(
|
||||
"GetSteamLevel.json", DOMAIN
|
||||
)
|
||||
client.GetOwnedGames.return_value = load_json_object_fixture(
|
||||
"GetOwnedGames.json", DOMAIN
|
||||
)
|
||||
client.GetPlayerSummaries.return_value = load_json_object_fixture(
|
||||
"GetPlayerSummaries.json", DOMAIN
|
||||
)
|
||||
|
||||
yield mock_client
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"friendslist": {
|
||||
"friends": [
|
||||
{
|
||||
"steamid": "12345678912345678"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"response": {
|
||||
"game_count": 1,
|
||||
"games": [
|
||||
{
|
||||
"appid": 20900,
|
||||
"name": "The Witcher: Enhanced Edition",
|
||||
"playtime_2weeks": 396,
|
||||
"playtime_forever": 5531,
|
||||
"img_icon_url": "746d1cd48fb2e57d579b05b6e9eccba95859e549",
|
||||
"playtime_windows_forever": 0,
|
||||
"playtime_mac_forever": 0,
|
||||
"playtime_linux_forever": 5531,
|
||||
"playtime_deck_forever": 0,
|
||||
"rtime_last_played": 1782145718,
|
||||
"content_descriptorids": [1, 2, 5],
|
||||
"playtime_disconnected": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"response": {
|
||||
"players": {
|
||||
"player": [
|
||||
{
|
||||
"steamid": "12345678901234567",
|
||||
"communityvisibilitystate": 1,
|
||||
"profilestate": 1,
|
||||
"personaname": "testaccount1",
|
||||
"profileurl": "https://steamcommunity.com/profiles/123456789/",
|
||||
"avatar": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb.jpg",
|
||||
"avatarmedium": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_medium.jpg",
|
||||
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg",
|
||||
"avatarhash": "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb",
|
||||
"lastlogoff": 1775409487,
|
||||
"personastate": 1,
|
||||
"realname": "John Dough",
|
||||
"personastateflags": 0,
|
||||
"gameextrainfo": "The Witcher: Enhanced Edition",
|
||||
"gameid": "20900"
|
||||
},
|
||||
{
|
||||
"steamid": "12345678912345678",
|
||||
"communityvisibilitystate": 1,
|
||||
"profilestate": 1,
|
||||
"personaname": "testaccount2",
|
||||
"profileurl": "https://steamcommunity.com/profiles/987654321/",
|
||||
"avatar": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb.jpg",
|
||||
"avatarmedium": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_medium.jpg",
|
||||
"avatarfull": "https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg",
|
||||
"avatarhash": "fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb",
|
||||
"lastlogoff": 1775409487,
|
||||
"personastate": 2,
|
||||
"personastateflags": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"response": {
|
||||
"player_level": 10
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
"""Test Steam config flow."""
|
||||
|
||||
from unittest.mock import patch
|
||||
from typing import Any
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import steam
|
||||
import pytest
|
||||
import steam.api
|
||||
|
||||
from homeassistant.components.steam_online.const import CONF_ACCOUNTS, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
@@ -18,216 +20,267 @@ from . import (
|
||||
CONF_DATA,
|
||||
CONF_OPTIONS,
|
||||
CONF_OPTIONS_2,
|
||||
create_entry,
|
||||
patch_interface,
|
||||
patch_interface_private,
|
||||
patch_user_interface_null,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
async def test_flow_user(hass: HomeAssistant) -> None:
|
||||
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_flow_user(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
) -> None:
|
||||
"""Test user initialized flow."""
|
||||
with (
|
||||
patch_interface(),
|
||||
patch(
|
||||
"homeassistant.components.steam_online.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ACCOUNT_NAME_1
|
||||
assert result["data"] == CONF_DATA
|
||||
assert result["options"] == CONF_OPTIONS
|
||||
assert result["result"].unique_id == ACCOUNT_1
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ACCOUNT_NAME_1
|
||||
assert result["data"] == CONF_DATA
|
||||
assert result["options"] == CONF_OPTIONS
|
||||
assert result["result"].unique_id == ACCOUNT_1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow with unreachable server."""
|
||||
with patch_interface() as servicemock:
|
||||
servicemock.side_effect = steam.api.HTTPTimeoutError
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "cannot_connect"
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_msg"),
|
||||
[
|
||||
(steam.api.HTTPTimeoutError, "cannot_connect"),
|
||||
(steam.api.HTTPError, "cannot_connect"),
|
||||
(steam.api.HTTPError("403"), "invalid_auth"),
|
||||
(ValueError, "unknown"),
|
||||
([{"response": {"players": {"player": [None]}}}], "invalid_account"),
|
||||
],
|
||||
)
|
||||
async def test_flow_user_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
side_effect: Exception | dict[str, Any],
|
||||
error_msg: str,
|
||||
steam_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test user initialized flow with errors."""
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_msg}
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["title"] == ACCOUNT_NAME_1
|
||||
assert result["data"] == CONF_DATA
|
||||
assert result["options"] == CONF_OPTIONS
|
||||
assert result["result"].unique_id == ACCOUNT_1
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow with invalid authentication."""
|
||||
with patch_interface() as servicemock:
|
||||
servicemock.side_effect = steam.api.HTTPError("403")
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
|
||||
async def test_flow_user_invalid_account(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow with invalid account ID."""
|
||||
with patch_user_interface_null():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "invalid_account"
|
||||
|
||||
|
||||
async def test_flow_user_unknown(hass: HomeAssistant) -> None:
|
||||
"""Test user initialized flow with unknown error."""
|
||||
with patch_interface() as servicemock:
|
||||
servicemock.side_effect = Exception
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "unknown"
|
||||
|
||||
|
||||
async def test_flow_user_already_configured(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_flow_user_already_configured(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test user initialized flow with duplicate account."""
|
||||
create_entry(hass)
|
||||
with patch_interface():
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=CONF_DATA
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "already_configured"
|
||||
|
||||
|
||||
async def test_flow_reauth(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_flow_reauth(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test reauth step."""
|
||||
entry = create_entry(hass)
|
||||
result = await entry.start_reauth_flow(hass)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
with patch_interface():
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
new_conf = CONF_DATA | {CONF_API_KEY: "1234567890"}
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=new_conf,
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert entry.data == new_conf
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "1234567890"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert config_entry.data == {**CONF_DATA, CONF_API_KEY: "1234567890"}
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
|
||||
|
||||
async def test_options_flow(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.parametrize(
|
||||
("side_effect", "error_msg"),
|
||||
[
|
||||
(steam.api.HTTPTimeoutError, "cannot_connect"),
|
||||
(steam.api.HTTPError, "cannot_connect"),
|
||||
(steam.api.HTTPError("403"), "invalid_auth"),
|
||||
(ValueError, "unknown"),
|
||||
([{"response": {"players": {"player": [None]}}}], "invalid_account"),
|
||||
],
|
||||
)
|
||||
async def test_flow_reauth_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
steam_api: MagicMock,
|
||||
side_effect: Exception | dict[str, Any],
|
||||
error_msg: str,
|
||||
) -> None:
|
||||
"""Test reauth step with errors."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
result = await config_entry.start_reauth_flow(hass)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = side_effect
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "1234567890"}
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": error_msg}
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = None
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "1234567890"}
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert config_entry.data == {**CONF_DATA, CONF_API_KEY: "1234567890"}
|
||||
|
||||
assert len(hass.config_entries.async_entries()) == 1
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_options_flow(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test updating options."""
|
||||
entry = create_entry(hass)
|
||||
with (
|
||||
patch_interface(),
|
||||
patch(
|
||||
"homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST",
|
||||
return_value=2,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1, ACCOUNT_2]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == CONF_OPTIONS_2
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_options_flow_deselect(
|
||||
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test deselecting user."""
|
||||
entry = create_entry(hass)
|
||||
with (
|
||||
patch_interface(),
|
||||
patch(
|
||||
"homeassistant.components.steam_online.config_flow.MAX_IDS_TO_REQUEST",
|
||||
return_value=2,
|
||||
),
|
||||
):
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with (
|
||||
patch_interface(),
|
||||
patch(
|
||||
"homeassistant.components.steam_online.async_setup_entry",
|
||||
return_value=True,
|
||||
),
|
||||
):
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: []},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: []},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == {CONF_ACCOUNTS: {}}
|
||||
assert len(entity_registry.entities) == 0
|
||||
|
||||
|
||||
async def test_options_flow_timeout(hass: HomeAssistant) -> None:
|
||||
async def test_options_flow_timeout(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
steam_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test updating options timeout getting friends list."""
|
||||
entry = create_entry(hass)
|
||||
with patch_interface() as servicemock:
|
||||
servicemock.side_effect = steam.api.HTTPTimeoutError
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
steam_api.return_value.GetFriendList.side_effect = steam.api.HTTPTimeoutError
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1]},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result["data"] == CONF_OPTIONS
|
||||
|
||||
|
||||
async def test_options_flow_unauthorized(hass: HomeAssistant) -> None:
|
||||
async def test_options_flow_unauthorized(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
steam_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test updating options when user's friends list is not public."""
|
||||
entry = create_entry(hass)
|
||||
with patch_interface_private():
|
||||
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
steam_api.return_value.GetFriendList.side_effect = steam.api.HTTPError
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1]},
|
||||
)
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_ACCOUNTS: [ACCOUNT_1]},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||
|
||||
@@ -1,54 +1,97 @@
|
||||
"""Tests for the Steam component."""
|
||||
"""Tests for the Steam integration."""
|
||||
|
||||
import steam
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
import steam.api
|
||||
|
||||
from homeassistant.components.steam_online.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import create_entry, patch_interface
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_setup(hass: HomeAssistant) -> None:
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_setup(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test unload."""
|
||||
entry = create_entry(hass)
|
||||
with patch_interface():
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state is ConfigEntryState.LOADED
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||
assert await hass.config_entries.async_unload(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None:
|
||||
async def test_setup_errors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
steam_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test setup errors."""
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = steam.api.HTTPError
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||
|
||||
|
||||
async def test_setup_auth_failed(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
steam_api: MagicMock,
|
||||
) -> None:
|
||||
"""Test that it throws ConfigEntryAuthFailed when authentication fails."""
|
||||
entry = create_entry(hass)
|
||||
with patch_interface() as interface:
|
||||
interface.side_effect = steam.api.HTTPError("401")
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
steam_api.return_value.GetPlayerSummaries.side_effect = steam.api.HTTPError("401")
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
assert entry.state is ConfigEntryState.SETUP_ERROR
|
||||
assert not hass.data.get(DOMAIN)
|
||||
assert config_entry.state is ConfigEntryState.SETUP_ERROR
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow.get("step_id") == "reauth_confirm"
|
||||
assert flow.get("handler") == DOMAIN
|
||||
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == config_entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_device_info(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Test device info."""
|
||||
entry = create_entry(hass)
|
||||
with patch_interface():
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
device = device_registry.async_get_device(identifiers={(DOMAIN, entry.entry_id)})
|
||||
assert (
|
||||
device := device_registry.async_get_device(
|
||||
identifiers={(DOMAIN, config_entry.entry_id)}
|
||||
)
|
||||
)
|
||||
|
||||
assert device.configuration_url == "https://store.steampowered.com"
|
||||
assert device.entry_type == dr.DeviceEntryType.SERVICE
|
||||
assert device.identifiers == {(DOMAIN, entry.entry_id)}
|
||||
assert device.identifiers == {(DOMAIN, config_entry.entry_id)}
|
||||
assert device.manufacturer == DEFAULT_NAME
|
||||
assert device.name == DEFAULT_NAME
|
||||
|
||||
@@ -11,8 +11,6 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import patch_interface
|
||||
|
||||
from tests.common import MockConfigEntry, snapshot_platform
|
||||
|
||||
|
||||
@@ -26,6 +24,7 @@ def sensor_only() -> Generator[None]:
|
||||
yield
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("steam_api")
|
||||
async def test_sensors(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
@@ -33,10 +32,10 @@ async def test_sensors(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test setup of the Steam sensor platform."""
|
||||
with patch_interface():
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
|
||||
Reference in New Issue
Block a user