From cbc1899990c294d1c817b4c1533bb1a01fbc990b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Sat, 22 Feb 2025 16:48:39 +0100 Subject: [PATCH] Use builtin store for remember the milk config --- .../components/remember_the_milk/__init__.py | 68 +++++---- .../components/remember_the_milk/const.py | 1 + .../components/remember_the_milk/entity.py | 115 ++++++++++------ .../components/remember_the_milk/storage.py | 120 +++++++++++----- .../components/remember_the_milk/conftest.py | 3 +- tests/components/remember_the_milk/const.py | 13 +- .../remember_the_milk/test_storage.py | 129 +++++++++++------- 7 files changed, 294 insertions(+), 155 deletions(-) diff --git a/homeassistant/components/remember_the_milk/__init__.py b/homeassistant/components/remember_the_milk/__init__.py index fc192bd538a..63c411c51ba 100644 --- a/homeassistant/components/remember_the_milk/__init__.py +++ b/homeassistant/components/remember_the_milk/__init__.py @@ -5,20 +5,18 @@ import voluptuous as vol from homeassistant.components import configurator from homeassistant.const import CONF_API_KEY, CONF_ID, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType -from .const import LOGGER +from .const import DOMAIN, LOGGER from .entity import RememberTheMilkEntity from .storage import RememberTheMilkConfiguration # httplib2 is a transitive dependency from RtmAPI. If this dependency is not # set explicitly, the library does not work. -DOMAIN = "remember_the_milk" - CONF_SHARED_SECRET = "shared_secret" RTM_SCHEMA = vol.Schema( @@ -43,11 +41,12 @@ SERVICE_SCHEMA_CREATE_TASK = vol.Schema( SERVICE_SCHEMA_COMPLETE_TASK = vol.Schema({vol.Required(CONF_ID): cv.string}) -def setup(hass: HomeAssistant, config: ConfigType) -> bool: +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the Remember the milk component.""" component = EntityComponent[RememberTheMilkEntity](LOGGER, DOMAIN, hass) stored_rtm_config = RememberTheMilkConfiguration(hass) + await stored_rtm_config.setup() for rtm_config in config[DOMAIN]: account_name = rtm_config[CONF_NAME] LOGGER.debug("Adding Remember the milk account %s", account_name) @@ -56,7 +55,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: token = stored_rtm_config.get_token(account_name) if token: LOGGER.debug("found token for account %s", account_name) - _create_instance( + await _create_instance( hass, account_name, api_key, @@ -66,7 +65,7 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: component, ) else: - _register_new_account( + await _register_new_account( hass, account_name, api_key, shared_secret, stored_rtm_config, component ) @@ -74,20 +73,28 @@ def setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -def _create_instance( - hass, account_name, api_key, shared_secret, token, stored_rtm_config, component -): +async def _create_instance( + hass: HomeAssistant, + account_name: str, + api_key: str, + shared_secret: str, + token: str, + stored_rtm_config: RememberTheMilkConfiguration, + component: EntityComponent[RememberTheMilkEntity], +) -> None: entity = RememberTheMilkEntity( account_name, api_key, shared_secret, token, stored_rtm_config ) - component.add_entities([entity]) - hass.services.register( + LOGGER.debug("Instance created for account %s", entity.name) + await entity.check_token(hass) + await component.async_add_entities([entity]) + hass.services.async_register( DOMAIN, f"{account_name}_create_task", entity.create_task, schema=SERVICE_SCHEMA_CREATE_TASK, ) - hass.services.register( + hass.services.async_register( DOMAIN, f"{account_name}_complete_task", entity.complete_task, @@ -95,21 +102,32 @@ def _create_instance( ) -def _register_new_account( - hass, account_name, api_key, shared_secret, stored_rtm_config, component -): - request_id = None +async def _register_new_account( + hass: HomeAssistant, + account_name: str, + api_key: str, + shared_secret: str, + stored_rtm_config: RememberTheMilkConfiguration, + component: EntityComponent[RememberTheMilkEntity], +) -> None: + """Register a new account.""" api = Rtm(api_key, shared_secret, "write", None) - url, frob = api.authenticate_desktop() + url, frob = await hass.async_add_executor_job(api.authenticate_desktop) LOGGER.debug("Sent authentication request to server") + @callback def register_account_callback(fields: list[dict[str, str]]) -> None: """Call for register the configurator.""" - api.retrieve_token(frob) - token = api.token - if api.token is None: + hass.async_create_task(handle_token(api, frob)) + + async def handle_token(api: Rtm, frob: str) -> None: + """Handle token.""" + await hass.async_add_executor_job(api.retrieve_token, frob) + + token: str | None = api.token + if token is None: LOGGER.error("Failed to register, please try again") - configurator.notify_errors( + configurator.async_notify_errors( hass, request_id, "Failed to register, please try again." ) return @@ -117,7 +135,7 @@ def _register_new_account( stored_rtm_config.set_token(account_name, token) LOGGER.debug("Retrieved new token from server") - _create_instance( + await _create_instance( hass, account_name, api_key, @@ -127,9 +145,9 @@ def _register_new_account( component, ) - configurator.request_done(hass, request_id) + configurator.async_request_done(hass, request_id) - request_id = configurator.request_config( + request_id = configurator.async_request_config( hass, f"{DOMAIN} - {account_name}", callback=register_account_callback, diff --git a/homeassistant/components/remember_the_milk/const.py b/homeassistant/components/remember_the_milk/const.py index 2fccbf3ee52..b8f398b92f2 100644 --- a/homeassistant/components/remember_the_milk/const.py +++ b/homeassistant/components/remember_the_milk/const.py @@ -2,4 +2,5 @@ import logging +DOMAIN = "remember_the_milk" LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/remember_the_milk/entity.py b/homeassistant/components/remember_the_milk/entity.py index bf75debe367..08acc7a4075 100644 --- a/homeassistant/components/remember_the_milk/entity.py +++ b/homeassistant/components/remember_the_milk/entity.py @@ -1,18 +1,30 @@ """Support to interact with Remember The Milk.""" +from __future__ import annotations + +from typing import Any + from rtmapi import Rtm, RtmRequestFailedException from homeassistant.const import CONF_ID, CONF_NAME, STATE_OK -from homeassistant.core import ServiceCall +from homeassistant.core import HomeAssistant, ServiceCall from homeassistant.helpers.entity import Entity from .const import LOGGER +from .storage import RememberTheMilkConfiguration class RememberTheMilkEntity(Entity): """Representation of an interface to Remember The Milk.""" - def __init__(self, name, api_key, shared_secret, token, rtm_config): + def __init__( + self, + name: str, + api_key: str, + shared_secret: str, + token: str, + rtm_config: RememberTheMilkConfiguration, + ) -> None: """Create new instance of Remember The Milk component.""" self._name = name self._api_key = api_key @@ -20,29 +32,27 @@ class RememberTheMilkEntity(Entity): self._token = token self._rtm_config = rtm_config self._rtm_api = Rtm(api_key, shared_secret, "delete", token) - self._token_valid = None - self._check_token() - LOGGER.debug("Instance created for account %s", self._name) + self._token_valid = False - def _check_token(self): + async def check_token(self, hass: HomeAssistant) -> None: """Check if the API token is still valid. If it is not valid any more, delete it from the configuration. This will trigger a new authentication process. """ - valid = self._rtm_api.token_valid() - if not valid: - LOGGER.error( - "Token for account %s is invalid. You need to register again!", - self.name, - ) - self._rtm_config.delete_token(self._name) - self._token_valid = False - else: + valid = await hass.async_add_executor_job(self._rtm_api.token_valid) + if valid: self._token_valid = True - return self._token_valid + return - def create_task(self, call: ServiceCall) -> None: + LOGGER.error( + "Token for account %s is invalid. You need to register again!", + self.name, + ) + self._rtm_config.delete_token(self._name) + self._token_valid = False + + async def create_task(self, call: ServiceCall) -> None: """Create a new task on Remember The Milk. You can use the smart syntax to define the attributes of a new task, @@ -55,12 +65,11 @@ class RememberTheMilkEntity(Entity): rtm_id = None if hass_id is not None: rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) - result = self._rtm_api.rtm.timelines.create() - timeline = result.timeline.value if rtm_id is None: - result = self._rtm_api.rtm.tasks.add( - timeline=timeline, name=task_name, parse="1" + result = await self.hass.async_add_executor_job( + self._add_task, + task_name, ) LOGGER.debug( "Created new task '%s' in account %s", task_name, self.name @@ -74,12 +83,10 @@ class RememberTheMilkEntity(Entity): result.list.taskseries.task.id, ) else: - self._rtm_api.rtm.tasks.setName( - name=task_name, - list_id=rtm_id[0], - taskseries_id=rtm_id[1], - task_id=rtm_id[2], - timeline=timeline, + await self.hass.async_add_executor_job( + self._rename_task, + rtm_id, + task_name, ) LOGGER.debug( "Updated task with id '%s' in account %s to name %s", @@ -94,7 +101,29 @@ class RememberTheMilkEntity(Entity): rtm_exception, ) - def complete_task(self, call: ServiceCall) -> None: + def _add_task(self, task_name: str) -> Any: + """Add a task.""" + result = self._rtm_api.rtm.timelines.create() + timeline = result.timeline.value + return self._rtm_api.rtm.tasks.add( + timeline=timeline, + name=task_name, + parse="1", + ) + + def _rename_task(self, rtm_id: tuple[str, str, str], task_name: str) -> None: + """Rename a task.""" + result = self._rtm_api.rtm.timelines.create() + timeline = result.timeline.value + self._rtm_api.rtm.tasks.setName( + name=task_name, + list_id=rtm_id[0], + taskseries_id=rtm_id[1], + task_id=rtm_id[2], + timeline=timeline, + ) + + async def complete_task(self, call: ServiceCall) -> None: """Complete a task that was previously created by this component.""" hass_id = call.data[CONF_ID] rtm_id = self._rtm_config.get_rtm_id(self._name, hass_id) @@ -109,30 +138,36 @@ class RememberTheMilkEntity(Entity): ) return try: - result = self._rtm_api.rtm.timelines.create() - timeline = result.timeline.value - self._rtm_api.rtm.tasks.complete( - list_id=rtm_id[0], - taskseries_id=rtm_id[1], - task_id=rtm_id[2], - timeline=timeline, - ) - self._rtm_config.delete_rtm_id(self._name, hass_id) - LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name) + await self.hass.async_add_executor_job(self._complete_task, rtm_id) except RtmRequestFailedException as rtm_exception: LOGGER.error( "Error creating new Remember The Milk task for account %s: %s", self._name, rtm_exception, ) + return + + self._rtm_config.delete_rtm_id(self._name, hass_id) + LOGGER.debug("Completed task with id %s in account %s", hass_id, self._name) + + def _complete_task(self, rtm_id: tuple[str, str, str]) -> None: + """Complete a task.""" + result = self._rtm_api.rtm.timelines.create() + timeline = result.timeline.value + self._rtm_api.rtm.tasks.complete( + list_id=rtm_id[0], + taskseries_id=rtm_id[1], + task_id=rtm_id[2], + timeline=timeline, + ) @property - def name(self): + def name(self) -> str: """Return the name of the device.""" return self._name @property - def state(self): + def state(self) -> str: """Return the state of the device.""" if not self._token_valid: return "API token invalid" diff --git a/homeassistant/components/remember_the_milk/storage.py b/homeassistant/components/remember_the_milk/storage.py index ae51acd963b..af88780658f 100644 --- a/homeassistant/components/remember_the_milk/storage.py +++ b/homeassistant/components/remember_the_milk/storage.py @@ -4,17 +4,31 @@ from __future__ import annotations import json from pathlib import Path +from typing import TypedDict from homeassistant.const import CONF_TOKEN -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.storage import Store -from .const import LOGGER +from .const import DOMAIN, LOGGER -CONFIG_FILE_NAME = ".remember_the_milk.conf" -CONF_ID_MAP = "id_map" -CONF_LIST_ID = "list_id" -CONF_TASK_ID = "task_id" -CONF_TIMESERIES_ID = "timeseries_id" +LEGACY_CONFIG_FILE_NAME = ".remember_the_milk.conf" +STORE_DELAY_SAVE = 30 + + +class StoredUserConfig(TypedDict, total=False): + """Represent the stored config for a username.""" + + id_map: dict[str, TaskIds] + token: str + + +class TaskIds(TypedDict): + """Represent the stored ids of a task.""" + + list_id: str + timeseries_id: str + task_id: str class RememberTheMilkConfiguration: @@ -22,31 +36,63 @@ class RememberTheMilkConfiguration: def __init__(self, hass: HomeAssistant) -> None: """Create new instance of configuration.""" - self._config_file_path = hass.config.path(CONFIG_FILE_NAME) - self._config = {} - LOGGER.debug("Loading configuration from file: %s", self._config_file_path) + self._legacy_config_path = hass.config.path(LEGACY_CONFIG_FILE_NAME) + self._config: dict[str, StoredUserConfig] = {} + self._hass = hass + self._store = Store[dict[str, StoredUserConfig]](hass, 1, DOMAIN) + + async def setup(self) -> None: + """Set up the configuration.""" + if not (config := await self._hass.async_add_executor_job(self._load_legacy)): + config = await self._load() + + self._config = config or {} + + def _load_legacy(self) -> dict[str, StoredUserConfig] | None: + """Load configuration from legacy storage.""" + # Do not load from legacy if the new store exists. + if Path(self._store.path).exists(): + return None + + LOGGER.debug( + "Loading legacy configuration from file: %s", self._legacy_config_path + ) + config: dict[str, StoredUserConfig] | None = None + try: - self._config = json.loads( - Path(self._config_file_path).read_text(encoding="utf8") + config = json.loads( + Path(self._legacy_config_path).read_text(encoding="utf8") ) except FileNotFoundError: - LOGGER.debug("Missing configuration file: %s", self._config_file_path) + LOGGER.debug( + "Missing legacy configuration file: %s", self._legacy_config_path + ) except OSError: LOGGER.debug( - "Failed to read from configuration file, %s, using empty configuration", - self._config_file_path, + "Failed to read from legacy configuration file, %s, using empty configuration", + self._legacy_config_path, ) except ValueError: LOGGER.error( - "Failed to parse configuration file, %s, using empty configuration", - self._config_file_path, + "Failed to parse legacy configuration file, %s, using empty configuration", + self._legacy_config_path, ) + return config + + async def _load(self) -> dict[str, StoredUserConfig] | None: + """Load the store.""" + return await self._store.async_load() + + @callback def _save_config(self) -> None: - """Write the configuration to a file.""" - Path(self._config_file_path).write_text( - json.dumps(self._config), encoding="utf8" - ) + """Save config.""" + self._store.async_delay_save(self._data_to_save, STORE_DELAY_SAVE) + + @callback + def _data_to_save(self) -> dict[str, StoredUserConfig]: + """Return data to save.""" + return self._config def get_token(self, profile_name: str) -> str | None: """Get the server token for a profile.""" @@ -54,12 +100,14 @@ class RememberTheMilkConfiguration: return self._config[profile_name][CONF_TOKEN] return None + @callback def set_token(self, profile_name: str, token: str) -> None: """Store a new server token for a profile.""" self._initialize_profile(profile_name) self._config[profile_name][CONF_TOKEN] = token self._save_config() + @callback def delete_token(self, profile_name: str) -> None: """Delete a token for a profile. @@ -72,8 +120,8 @@ class RememberTheMilkConfiguration: """Initialize the data structures for a profile.""" if profile_name not in self._config: self._config[profile_name] = {} - if CONF_ID_MAP not in self._config[profile_name]: - self._config[profile_name][CONF_ID_MAP] = {} + if "id_map" not in self._config[profile_name]: + self._config[profile_name]["id_map"] = {} def get_rtm_id( self, profile_name: str, hass_id: str @@ -84,11 +132,12 @@ class RememberTheMilkConfiguration: list id, timeseries id and the task id. """ self._initialize_profile(profile_name) - ids = self._config[profile_name][CONF_ID_MAP].get(hass_id) - if ids is None: + task_ids = self._config[profile_name]["id_map"].get(hass_id) + if task_ids is None: return None - return ids[CONF_LIST_ID], ids[CONF_TIMESERIES_ID], ids[CONF_TASK_ID] + return task_ids["list_id"], task_ids["timeseries_id"], task_ids["task_id"] + @callback def set_rtm_id( self, profile_name: str, @@ -97,19 +146,20 @@ class RememberTheMilkConfiguration: time_series_id: str, rtm_task_id: str, ) -> None: - """Add/Update the RTM task ID for a Home Assistant task IS.""" + """Add/Update the RTM task ID for a Home Assistant task ID.""" self._initialize_profile(profile_name) - id_tuple = { - CONF_LIST_ID: list_id, - CONF_TIMESERIES_ID: time_series_id, - CONF_TASK_ID: rtm_task_id, - } - self._config[profile_name][CONF_ID_MAP][hass_id] = id_tuple + ids = TaskIds( + list_id=list_id, + timeseries_id=time_series_id, + task_id=rtm_task_id, + ) + self._config[profile_name]["id_map"][hass_id] = ids self._save_config() + @callback def delete_rtm_id(self, profile_name: str, hass_id: str) -> None: """Delete a key mapping.""" self._initialize_profile(profile_name) - if hass_id in self._config[profile_name][CONF_ID_MAP]: - del self._config[profile_name][CONF_ID_MAP][hass_id] + if hass_id in self._config[profile_name]["id_map"]: + del self._config[profile_name]["id_map"][hass_id] self._save_config() diff --git a/tests/components/remember_the_milk/conftest.py b/tests/components/remember_the_milk/conftest.py index f7257f35c64..44a3a58e030 100644 --- a/tests/components/remember_the_milk/conftest.py +++ b/tests/components/remember_the_milk/conftest.py @@ -32,7 +32,8 @@ def client_fixture() -> Generator[MagicMock]: async def storage(hass: HomeAssistant, client) -> AsyncGenerator[MagicMock]: """Mock the config storage.""" with patch( - "homeassistant.components.remember_the_milk.RememberTheMilkConfiguration" + "homeassistant.components.remember_the_milk.RememberTheMilkConfiguration", + autospec=True, ) as storage_class: storage = storage_class.return_value storage.get_token.return_value = TOKEN diff --git a/tests/components/remember_the_milk/const.py b/tests/components/remember_the_milk/const.py index 3f1d0067219..8b42e9cb8c9 100644 --- a/tests/components/remember_the_milk/const.py +++ b/tests/components/remember_the_milk/const.py @@ -4,11 +4,10 @@ import json PROFILE = "myprofile" TOKEN = "mytoken" -JSON_STRING = json.dumps( - { - "myprofile": { - "token": "mytoken", - "id_map": {"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}}, - } +STORED_DATA = { + "myprofile": { + "token": "mytoken", + "id_map": {"123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"}}, } -) +} +JSON_STRING = json.dumps(STORED_DATA) diff --git a/tests/components/remember_the_milk/test_storage.py b/tests/components/remember_the_milk/test_storage.py index 6ae774a3d0d..0b908efce78 100644 --- a/tests/components/remember_the_milk/test_storage.py +++ b/tests/components/remember_the_milk/test_storage.py @@ -1,54 +1,88 @@ """Tests for the Remember The Milk integration.""" -import json +from collections.abc import Generator +from typing import Any from unittest.mock import mock_open, patch import pytest -from homeassistant.components import remember_the_milk as rtm +from homeassistant.components.remember_the_milk import ( + DOMAIN, + RememberTheMilkConfiguration, +) from homeassistant.core import HomeAssistant -from .const import JSON_STRING, PROFILE, TOKEN +from .const import JSON_STRING, PROFILE, STORED_DATA, TOKEN + +from tests.common import async_fire_time_changed + +pytestmark = pytest.mark.parametrize("new_storage_exists", [True, False]) -def test_set_get_delete_token(hass: HomeAssistant) -> None: +@pytest.fixture(autouse=True) +def mock_path_exists(new_storage_exists: bool) -> Generator[None]: + """Mock path exists.""" + with patch( + "homeassistant.components.remember_the_milk.storage.Path.exists", + return_value=new_storage_exists, + ): + yield + + +@pytest.fixture(autouse=True) +def mock_delay_save() -> Generator[None]: + """Mock delay save.""" + with patch( + "homeassistant.components.remember_the_milk.storage.STORE_DELAY_SAVE", 0 + ): + yield + + +async def test_set_get_delete_token( + hass: HomeAssistant, + hass_storage: dict[str, Any], +) -> None: """Test set, get and delete token.""" open_mock = mock_open() + config = RememberTheMilkConfiguration(hass) with patch( "homeassistant.components.remember_the_milk.storage.Path.open", open_mock ): - config = rtm.RememberTheMilkConfiguration(hass) - assert open_mock.return_value.write.call_count == 0 + await config.setup() assert config.get_token(PROFILE) is None - assert open_mock.return_value.write.call_count == 0 config.set_token(PROFILE, TOKEN) - assert open_mock.return_value.write.call_count == 1 - assert open_mock.return_value.write.call_args[0][0] == json.dumps( - { - "myprofile": { - "id_map": {}, - "token": "mytoken", - } + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass_storage[DOMAIN]["data"] == { + "myprofile": { + "id_map": {}, + "token": "mytoken", } - ) + } assert config.get_token(PROFILE) == TOKEN - assert open_mock.return_value.write.call_count == 1 config.delete_token(PROFILE) - assert open_mock.return_value.write.call_count == 2 - assert open_mock.return_value.write.call_args[0][0] == json.dumps({}) + async_fire_time_changed(hass) + await hass.async_block_till_done() + assert hass_storage[DOMAIN]["data"] == {} assert config.get_token(PROFILE) is None - assert open_mock.return_value.write.call_count == 2 -def test_config_load(hass: HomeAssistant) -> None: +async def test_config_load(hass: HomeAssistant, hass_storage: dict[str, Any]) -> None: """Test loading from the file.""" + hass_storage[DOMAIN] = { + "data": STORED_DATA, + "key": DOMAIN, + "version": 1, + "minor_version": 1, + } + config = RememberTheMilkConfiguration(hass) with ( patch( "homeassistant.components.remember_the_milk.storage.Path.open", mock_open(read_data=JSON_STRING), ), ): - config = rtm.RememberTheMilkConfiguration(hass) + await config.setup() rtm_id = config.get_rtm_id(PROFILE, "123") assert rtm_id is not None @@ -58,16 +92,18 @@ def test_config_load(hass: HomeAssistant) -> None: @pytest.mark.parametrize( "side_effect", [FileNotFoundError("Missing file"), OSError("IO error")] ) -def test_config_load_file_error(hass: HomeAssistant, side_effect: Exception) -> None: +async def test_config_load_file_error( + hass: HomeAssistant, side_effect: Exception +) -> None: """Test loading with file error.""" - config = rtm.RememberTheMilkConfiguration(hass) + config = RememberTheMilkConfiguration(hass) with ( patch( "homeassistant.components.remember_the_milk.storage.Path.open", side_effect=side_effect, ), ): - config = rtm.RememberTheMilkConfiguration(hass) + await config.setup() # The config should be empty and we should not have any errors # when trying to access it. @@ -75,16 +111,16 @@ def test_config_load_file_error(hass: HomeAssistant, side_effect: Exception) -> assert rtm_id is None -def test_config_load_invalid_data(hass: HomeAssistant) -> None: +async def test_config_load_invalid_data(hass: HomeAssistant) -> None: """Test loading invalid data.""" - config = rtm.RememberTheMilkConfiguration(hass) + config = RememberTheMilkConfiguration(hass) with ( patch( "homeassistant.components.remember_the_milk.storage.Path.open", mock_open(read_data="random characters"), ), ): - config = rtm.RememberTheMilkConfiguration(hass) + await config.setup() # The config should be empty and we should not have any errors # when trying to access it. @@ -92,40 +128,39 @@ def test_config_load_invalid_data(hass: HomeAssistant) -> None: assert rtm_id is None -def test_config_set_delete_id(hass: HomeAssistant) -> None: +async def test_config_set_delete_id( + hass: HomeAssistant, + hass_storage: dict[str, Any], +) -> None: """Test setting and deleting an id from the config.""" hass_id = "123" list_id = "1" timeseries_id = "2" rtm_id = "3" open_mock = mock_open() - config = rtm.RememberTheMilkConfiguration(hass) + config = RememberTheMilkConfiguration(hass) with patch( "homeassistant.components.remember_the_milk.storage.Path.open", open_mock ): - config = rtm.RememberTheMilkConfiguration(hass) - assert open_mock.return_value.write.call_count == 0 + await config.setup() assert config.get_rtm_id(PROFILE, hass_id) is None - assert open_mock.return_value.write.call_count == 0 config.set_rtm_id(PROFILE, hass_id, list_id, timeseries_id, rtm_id) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert (list_id, timeseries_id, rtm_id) == config.get_rtm_id(PROFILE, hass_id) - assert open_mock.return_value.write.call_count == 1 - assert open_mock.return_value.write.call_args[0][0] == json.dumps( - { - "myprofile": { - "id_map": { - "123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"} - } + assert hass_storage[DOMAIN]["data"] == { + "myprofile": { + "id_map": { + "123": {"list_id": "1", "timeseries_id": "2", "task_id": "3"} } } - ) + } config.delete_rtm_id(PROFILE, hass_id) + async_fire_time_changed(hass) + await hass.async_block_till_done() assert config.get_rtm_id(PROFILE, hass_id) is None - assert open_mock.return_value.write.call_count == 2 - assert open_mock.return_value.write.call_args[0][0] == json.dumps( - { - "myprofile": { - "id_map": {}, - } + assert hass_storage[DOMAIN]["data"] == { + "myprofile": { + "id_map": {}, } - ) + }