Add WS command thread/delete_dataset (#88077)

* Add WS command thread/delete_dataset

* Return not_allowed when trying to delete the preferred dataset
This commit is contained in:
Erik Montnemery 2023-02-15 18:55:10 +01:00 committed by GitHub
parent 4221433ca6
commit 80e2f96097
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 136 additions and 4 deletions

View file

@ -9,6 +9,7 @@ from typing import Any, cast
from python_otbr_api import tlv_parser from python_otbr_api import tlv_parser
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.singleton import singleton from homeassistant.helpers.singleton import singleton
from homeassistant.helpers.storage import Store from homeassistant.helpers.storage import Store
from homeassistant.util import dt as dt_util, ulid as ulid_util from homeassistant.util import dt as dt_util, ulid as ulid_util
@ -20,6 +21,10 @@ STORAGE_VERSION_MINOR = 1
SAVE_DELAY = 10 SAVE_DELAY = 10
class DatasetPreferredError(HomeAssistantError):
"""Raised when attempting to delete the preferred dataset."""
@dataclasses.dataclass(frozen=True) @dataclasses.dataclass(frozen=True)
class DatasetEntry: class DatasetEntry:
"""Dataset store entry.""" """Dataset store entry."""
@ -92,6 +97,14 @@ class DatasetStore:
self.preferred_dataset = entry.id self.preferred_dataset = entry.id
self.async_schedule_save() self.async_schedule_save()
@callback
def async_delete(self, dataset_id: str) -> None:
"""Delete dataset."""
if self.preferred_dataset == dataset_id:
raise DatasetPreferredError("attempt to remove preferred dataset")
del self.datasets[dataset_id]
self.async_schedule_save()
@callback @callback
def async_get(self, dataset_id: str) -> DatasetEntry | None: def async_get(self, dataset_id: str) -> DatasetEntry | None:
"""Get dataset by id.""" """Get dataset by id."""

View file

@ -16,6 +16,7 @@ from . import dataset_store, discovery
def async_setup(hass: HomeAssistant) -> None: def async_setup(hass: HomeAssistant) -> None:
"""Set up the sensor websocket API.""" """Set up the sensor websocket API."""
websocket_api.async_register_command(hass, ws_add_dataset) websocket_api.async_register_command(hass, ws_add_dataset)
websocket_api.async_register_command(hass, ws_delete_dataset)
websocket_api.async_register_command(hass, ws_discover_routers) websocket_api.async_register_command(hass, ws_discover_routers)
websocket_api.async_register_command(hass, ws_get_dataset) websocket_api.async_register_command(hass, ws_get_dataset)
websocket_api.async_register_command(hass, ws_list_datasets) websocket_api.async_register_command(hass, ws_list_datasets)
@ -48,6 +49,33 @@ async def ws_add_dataset(
connection.send_result(msg["id"]) connection.send_result(msg["id"])
@websocket_api.require_admin
@websocket_api.websocket_command(
{
vol.Required("type"): "thread/delete_dataset",
vol.Required("dataset_id"): str,
}
)
@websocket_api.async_response
async def ws_delete_dataset(
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
) -> None:
"""Delete a thread dataset."""
dataset_id = msg["dataset_id"]
store = await dataset_store.async_get_store(hass)
try:
store.async_delete(dataset_id)
except KeyError as exc:
connection.send_error(msg["id"], websocket_api.const.ERR_NOT_FOUND, str(exc))
return
except dataset_store.DatasetPreferredError as exc:
connection.send_error(msg["id"], websocket_api.const.ERR_NOT_ALLOWED, str(exc))
return
connection.send_result(msg["id"])
@websocket_api.require_admin @websocket_api.require_admin
@websocket_api.websocket_command( @websocket_api.websocket_command(
{ {

View file

@ -30,6 +30,7 @@ MAX_PENDING_MSG: Final = 4096
ERR_ID_REUSE: Final = "id_reuse" ERR_ID_REUSE: Final = "id_reuse"
ERR_INVALID_FORMAT: Final = "invalid_format" ERR_INVALID_FORMAT: Final = "invalid_format"
ERR_NOT_ALLOWED: Final = "not_allowed"
ERR_NOT_FOUND: Final = "not_found" ERR_NOT_FOUND: Final = "not_found"
ERR_NOT_SUPPORTED: Final = "not_supported" ERR_NOT_SUPPORTED: Final = "not_supported"
ERR_HOME_ASSISTANT_ERROR: Final = "home_assistant_error" ERR_HOME_ASSISTANT_ERROR: Final = "home_assistant_error"

View file

@ -4,6 +4,7 @@ from python_otbr_api.tlv_parser import TLVError
from homeassistant.components.thread import dataset_store from homeassistant.components.thread import dataset_store
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from . import DATASET_1, DATASET_2, DATASET_3 from . import DATASET_1, DATASET_2, DATASET_3
@ -52,6 +53,34 @@ async def test_add_dataset_reordered(hass: HomeAssistant) -> None:
assert list(store.datasets.values())[0].created == created assert list(store.datasets.values())[0].created == created
async def test_delete_dataset_twice(hass: HomeAssistant) -> None:
"""Test deleting dataset twice raises."""
await dataset_store.async_add_dataset(hass, "source", DATASET_1)
await dataset_store.async_add_dataset(hass, "source", DATASET_2)
store = await dataset_store.async_get_store(hass)
dataset_id = list(store.datasets.values())[1].id
store.async_delete(dataset_id)
assert len(store.datasets) == 1
with pytest.raises(KeyError, match=f"'{dataset_id}'"):
store.async_delete(dataset_id)
assert len(store.datasets) == 1
async def test_delete_preferred_dataset(hass: HomeAssistant) -> None:
"""Test deleting preferred dataset raises."""
await dataset_store.async_add_dataset(hass, "source", DATASET_1)
store = await dataset_store.async_get_store(hass)
dataset_id = list(store.datasets.values())[0].id
with pytest.raises(HomeAssistantError, match="attempt to remove preferred dataset"):
store.async_delete(dataset_id)
assert len(store.datasets) == 1
async def test_get_preferred_dataset(hass: HomeAssistant) -> None: async def test_get_preferred_dataset(hass: HomeAssistant) -> None:
"""Test get the preferred dataset.""" """Test get the preferred dataset."""
assert await dataset_store.async_get_preferred_dataset(hass) is None assert await dataset_store.async_get_preferred_dataset(hass) is None
@ -133,24 +162,27 @@ async def test_load_datasets(hass: HomeAssistant) -> None:
assert store1.preferred_dataset == dataset_1_store_1.id assert store1.preferred_dataset == dataset_1_store_1.id
with pytest.raises(HomeAssistantError):
store1.async_delete(dataset_1_store_1.id)
store1.async_delete(dataset_2_store_1.id)
assert len(store1.datasets) == 2
store2 = dataset_store.DatasetStore(hass) store2 = dataset_store.DatasetStore(hass)
await flush_store(store1._store) await flush_store(store1._store)
await store2.async_load() await store2.async_load()
assert len(store2.datasets) == 3 assert len(store2.datasets) == 2
for dataset in store2.datasets.values(): for dataset in store2.datasets.values():
if dataset.source == "Google": if dataset.source == "Google":
dataset_1_store_2 = dataset dataset_1_store_2 = dataset
if dataset.source == "Multipan":
dataset_2_store_2 = dataset
if dataset.source == "🎅": if dataset.source == "🎅":
dataset_3_store_2 = dataset dataset_3_store_2 = dataset
assert list(store1.datasets) == list(store2.datasets) assert list(store1.datasets) == list(store2.datasets)
assert dataset_1_store_1 == dataset_1_store_2 assert dataset_1_store_1 == dataset_1_store_2
assert dataset_2_store_1 == dataset_2_store_2
assert dataset_3_store_1 == dataset_3_store_2 assert dataset_3_store_1 == dataset_3_store_2

View file

@ -17,6 +17,8 @@ from . import (
ROUTER_DISCOVERY_HASS, ROUTER_DISCOVERY_HASS,
) )
from tests.typing import WebSocketGenerator
async def test_add_dataset(hass: HomeAssistant, hass_ws_client) -> None: async def test_add_dataset(hass: HomeAssistant, hass_ws_client) -> None:
"""Test we can add a dataset.""" """Test we can add a dataset."""
@ -54,6 +56,62 @@ async def test_add_invalid_dataset(hass: HomeAssistant, hass_ws_client) -> None:
assert msg["error"] == {"code": "invalid_format", "message": "unknown type 222"} assert msg["error"] == {"code": "invalid_format", "message": "unknown type 222"}
async def test_delete_dataset(
hass: HomeAssistant, hass_ws_client: WebSocketGenerator
) -> None:
"""Test we can delete a dataset."""
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{"type": "thread/add_dataset_tlv", "source": "test", "tlv": DATASET_1}
)
msg = await client.receive_json()
assert msg["success"]
await client.send_json_auto_id(
{"type": "thread/add_dataset_tlv", "source": "test", "tlv": DATASET_2}
)
msg = await client.receive_json()
assert msg["success"]
await client.send_json_auto_id({"type": "thread/list_datasets"})
msg = await client.receive_json()
assert msg["success"]
datasets = msg["result"]["datasets"]
# Try deleting the preferred dataset
await client.send_json_auto_id(
{"type": "thread/delete_dataset", "dataset_id": datasets[0]["dataset_id"]}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"] == {
"code": "not_allowed",
"message": "attempt to remove preferred dataset",
}
# Try deleting a non preferred dataset
await client.send_json_auto_id(
{"type": "thread/delete_dataset", "dataset_id": datasets[1]["dataset_id"]}
)
msg = await client.receive_json()
assert msg["success"]
# Try deleting the same dataset again
await client.send_json_auto_id(
{"type": "thread/delete_dataset", "dataset_id": datasets[1]["dataset_id"]}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"] == {
"code": "not_found",
"message": f"'{datasets[1]['dataset_id']}'",
}
async def test_list_get_dataset(hass: HomeAssistant, hass_ws_client) -> None: async def test_list_get_dataset(hass: HomeAssistant, hass_ws_client) -> None:
"""Test list and get datasets.""" """Test list and get datasets."""
assert await async_setup_component(hass, DOMAIN, {}) assert await async_setup_component(hass, DOMAIN, {})