Refactor Steam integration config flow and tests (#174504)

Co-authored-by: Erwin Douna <e.douna@gmail.com>
This commit is contained in:
Manu
2026-06-24 17:06:29 +02:00
committed by GitHub
co-authored by Erwin Douna
parent b4b710b474
commit f0ed257f47
12 changed files with 463 additions and 367 deletions
@@ -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 -139
View File
@@ -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())
+46 -2
View File
@@ -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
}
}
+215 -162
View File
@@ -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
+67 -24
View File
@@ -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
+5 -6
View File
@@ -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