mirror of
https://github.com/home-assistant/core.git
synced 2026-10-07 14:58:11 -04:00
Add ScorpionTrack share reconfiguration (#182089)
This commit is contained in:
@@ -12,7 +12,11 @@ from pyscorpiontrack import (
|
||||
ScorpionTrackShareUnavailableError,
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_RECONFIGURE,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
@@ -55,6 +59,12 @@ class ScorpionTrackConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Update the link for the configured share."""
|
||||
return await self.async_step_user(user_input)
|
||||
|
||||
@override
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -78,6 +88,12 @@ class ScorpionTrackConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(str(share.id))
|
||||
if self.source == SOURCE_RECONFIGURE:
|
||||
self._abort_if_unique_id_mismatch(reason="wrong_share")
|
||||
return self.async_update_reload_and_abort(
|
||||
self._get_reconfigure_entry(),
|
||||
data_updates={CONF_SHARE_TOKEN: share.token},
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
user_input[CONF_SHARE_TOKEN] = share.token
|
||||
return self.async_create_entry(
|
||||
@@ -85,10 +101,19 @@ class ScorpionTrackConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
suggested_values = (
|
||||
self._get_reconfigure_entry().data
|
||||
if user_input is None and self.source == SOURCE_RECONFIGURE
|
||||
else user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=probatio.Schema(
|
||||
{probatio.Required(probatio.Secret(CONF_SHARE_TOKEN)): str}
|
||||
step_id="reconfigure" if self.source == SOURCE_RECONFIGURE else "user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
probatio.Schema(
|
||||
{probatio.Required(probatio.Secret(CONF_SHARE_TOKEN)): str}
|
||||
),
|
||||
suggested_values,
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -81,7 +81,7 @@ rules:
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
reconfiguration-flow: done
|
||||
repair-issues: todo
|
||||
stale-devices: done
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "This shared location is already configured."
|
||||
"already_configured": "This shared location is already configured.",
|
||||
"wrong_share": "This link belongs to a different shared location. Add it as a new integration entry instead."
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "The ScorpionTrack share could not be reached right now.",
|
||||
@@ -10,6 +11,16 @@
|
||||
"unknown": "An unexpected error occurred."
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"share_token": "[%key:component::scorpiontrack::config::step::user::data::share_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"share_token": "[%key:component::scorpiontrack::config::step::user::data_description::share_token%]"
|
||||
},
|
||||
"description": "Enter a link for the same shared location. A different share must be added as a new integration entry.",
|
||||
"title": "Update a ScorpionTrack share"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"share_token": "Share URL or token"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from collections.abc import Iterator
|
||||
from datetime import timedelta
|
||||
from unittest.mock import AsyncMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from pyscorpiontrack import (
|
||||
ScorpionTrackClient,
|
||||
@@ -64,11 +64,9 @@ def mock_config_entry() -> MockConfigEntry:
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_scorpiontrack_client(
|
||||
mock_share: ScorpionTrackShare,
|
||||
) -> Iterator[AsyncMock]:
|
||||
"""Mock the ScorpionTrack client."""
|
||||
@pytest.fixture
|
||||
def mock_scorpiontrack_client_class() -> Iterator[MagicMock]:
|
||||
"""Mock the ScorpionTrack client constructor."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.scorpiontrack.ScorpionTrackClient",
|
||||
@@ -79,8 +77,17 @@ def mock_scorpiontrack_client(
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
client = mock_client.return_value
|
||||
mock_client.extract_token.side_effect = ScorpionTrackClient.extract_token
|
||||
client.token = "canonical-token"
|
||||
client.async_get_share.return_value = mock_share
|
||||
yield client
|
||||
yield mock_client
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_scorpiontrack_client(
|
||||
mock_scorpiontrack_client_class: MagicMock,
|
||||
mock_share: ScorpionTrackShare,
|
||||
) -> AsyncMock:
|
||||
"""Return the mocked ScorpionTrack client."""
|
||||
client = mock_scorpiontrack_client_class.return_value
|
||||
client.token = "canonical-token"
|
||||
client.async_get_share.return_value = mock_share
|
||||
return client
|
||||
|
||||
@@ -20,7 +20,7 @@ from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult, FlowResultType
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, get_schema_suggested_value
|
||||
|
||||
|
||||
async def _async_start_user_flow(hass: HomeAssistant) -> FlowResult:
|
||||
@@ -33,6 +33,10 @@ async def _async_start_user_flow(hass: HomeAssistant) -> FlowResult:
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {}
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, CONF_SHARE_TOKEN)
|
||||
is None
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -150,6 +154,10 @@ async def test_user_flow_shows_validation_errors(
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, CONF_SHARE_TOKEN)
|
||||
== "canonical-token"
|
||||
)
|
||||
|
||||
|
||||
async def test_user_flow_maps_malformed_input_to_invalid_token(
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Test reconfiguring a ScorpionTrack share."""
|
||||
|
||||
from dataclasses import replace
|
||||
from unittest.mock import ANY, AsyncMock, MagicMock
|
||||
|
||||
from pyscorpiontrack import (
|
||||
ScorpionTrackConnectionError,
|
||||
ScorpionTrackInvalidTokenError,
|
||||
ScorpionTrackShare,
|
||||
ScorpionTrackShareUnavailableError,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.scorpiontrack.const import CONF_SHARE_TOKEN, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_RECONFIGURE, ConfigEntryState
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfSpeed
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import setup_integration
|
||||
|
||||
from tests.common import MockConfigEntry, get_schema_suggested_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("share_input", "token"),
|
||||
[
|
||||
pytest.param("updated-token", "updated-token", id="changed-token"),
|
||||
pytest.param("canonical-token", "canonical-token", id="unchanged-token"),
|
||||
pytest.param(
|
||||
"https://app.scorpiontrack.com/shared/location?token=canonical-token",
|
||||
"canonical-token",
|
||||
id="share-url",
|
||||
),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_same_share(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
mock_scorpiontrack_client_class: MagicMock,
|
||||
entity_registry: er.EntityRegistry,
|
||||
share_input: str,
|
||||
token: str,
|
||||
) -> None:
|
||||
"""Update the token and reload without replacing the share or its entities."""
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
original_entities = er.async_entries_for_config_entry(
|
||||
entity_registry, mock_config_entry.entry_id
|
||||
)
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share, token=token, title="New share title"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, CONF_SHARE_TOKEN)
|
||||
== "canonical-token"
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_SHARE_TOKEN: share_input}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: token}
|
||||
assert mock_config_entry.unique_id == "101"
|
||||
assert mock_config_entry.title == "Family Cars"
|
||||
assert (
|
||||
er.async_entries_for_config_entry(entity_registry, mock_config_entry.entry_id)
|
||||
== original_entities
|
||||
)
|
||||
assert mock_config_entry.state is ConfigEntryState.LOADED
|
||||
mock_scorpiontrack_client_class.assert_called_with(session=ANY, token=token)
|
||||
assert mock_scorpiontrack_client.async_get_share.await_count == 3
|
||||
speed = hass.states.get("sensor.ab12_cde_speed")
|
||||
assert speed is not None
|
||||
assert float(speed.state) == pytest.approx(48.3 * 0.621371192237334)
|
||||
assert speed.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfSpeed.MILES_PER_HOUR
|
||||
|
||||
|
||||
async def test_reconfigure_rejects_different_share(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
) -> None:
|
||||
"""Keep the original configuration when the link identifies another share."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(mock_share, id=202)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
data={CONF_SHARE_TOKEN: "another-token"},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "wrong_share"
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: "canonical-token"}
|
||||
assert mock_config_entry.unique_id == "101"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("error", "expected_error"),
|
||||
[
|
||||
pytest.param(ScorpionTrackConnectionError(), "cannot_connect", id="connection"),
|
||||
pytest.param(ScorpionTrackInvalidTokenError(), "invalid_token", id="invalid"),
|
||||
pytest.param(
|
||||
ScorpionTrackShareUnavailableError(), "share_unavailable", id="unavailable"
|
||||
),
|
||||
pytest.param(Exception(), "unknown", id="unexpected"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_validation_error(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_share: ScorpionTrackShare,
|
||||
mock_scorpiontrack_client: AsyncMock,
|
||||
error: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Recover from a validation error without losing the stored configuration."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
mock_scorpiontrack_client.async_get_share.side_effect = error
|
||||
share_input = "https://app.scorpiontrack.com/shared/location?token=updated-token"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
data={CONF_SHARE_TOKEN: share_input},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
assert result["errors"] == {"base": expected_error}
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, CONF_SHARE_TOKEN)
|
||||
== share_input
|
||||
)
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: "canonical-token"}
|
||||
|
||||
mock_scorpiontrack_client.async_get_share.side_effect = None
|
||||
mock_scorpiontrack_client.async_get_share.return_value = replace(
|
||||
mock_share, token="updated-token"
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_SHARE_TOKEN: share_input}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: "updated-token"}
|
||||
assert mock_config_entry.unique_id == "101"
|
||||
|
||||
|
||||
async def test_reconfigure_malformed_link(
|
||||
hass: HomeAssistant,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
) -> None:
|
||||
"""Recover from an empty token by submitting a valid token."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
data={CONF_SHARE_TOKEN: " "},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["errors"] == {"base": "invalid_token"}
|
||||
assert (
|
||||
get_schema_suggested_value(result["data_schema"].schema, CONF_SHARE_TOKEN)
|
||||
== " "
|
||||
)
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: "canonical-token"}
|
||||
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], {CONF_SHARE_TOKEN: "canonical-token"}
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data == {CONF_SHARE_TOKEN: "canonical-token"}
|
||||
assert mock_config_entry.unique_id == "101"
|
||||
Reference in New Issue
Block a user