Major rework of tests and refactoring

This commit is contained in:
Robin Lintermann 2025-05-10 12:07:55 +00:00
parent 8893f97f95
commit d5e4ed2b9e
6 changed files with 114 additions and 68 deletions

View File

@ -14,7 +14,7 @@ type FederwiegeConfigEntry = ConfigEntry[Federwiege]
async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: FederwiegeConfigEntry) -> bool:
"""Set up this integration using UI.""" """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 # Check if token still has access
if not await connection.refresh_token(): if not await connection.refresh_token():

View File

@ -5,6 +5,7 @@ from __future__ import annotations
from typing import Any from typing import Any
from pysmarlaapi import Connection from pysmarlaapi import Connection
from pysmarlaapi.classes import AuthToken
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@ -20,23 +21,21 @@ class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1 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.""" """Handle the token input."""
errors: dict[str, str] = {} errors: dict[str, str] = {}
info: dict[str, str] = {}
try: try:
conn = Connection(url=HOST, token_b64=token) conn = Connection(url=HOST, token_b64=token)
except ValueError: except ValueError:
errors["base"] = "invalid_token" errors["base"] = "malformed_token"
return (errors, info) return (errors, None)
if await conn.refresh_token(): if not await conn.refresh_token():
info["serial_number"] = conn.token.serialNumber
else:
errors["base"] = "invalid_auth" errors["base"] = "invalid_auth"
return (errors, None)
return (errors, info) return (errors, conn.token)
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
@ -45,19 +44,16 @@ class SmarlaConfigFlow(ConfigFlow, domain=DOMAIN):
errors: dict[str, str] = {} errors: dict[str, str] = {}
if user_input is not None: if user_input is not None:
token = user_input[CONF_ACCESS_TOKEN] raw_token = user_input[CONF_ACCESS_TOKEN]
errors, token = await self._handle_token(token=raw_token)
errors, info = await self._handle_token(token=token)
if not errors: if not errors:
serial_number = info["serial_number"] await self.async_set_unique_id(token.serialNumber)
await self.async_set_unique_id(serial_number)
self._abort_if_unique_id_configured() self._abort_if_unique_id_configured()
return self.async_create_entry( return self.async_create_entry(
title=serial_number, title=token.serialNumber,
data={CONF_ACCESS_TOKEN: token}, data={CONF_ACCESS_TOKEN: raw_token},
) )
return self.async_show_form( return self.async_show_form(

View File

@ -2,7 +2,7 @@
"config": { "config": {
"error": { "error": {
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"invalid_token": "Invalid access token" "malformed_token": "Malformed access token"
}, },
"step": { "step": {
"user": { "user": {

View File

@ -1 +1,20 @@
"""Tests for the Smarla integration.""" """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}

View File

@ -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))

View File

@ -1,85 +1,72 @@
"""Test config flow for Swing2Sleep Smarla integration.""" """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.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.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from . import MOCK_SERIAL_NUMBER, MOCK_USER_INPUT
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
MOCK_ACCESS_TOKEN = "eyJyZWZyZXNoVG9rZW4iOiJ0ZXN0IiwiYXBwSWRlbnRpZmllciI6IkhBLXRlc3QiLCJzZXJpYWxOdW1iZXIiOiJBQkNEIn0="
MOCK_SERIAL_NUMBER = "ABCD"
async def test_config_flow(hass: HomeAssistant, mock_refresh_token_success) -> None:
async def test_show_form(hass: HomeAssistant) -> None: """Test creating a config entry."""
"""Test that the form is shown initially."""
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": "user"} DOMAIN, context={"source": SOURCE_USER}
) )
assert result["type"] == FlowResultType.FORM assert result["type"] == FlowResultType.FORM
assert result["step_id"] == "user" assert result["step_id"] == "user"
result = await hass.config_entries.flow.async_init(
async def test_create_entry(hass: HomeAssistant) -> None: DOMAIN, context={"source": SOURCE_USER}, data=MOCK_USER_INPUT
"""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},
)
assert result["type"] == FlowResultType.CREATE_ENTRY assert result["type"] == FlowResultType.CREATE_ENTRY
assert result["title"] == MOCK_SERIAL_NUMBER 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.""" """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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": SOURCE_USER},
data={CONF_ACCESS_TOKEN: MOCK_ACCESS_TOKEN}, data=MOCK_USER_INPUT,
) )
assert result["type"] == FlowResultType.FORM assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "invalid_auth"} 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: async def test_device_exists_abort(
"""Test we handle invalid/malformed tokens.""" 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( result = await hass.config_entries.flow.async_init(
DOMAIN, DOMAIN,
context={"source": config_entries.SOURCE_USER}, context={"source": SOURCE_USER},
data={CONF_ACCESS_TOKEN: "invalid_token"}, 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["type"] is FlowResultType.ABORT
assert result["reason"] == "already_configured" assert result["reason"] == "already_configured"
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1