From d5e4ed2b9ece3639b70ed54c524afef8014412d7 Mon Sep 17 00:00:00 2001 From: Robin Lintermann Date: Sat, 10 May 2025 12:07:55 +0000 Subject: [PATCH] Major rework of tests and refactoring --- homeassistant/components/smarla/__init__.py | 2 +- .../components/smarla/config_flow.py | 28 +++--- homeassistant/components/smarla/strings.json | 2 +- tests/components/smarla/__init__.py | 19 ++++ tests/components/smarla/conftest.py | 44 ++++++++++ tests/components/smarla/test_config_flow.py | 87 ++++++++----------- 6 files changed, 114 insertions(+), 68 deletions(-) create mode 100644 tests/components/smarla/conftest.py diff --git a/homeassistant/components/smarla/__init__.py b/homeassistant/components/smarla/__init__.py index c8738639a85..f5c39a274ab 100644 --- a/homeassistant/components/smarla/__init__.py +++ b/homeassistant/components/smarla/__init__.py @@ -14,7 +14,7 @@ type FederwiegeConfigEntry = ConfigEntry[Federwiege] async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool: """Set up this integration using UI.""" - connection = Connection(HOST, token_b64=entry.data.get(CONF_ACCESS_TOKEN, None)) + connection = Connection(HOST, token_b64=entry.data.get(CONF_ACCESS_TOKEN)) # Check if token still has access if not await connection.refresh_token(): diff --git a/homeassistant/components/smarla/config_flow.py b/homeassistant/components/smarla/config_flow.py index 9fce505f2a6..3ff31ccbee1 100644 --- a/homeassistant/components/smarla/config_flow.py +++ b/homeassistant/components/smarla/config_flow.py @@ -5,6 +5,7 @@ from __future__ import annotations from typing import Any from pysmarlaapi import Connection +from pysmarlaapi.classes import AuthToken import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult @@ -20,23 +21,21 @@ class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 - async def _handle_token(self, token: str) -> tuple[dict[str, str], dict[str, str]]: + async def _handle_token(self, token: str) -> tuple[dict[str, str], AuthToken]: """Handle the token input.""" errors: dict[str, str] = {} - info: dict[str, str] = {} try: conn = Connection(url=HOST, token_b64=token) except ValueError: - errors["base"] = "invalid_token" - return (errors, info) + errors["base"] = "malformed_token" + return (errors, None) - if await conn.refresh_token(): - info["serial_number"] = conn.token.serialNumber - else: + if not await conn.refresh_token(): errors["base"] = "invalid_auth" + return (errors, None) - return (errors, info) + return (errors, conn.token) async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -45,19 +44,16 @@ class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - token = user_input[CONF_ACCESS_TOKEN] - - errors, info = await self._handle_token(token=token) + raw_token = user_input[CONF_ACCESS_TOKEN] + errors, token = await self._handle_token(token=raw_token) if not errors: - serial_number = info["serial_number"] - - await self.async_set_unique_id(serial_number) + await self.async_set_unique_id(token.serialNumber) self._abort_if_unique_id_configured() return self.async_create_entry( - title=serial_number, - data={CONF_ACCESS_TOKEN: token}, + title=token.serialNumber, + data={CONF_ACCESS_TOKEN: raw_token}, ) return self.async_show_form( diff --git a/homeassistant/components/smarla/strings.json b/homeassistant/components/smarla/strings.json index 75e4ae9a177..116a139e2a3 100644 --- a/homeassistant/components/smarla/strings.json +++ b/homeassistant/components/smarla/strings.json @@ -2,7 +2,7 @@ "config": { "error": { "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", - "invalid_token": "Invalid access token" + "malformed_token": "Malformed access token" }, "step": { "user": { diff --git a/tests/components/smarla/__init__.py b/tests/components/smarla/__init__.py index afa9f035eb7..5bb8b70f030 100644 --- a/tests/components/smarla/__init__.py +++ b/tests/components/smarla/__init__.py @@ -1 +1,20 @@ """Tests for the Smarla integration.""" + +import base64 +import json + +from homeassistant.const import CONF_ACCESS_TOKEN + +MOCK_ACCESS_TOKEN_JSON = { + "refreshToken": "test", + "appIdentifier": "HA-test", + "serialNumber": "ABCD", +} + +MOCK_SERIAL_NUMBER = MOCK_ACCESS_TOKEN_JSON["serialNumber"] + +MOCK_ACCESS_TOKEN = base64.b64encode( + json.dumps(MOCK_ACCESS_TOKEN_JSON).encode() +).decode() + +MOCK_USER_INPUT = {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN} diff --git a/tests/components/smarla/conftest.py b/tests/components/smarla/conftest.py new file mode 100644 index 00000000000..c6b3d0961e4 --- /dev/null +++ b/tests/components/smarla/conftest.py @@ -0,0 +1,44 @@ +"""Configuration for Sentry tests.""" + +from __future__ import annotations + +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant.components.smarla.config_flow import Connection +from homeassistant.components.smarla.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER + +from . import MOCK_SERIAL_NUMBER + +from tests.common import MockConfigEntry + + +@pytest.fixture +def mock_config_entry() -> MockConfigEntry: + """Create a mock config entry.""" + return MockConfigEntry( + domain=DOMAIN, + unique_id=MOCK_SERIAL_NUMBER, + source=SOURCE_USER, + ) + + +@pytest.fixture +def mock_refresh_token_success(): + """Patch Connection.refresh_token to return True.""" + with patch.object(Connection, "refresh_token", new=AsyncMock(return_value=True)): + yield + + +@pytest.fixture +def malformed_token_patch(): + """Patch Connection to raise exception.""" + return patch.object(Connection, "__init__", side_effect=ValueError) + + +@pytest.fixture +def invalid_auth_patch(): + """Patch Connection.refresh_token to return False.""" + return patch.object(Connection, "refresh_token", new=AsyncMock(return_value=False)) diff --git a/tests/components/smarla/test_config_flow.py b/tests/components/smarla/test_config_flow.py index eff708aeaa8..9b735175cb7 100644 --- a/tests/components/smarla/test_config_flow.py +++ b/tests/components/smarla/test_config_flow.py @@ -1,85 +1,72 @@ """Test config flow for Swing2Sleep Smarla integration.""" -from unittest.mock import AsyncMock, patch +import pytest -from homeassistant import config_entries -from homeassistant.components.smarla.config_flow import Connection from homeassistant.components.smarla.const import DOMAIN -from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.config_entries import SOURCE_USER from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import MOCK_SERIAL_NUMBER, MOCK_USER_INPUT + from tests.common import MockConfigEntry -MOCK_ACCESS_TOKEN = "eyJyZWZyZXNoVG9rZW4iOiJ0ZXN0IiwiYXBwSWRlbnRpZmllciI6IkhBLXRlc3QiLCJzZXJpYWxOdW1iZXIiOiJBQkNEIn0=" -MOCK_SERIAL_NUMBER = "ABCD" - -async def test_show_form(hass: HomeAssistant) -> None: - """Test that the form is shown initially.""" +async def test_config_flow(hass: HomeAssistant, mock_refresh_token_success) -> None: + """Test creating a config entry.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": "user"} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" - -async def test_create_entry(hass: HomeAssistant) -> None: - """Test creating a config entry.""" - with patch.object(Connection, "refresh_token", new=AsyncMock(return_value=True)): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_INPUT + ) assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == MOCK_SERIAL_NUMBER - assert result["data"] == {CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN} + assert result["data"] == MOCK_USER_INPUT + assert result["result"].unique_id == MOCK_SERIAL_NUMBER -async def test_invalid_auth(hass: HomeAssistant) -> None: +@pytest.mark.parametrize("error", ["malformed_token", "invalid_auth"]) +async def test_form_error( + hass: HomeAssistant, request: pytest.FixtureRequest, error: str +) -> None: """Test we show user form on invalid auth.""" - with patch.object(Connection, "refresh_token", new=AsyncMock(return_value=None)): + error_patch = request.getfixturevalue(f"{error}_patch") + with error_patch: result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {"base": "invalid_auth"} + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error} + + request.getfixturevalue("mock_refresh_token_success") + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_INPUT + ) + + assert result["type"] == FlowResultType.CREATE_ENTRY -async def test_invalid_token(hass: HomeAssistant) -> None: - """Test we handle invalid/malformed tokens.""" +async def test_device_exists_abort( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_refresh_token_success +) -> None: + """Test we abort config flow if Smarla device already configured.""" + mock_config_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_ACCESS_TOKEN: "invalid_token"}, + context={"source": SOURCE_USER}, + data=MOCK_USER_INPUT, ) - assert result["type"] == FlowResultType.FORM - assert result["errors"] == {"base": "invalid_token"} - - -async def test_device_exists_abort(hass: HomeAssistant) -> None: - """Test we abort config flow if Smarla device already configured.""" - config_entry = MockConfigEntry( - domain=DOMAIN, - unique_id=MOCK_SERIAL_NUMBER, - source=config_entries.SOURCE_USER, - ) - config_entry.add_to_hass(hass) - - with patch.object(Connection, "refresh_token", new=AsyncMock(return_value=True)): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_USER}, - data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, - ) - assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" assert len(hass.config_entries.async_entries(DOMAIN)) == 1