Skip to content
Snippets Groups Projects
Unverified Commit c1e5673c authored by Josef Zweck's avatar Josef Zweck Committed by GitHub
Browse files

Allow rename of the backup folder for OneDrive (#138407)

parent 800fe1b0
No related branches found
No related tags found
No related merge requests found
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable
from html import unescape from html import unescape
from json import dumps, loads from json import dumps, loads
import logging import logging
...@@ -10,10 +11,10 @@ from typing import cast ...@@ -10,10 +11,10 @@ from typing import cast
from onedrive_personal_sdk import OneDriveClient from onedrive_personal_sdk import OneDriveClient
from onedrive_personal_sdk.exceptions import ( from onedrive_personal_sdk.exceptions import (
AuthenticationError, AuthenticationError,
HttpRequestException, NotFoundError,
OneDriveException, OneDriveException,
) )
from onedrive_personal_sdk.models.items import ItemUpdate from onedrive_personal_sdk.models.items import Item, ItemUpdate
from homeassistant.const import CONF_ACCESS_TOKEN, Platform from homeassistant.const import CONF_ACCESS_TOKEN, Platform
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
...@@ -25,7 +26,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import ( ...@@ -25,7 +26,7 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
) )
from homeassistant.helpers.instance_id import async_get as async_get_instance_id from homeassistant.helpers.instance_id import async_get as async_get_instance_id
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN from .const import CONF_FOLDER_ID, CONF_FOLDER_NAME, DATA_BACKUP_AGENT_LISTENERS, DOMAIN
from .coordinator import ( from .coordinator import (
OneDriveConfigEntry, OneDriveConfigEntry,
OneDriveRuntimeData, OneDriveRuntimeData,
...@@ -50,33 +51,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) -> ...@@ -50,33 +51,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
client = OneDriveClient(get_access_token, async_get_clientsession(hass)) client = OneDriveClient(get_access_token, async_get_clientsession(hass))
# get approot, will be created automatically if it does not exist # get approot, will be created automatically if it does not exist
try: approot = await _handle_item_operation(client.get_approot, "approot")
approot = await client.get_approot() folder_name = entry.data[CONF_FOLDER_NAME]
except AuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
except (HttpRequestException, OneDriveException, TimeoutError) as err:
_LOGGER.debug("Failed to get approot", exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_get_folder",
translation_placeholders={"folder": "approot"},
) from err
instance_id = await async_get_instance_id(hass)
backup_folder_name = f"backups_{instance_id[:8]}"
try: try:
backup_folder = await client.create_folder( backup_folder = await _handle_item_operation(
parent_id=approot.id, name=backup_folder_name lambda: client.get_drive_item(path_or_id=entry.data[CONF_FOLDER_ID]),
folder_name,
)
except NotFoundError:
_LOGGER.debug("Creating backup folder %s", folder_name)
backup_folder = await _handle_item_operation(
lambda: client.create_folder(parent_id=approot.id, name=folder_name),
folder_name,
)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_FOLDER_ID: backup_folder.id}
)
# write instance id to description
if backup_folder.description != (instance_id := await async_get_instance_id(hass)):
await _handle_item_operation(
lambda: client.update_drive_item(
backup_folder.id, ItemUpdate(description=instance_id)
),
folder_name,
)
# update in case folder was renamed manually inside OneDrive
if backup_folder.name != entry.data[CONF_FOLDER_NAME]:
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_FOLDER_NAME: backup_folder.name}
) )
except (HttpRequestException, OneDriveException, TimeoutError) as err:
_LOGGER.debug("Failed to create backup folder", exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_get_folder",
translation_placeholders={"folder": backup_folder_name},
) from err
coordinator = OneDriveUpdateCoordinator(hass, entry, client) coordinator = OneDriveUpdateCoordinator(hass, entry, client)
await coordinator.async_config_entry_first_refresh() await coordinator.async_config_entry_first_refresh()
...@@ -152,3 +158,47 @@ async def _migrate_backup_files(client: OneDriveClient, backup_folder_id: str) - ...@@ -152,3 +158,47 @@ async def _migrate_backup_files(client: OneDriveClient, backup_folder_id: str) -
data=ItemUpdate(description=""), data=ItemUpdate(description=""),
) )
_LOGGER.debug("Migrated backup file %s", file.name) _LOGGER.debug("Migrated backup file %s", file.name)
async def async_migrate_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) -> bool:
"""Migrate old entry."""
if entry.version > 1:
# This means the user has downgraded from a future version
return False
if (version := entry.version) == 1 and (minor_version := entry.minor_version) == 1:
_LOGGER.debug(
"Migrating OneDrive config entry from version %s.%s", version, minor_version
)
instance_id = await async_get_instance_id(hass)
hass.config_entries.async_update_entry(
entry,
data={
**entry.data,
CONF_FOLDER_ID: "id", # will be updated during setup_entry
CONF_FOLDER_NAME: f"backups_{instance_id[:8]}",
},
)
_LOGGER.debug("Migration to version 1.2 successful")
return True
async def _handle_item_operation(
func: Callable[[], Awaitable[Item]], folder: str
) -> Item:
try:
return await func()
except NotFoundError:
raise
except AuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
except (OneDriveException, TimeoutError) as err:
_LOGGER.debug("Failed to get approot", exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_get_folder",
translation_placeholders={"folder": folder},
) from err
...@@ -74,7 +74,7 @@ def async_register_backup_agents_listener( ...@@ -74,7 +74,7 @@ def async_register_backup_agents_listener(
def handle_backup_errors[_R, **P]( def handle_backup_errors[_R, **P](
func: Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]], func: Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]],
) -> Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]]: ) -> Callable[Concatenate[OneDriveBackupAgent, P], Coroutine[Any, Any, _R]]:
"""Handle backup errors with a specific translation key.""" """Handle backup errors."""
@wraps(func) @wraps(func)
async def wrapper( async def wrapper(
......
...@@ -8,22 +8,47 @@ from typing import Any, cast ...@@ -8,22 +8,47 @@ from typing import Any, cast
from onedrive_personal_sdk.clients.client import OneDriveClient from onedrive_personal_sdk.clients.client import OneDriveClient
from onedrive_personal_sdk.exceptions import OneDriveException from onedrive_personal_sdk.exceptions import OneDriveException
from onedrive_personal_sdk.models.items import AppRoot, ItemUpdate
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult, OptionsFlow from homeassistant.config_entries import (
SOURCE_REAUTH,
SOURCE_RECONFIGURE,
SOURCE_USER,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
from .const import CONF_DELETE_PERMANENTLY, DOMAIN, OAUTH_SCOPES from .const import (
CONF_DELETE_PERMANENTLY,
CONF_FOLDER_ID,
CONF_FOLDER_NAME,
DOMAIN,
OAUTH_SCOPES,
)
from .coordinator import OneDriveConfigEntry from .coordinator import OneDriveConfigEntry
FOLDER_NAME_SCHEMA = vol.Schema({vol.Required(CONF_FOLDER_NAME): str})
class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN): class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Config flow to handle OneDrive OAuth2 authentication.""" """Config flow to handle OneDrive OAuth2 authentication."""
DOMAIN = DOMAIN DOMAIN = DOMAIN
MINOR_VERSION = 2
client: OneDriveClient
approot: AppRoot
def __init__(self) -> None:
"""Initialize the OneDrive config flow."""
super().__init__()
self.step_data: dict[str, Any] = {}
@property @property
def logger(self) -> logging.Logger: def logger(self) -> logging.Logger:
...@@ -35,6 +60,15 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN): ...@@ -35,6 +60,15 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
"""Extra data that needs to be appended to the authorize url.""" """Extra data that needs to be appended to the authorize url."""
return {"scope": " ".join(OAUTH_SCOPES)} return {"scope": " ".join(OAUTH_SCOPES)}
@property
def apps_folder(self) -> str:
"""Return the name of the Apps folder (translated)."""
return (
path.split("/")[-1]
if (path := self.approot.parent_reference.path)
else "Apps"
)
async def async_oauth_create_entry( async def async_oauth_create_entry(
self, self,
data: dict[str, Any], data: dict[str, Any],
...@@ -44,12 +78,12 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN): ...@@ -44,12 +78,12 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
async def get_access_token() -> str: async def get_access_token() -> str:
return cast(str, data[CONF_TOKEN][CONF_ACCESS_TOKEN]) return cast(str, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
graph_client = OneDriveClient( self.client = OneDriveClient(
get_access_token, async_get_clientsession(self.hass) get_access_token, async_get_clientsession(self.hass)
) )
try: try:
approot = await graph_client.get_approot() self.approot = await self.client.get_approot()
except OneDriveException: except OneDriveException:
self.logger.exception("Failed to connect to OneDrive") self.logger.exception("Failed to connect to OneDrive")
return self.async_abort(reason="connection_error") return self.async_abort(reason="connection_error")
...@@ -57,26 +91,118 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN): ...@@ -57,26 +91,118 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
self.logger.exception("Unknown error") self.logger.exception("Unknown error")
return self.async_abort(reason="unknown") return self.async_abort(reason="unknown")
await self.async_set_unique_id(approot.parent_reference.drive_id) await self.async_set_unique_id(self.approot.parent_reference.drive_id)
if self.source == SOURCE_REAUTH: if self.source != SOURCE_USER:
reauth_entry = self._get_reauth_entry()
self._abort_if_unique_id_mismatch( self._abort_if_unique_id_mismatch(
reason="wrong_drive", reason="wrong_drive",
) )
if self.source == SOURCE_REAUTH:
reauth_entry = self._get_reauth_entry()
return self.async_update_reload_and_abort( return self.async_update_reload_and_abort(
entry=reauth_entry, entry=reauth_entry,
data=data, data=data,
) )
self._abort_if_unique_id_configured() if self.source != SOURCE_RECONFIGURE:
self._abort_if_unique_id_configured()
self.step_data = data
title = ( if self.source == SOURCE_RECONFIGURE:
f"{approot.created_by.user.display_name}'s OneDrive" return await self.async_step_reconfigure_folder()
if approot.created_by.user and approot.created_by.user.display_name
else "OneDrive" return await self.async_step_folder_name()
async def async_step_folder_name(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Step to ask for the folder name."""
errors: dict[str, str] = {}
instance_id = await async_get_instance_id(self.hass)
if user_input is not None:
try:
folder = await self.client.create_folder(
self.approot.id, user_input[CONF_FOLDER_NAME]
)
except OneDriveException:
self.logger.debug("Failed to create folder", exc_info=True)
errors["base"] = "folder_creation_error"
else:
if folder.description and folder.description != instance_id:
errors[CONF_FOLDER_NAME] = "folder_already_in_use"
if not errors:
title = (
f"{self.approot.created_by.user.display_name}'s OneDrive"
if self.approot.created_by.user
and self.approot.created_by.user.display_name
else "OneDrive"
)
return self.async_create_entry(
title=title,
data={
**self.step_data,
CONF_FOLDER_ID: folder.id,
CONF_FOLDER_NAME: user_input[CONF_FOLDER_NAME],
},
)
default_folder_name = (
f"backups_{instance_id[:8]}"
if user_input is None
else user_input[CONF_FOLDER_NAME]
)
return self.async_show_form(
step_id="folder_name",
data_schema=self.add_suggested_values_to_schema(
FOLDER_NAME_SCHEMA, {CONF_FOLDER_NAME: default_folder_name}
),
description_placeholders={
"apps_folder": self.apps_folder,
"approot": self.approot.name,
},
errors=errors,
)
async def async_step_reconfigure_folder(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the folder name."""
errors: dict[str, str] = {}
reconfigure_entry = self._get_reconfigure_entry()
if user_input is not None:
if (
new_folder_name := user_input[CONF_FOLDER_NAME]
) != reconfigure_entry.data[CONF_FOLDER_NAME]:
try:
await self.client.update_drive_item(
reconfigure_entry.data[CONF_FOLDER_ID],
ItemUpdate(name=new_folder_name),
)
except OneDriveException:
self.logger.debug("Failed to update folder", exc_info=True)
errors["base"] = "folder_rename_error"
if not errors:
return self.async_update_reload_and_abort(
reconfigure_entry,
data={**reconfigure_entry.data, CONF_FOLDER_NAME: new_folder_name},
)
return self.async_show_form(
step_id="reconfigure_folder",
data_schema=self.add_suggested_values_to_schema(
FOLDER_NAME_SCHEMA,
{CONF_FOLDER_NAME: reconfigure_entry.data[CONF_FOLDER_NAME]},
),
description_placeholders={
"apps_folder": self.apps_folder,
"approot": self.approot.name,
},
errors=errors,
) )
return self.async_create_entry(title=title, data=data)
async def async_step_reauth( async def async_step_reauth(
self, entry_data: Mapping[str, Any] self, entry_data: Mapping[str, Any]
...@@ -92,6 +218,12 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN): ...@@ -92,6 +218,12 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
return self.async_show_form(step_id="reauth_confirm") return self.async_show_form(step_id="reauth_confirm")
return await self.async_step_user() return await self.async_step_user()
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Reconfigure the entry."""
return await self.async_step_user()
@staticmethod @staticmethod
@callback @callback
def async_get_options_flow( def async_get_options_flow(
......
...@@ -6,6 +6,8 @@ from typing import Final ...@@ -6,6 +6,8 @@ from typing import Final
from homeassistant.util.hass_dict import HassKey from homeassistant.util.hass_dict import HassKey
DOMAIN: Final = "onedrive" DOMAIN: Final = "onedrive"
CONF_FOLDER_NAME: Final = "folder_name"
CONF_FOLDER_ID: Final = "folder_id"
CONF_DELETE_PERMANENTLY: Final = "delete_permanently" CONF_DELETE_PERMANENTLY: Final = "delete_permanently"
......
...@@ -73,10 +73,7 @@ rules: ...@@ -73,10 +73,7 @@ rules:
entity-translations: done entity-translations: done
exception-translations: done exception-translations: done
icon-translations: done icon-translations: done
reconfiguration-flow: reconfiguration-flow: done
status: exempt
comment: |
Nothing to reconfigure.
repair-issues: done repair-issues: done
stale-devices: stale-devices:
status: exempt status: exempt
......
...@@ -7,6 +7,26 @@ ...@@ -7,6 +7,26 @@
"reauth_confirm": { "reauth_confirm": {
"title": "[%key:common::config_flow::title::reauth%]", "title": "[%key:common::config_flow::title::reauth%]",
"description": "The OneDrive integration needs to re-authenticate your account" "description": "The OneDrive integration needs to re-authenticate your account"
},
"folder_name": {
"title": "Pick a folder name",
"description": "This name will be used to create a folder that is specific for this Home Assistant instance. This folder will be created inside `{apps_folder}/{approot}`",
"data": {
"folder_name": "Folder name"
},
"data_description": {
"folder_name": "Name of the folder"
}
},
"reconfigure_folder": {
"title": "Change the folder name",
"description": "Rename the instance specific folder inside `{apps_folder}/{approot}`. This will only rename the folder (and does not select another folder), so make sure the new name is not already in use.",
"data": {
"folder_name": "[%key:component::onedrive::config::step::folder_name::data::folder_name%]"
},
"data_description": {
"folder_name": "[%key:component::onedrive::config::step::folder_name::data_description::folder_name%]"
}
} }
}, },
"abort": { "abort": {
...@@ -23,10 +43,16 @@ ...@@ -23,10 +43,16 @@
"connection_error": "Failed to connect to OneDrive.", "connection_error": "Failed to connect to OneDrive.",
"wrong_drive": "New account does not contain previously configured OneDrive.", "wrong_drive": "New account does not contain previously configured OneDrive.",
"unknown": "[%key:common::config_flow::error::unknown%]", "unknown": "[%key:common::config_flow::error::unknown%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}, },
"create_entry": { "create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]" "default": "[%key:common::config_flow::create_entry::authenticated%]"
},
"error": {
"folder_rename_error": "Failed to rename folder",
"folder_creation_error": "Failed to create folder",
"folder_already_in_use": "Folder already used for backups from another Home Assistant instance"
} }
}, },
"options": { "options": {
......
...@@ -5,13 +5,28 @@ from json import dumps ...@@ -5,13 +5,28 @@ from json import dumps
import time import time
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from onedrive_personal_sdk.const import DriveState, DriveType
from onedrive_personal_sdk.models.items import (
AppRoot,
Drive,
DriveQuota,
Folder,
IdentitySet,
ItemParentReference,
User,
)
import pytest import pytest
from homeassistant.components.application_credentials import ( from homeassistant.components.application_credentials import (
ClientCredential, ClientCredential,
async_import_client_credential, async_import_client_credential,
) )
from homeassistant.components.onedrive.const import DOMAIN, OAUTH_SCOPES from homeassistant.components.onedrive.const import (
CONF_FOLDER_ID,
CONF_FOLDER_NAME,
DOMAIN,
OAUTH_SCOPES,
)
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
...@@ -19,10 +34,9 @@ from .const import ( ...@@ -19,10 +34,9 @@ from .const import (
BACKUP_METADATA, BACKUP_METADATA,
CLIENT_ID, CLIENT_ID,
CLIENT_SECRET, CLIENT_SECRET,
MOCK_APPROOT, IDENTITY_SET,
INSTANCE_ID,
MOCK_BACKUP_FILE, MOCK_BACKUP_FILE,
MOCK_BACKUP_FOLDER,
MOCK_DRIVE,
MOCK_METADATA_FILE, MOCK_METADATA_FILE,
) )
...@@ -66,8 +80,11 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry: ...@@ -66,8 +80,11 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
"expires_at": expires_at, "expires_at": expires_at,
"scope": " ".join(scopes), "scope": " ".join(scopes),
}, },
CONF_FOLDER_NAME: "backups_123",
CONF_FOLDER_ID: "my_folder_id",
}, },
unique_id="mock_drive_id", unique_id="mock_drive_id",
minor_version=2,
) )
...@@ -87,14 +104,80 @@ def mock_onedrive_client_init() -> Generator[MagicMock]: ...@@ -87,14 +104,80 @@ def mock_onedrive_client_init() -> Generator[MagicMock]:
yield onedrive_client yield onedrive_client
@pytest.fixture
def mock_approot() -> AppRoot:
"""Return a mocked approot."""
return AppRoot(
id="id",
child_count=0,
size=0,
name="name",
parent_reference=ItemParentReference(
drive_id="mock_drive_id", id="id", path="path"
),
created_by=IdentitySet(
user=User(
display_name="John Doe",
id="id",
email="john@doe.com",
)
),
)
@pytest.fixture
def mock_drive() -> Drive:
"""Return a mocked drive."""
return Drive(
id="mock_drive_id",
name="My Drive",
drive_type=DriveType.PERSONAL,
owner=IDENTITY_SET,
quota=DriveQuota(
deleted=5,
remaining=805306368,
state=DriveState.NEARING,
total=5368709120,
used=4250000000,
),
)
@pytest.fixture
def mock_folder() -> Folder:
"""Return a mocked backup folder."""
return Folder(
id="my_folder_id",
name="name",
size=0,
child_count=0,
description="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0",
parent_reference=ItemParentReference(
drive_id="mock_drive_id", id="id", path="path"
),
created_by=IdentitySet(
user=User(
display_name="John Doe",
id="id",
email="john@doe.com",
),
),
)
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_onedrive_client(mock_onedrive_client_init: MagicMock) -> Generator[MagicMock]: def mock_onedrive_client(
mock_onedrive_client_init: MagicMock,
mock_approot: AppRoot,
mock_drive: Drive,
mock_folder: Folder,
) -> Generator[MagicMock]:
"""Return a mocked GraphServiceClient.""" """Return a mocked GraphServiceClient."""
client = mock_onedrive_client_init.return_value client = mock_onedrive_client_init.return_value
client.get_approot.return_value = MOCK_APPROOT client.get_approot.return_value = mock_approot
client.create_folder.return_value = MOCK_BACKUP_FOLDER client.create_folder.return_value = mock_folder
client.list_drive_items.return_value = [MOCK_BACKUP_FILE, MOCK_METADATA_FILE] client.list_drive_items.return_value = [MOCK_BACKUP_FILE, MOCK_METADATA_FILE]
client.get_drive_item.return_value = MOCK_BACKUP_FILE client.get_drive_item.return_value = mock_folder
client.upload_file.return_value = MOCK_METADATA_FILE client.upload_file.return_value = MOCK_METADATA_FILE
class MockStreamReader: class MockStreamReader:
...@@ -105,7 +188,7 @@ def mock_onedrive_client(mock_onedrive_client_init: MagicMock) -> Generator[Magi ...@@ -105,7 +188,7 @@ def mock_onedrive_client(mock_onedrive_client_init: MagicMock) -> Generator[Magi
return dumps(BACKUP_METADATA).encode() return dumps(BACKUP_METADATA).encode()
client.download_drive_item.return_value = MockStreamReader() client.download_drive_item.return_value = MockStreamReader()
client.get_drive.return_value = MOCK_DRIVE client.get_drive.return_value = mock_drive
return client return client
...@@ -131,8 +214,14 @@ def mock_setup_entry() -> Generator[AsyncMock]: ...@@ -131,8 +214,14 @@ def mock_setup_entry() -> Generator[AsyncMock]:
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_instance_id() -> Generator[AsyncMock]: def mock_instance_id() -> Generator[AsyncMock]:
"""Mock the instance ID.""" """Mock the instance ID."""
with patch( with (
"homeassistant.components.onedrive.async_get_instance_id", patch(
return_value="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0", "homeassistant.components.onedrive.async_get_instance_id",
return_value=INSTANCE_ID,
) as mock_instance_id,
patch(
"homeassistant.components.onedrive.config_flow.async_get_instance_id",
new=mock_instance_id,
),
): ):
yield yield
...@@ -3,13 +3,8 @@ ...@@ -3,13 +3,8 @@
from html import escape from html import escape
from json import dumps from json import dumps
from onedrive_personal_sdk.const import DriveState, DriveType
from onedrive_personal_sdk.models.items import ( from onedrive_personal_sdk.models.items import (
AppRoot,
Drive,
DriveQuota,
File, File,
Folder,
Hashes, Hashes,
IdentitySet, IdentitySet,
ItemParentReference, ItemParentReference,
...@@ -34,6 +29,8 @@ BACKUP_METADATA = { ...@@ -34,6 +29,8 @@ BACKUP_METADATA = {
"size": 34519040, "size": 34519040,
} }
INSTANCE_ID = "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0"
IDENTITY_SET = IdentitySet( IDENTITY_SET = IdentitySet(
user=User( user=User(
display_name="John Doe", display_name="John Doe",
...@@ -42,28 +39,6 @@ IDENTITY_SET = IdentitySet( ...@@ -42,28 +39,6 @@ IDENTITY_SET = IdentitySet(
) )
) )
MOCK_APPROOT = AppRoot(
id="id",
child_count=0,
size=0,
name="name",
parent_reference=ItemParentReference(
drive_id="mock_drive_id", id="id", path="path"
),
created_by=IDENTITY_SET,
)
MOCK_BACKUP_FOLDER = Folder(
id="id",
name="name",
size=0,
child_count=0,
parent_reference=ItemParentReference(
drive_id="mock_drive_id", id="id", path="path"
),
created_by=IDENTITY_SET,
)
MOCK_BACKUP_FILE = File( MOCK_BACKUP_FILE = File(
id="id", id="id",
name="23e64aec.tar", name="23e64aec.tar",
...@@ -75,7 +50,6 @@ MOCK_BACKUP_FILE = File( ...@@ -75,7 +50,6 @@ MOCK_BACKUP_FILE = File(
quick_xor_hash="hash", quick_xor_hash="hash",
), ),
mime_type="application/x-tar", mime_type="application/x-tar",
description="",
created_by=IDENTITY_SET, created_by=IDENTITY_SET,
) )
...@@ -101,18 +75,3 @@ MOCK_METADATA_FILE = File( ...@@ -101,18 +75,3 @@ MOCK_METADATA_FILE = File(
), ),
created_by=IDENTITY_SET, created_by=IDENTITY_SET,
) )
MOCK_DRIVE = Drive(
id="mock_drive_id",
name="My Drive",
drive_type=DriveType.PERSONAL,
owner=IDENTITY_SET,
quota=DriveQuota(
deleted=5,
remaining=805306368,
state=DriveState.NEARING,
total=5368709120,
used=4250000000,
),
)
...@@ -4,11 +4,14 @@ from http import HTTPStatus ...@@ -4,11 +4,14 @@ from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock from unittest.mock import AsyncMock, MagicMock
from onedrive_personal_sdk.exceptions import OneDriveException from onedrive_personal_sdk.exceptions import OneDriveException
from onedrive_personal_sdk.models.items import AppRoot, Folder, ItemUpdate
import pytest import pytest
from homeassistant import config_entries from homeassistant import config_entries
from homeassistant.components.onedrive.const import ( from homeassistant.components.onedrive.const import (
CONF_DELETE_PERMANENTLY, CONF_DELETE_PERMANENTLY,
CONF_FOLDER_ID,
CONF_FOLDER_NAME,
DOMAIN, DOMAIN,
OAUTH2_AUTHORIZE, OAUTH2_AUTHORIZE,
OAUTH2_TOKEN, OAUTH2_TOKEN,
...@@ -20,7 +23,7 @@ from homeassistant.data_entry_flow import FlowResultType ...@@ -20,7 +23,7 @@ from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers import config_entry_oauth2_flow
from . import setup_integration from . import setup_integration
from .const import CLIENT_ID, MOCK_APPROOT from .const import CLIENT_ID
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker from tests.test_util.aiohttp import AiohttpClientMocker
...@@ -85,6 +88,11 @@ async def test_full_flow( ...@@ -85,6 +88,11 @@ async def test_full_flow(
token_callback = mock_onedrive_client_init.call_args[0][0] token_callback = mock_onedrive_client_init.call_args[0][0]
assert await token_callback() == "mock-access-token" assert await token_callback() == "mock-access-token"
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
...@@ -92,6 +100,8 @@ async def test_full_flow( ...@@ -92,6 +100,8 @@ async def test_full_flow(
assert result["result"].unique_id == "mock_drive_id" assert result["result"].unique_id == "mock_drive_id"
assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token" assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token" assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert result["data"][CONF_FOLDER_NAME] == "myFolder"
assert result["data"][CONF_FOLDER_ID] == "my_folder_id"
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
...@@ -101,10 +111,11 @@ async def test_full_flow_with_owner_not_found( ...@@ -101,10 +111,11 @@ async def test_full_flow_with_owner_not_found(
aioclient_mock: AiohttpClientMocker, aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_onedrive_client: MagicMock, mock_onedrive_client: MagicMock,
mock_approot: MagicMock,
) -> None: ) -> None:
"""Ensure we get a default title if the drive's owner can't be read.""" """Ensure we get a default title if the drive's owner can't be read."""
mock_onedrive_client.get_approot.return_value.created_by.user = None mock_approot.created_by.user = None
result = await hass.config_entries.flow.async_init( result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER} DOMAIN, context={"source": config_entries.SOURCE_USER}
...@@ -112,6 +123,11 @@ async def test_full_flow_with_owner_not_found( ...@@ -112,6 +123,11 @@ async def test_full_flow_with_owner_not_found(
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock) await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"]) result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY assert result["type"] is FlowResultType.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1
...@@ -119,6 +135,94 @@ async def test_full_flow_with_owner_not_found( ...@@ -119,6 +135,94 @@ async def test_full_flow_with_owner_not_found(
assert result["result"].unique_id == "mock_drive_id" assert result["result"].unique_id == "mock_drive_id"
assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token" assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token" assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert result["data"][CONF_FOLDER_NAME] == "myFolder"
assert result["data"][CONF_FOLDER_ID] == "my_folder_id"
mock_onedrive_client.reset_mock()
@pytest.mark.usefixtures("current_request_with_host")
async def test_folder_already_in_use(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_onedrive_client: MagicMock,
mock_instance_id: AsyncMock,
mock_folder: Folder,
) -> None:
"""Ensure a folder that is already in use is not allowed."""
mock_folder.description = "1234"
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {CONF_FOLDER_NAME: "folder_already_in_use"}
# clear error and try again
mock_onedrive_client.create_folder.return_value.description = mock_instance_id
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "John Doe's OneDrive"
assert result["result"].unique_id == "mock_drive_id"
assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert result["data"][CONF_FOLDER_NAME] == "myFolder"
assert result["data"][CONF_FOLDER_ID] == "my_folder_id"
@pytest.mark.usefixtures("current_request_with_host")
async def test_error_during_folder_creation(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_onedrive_client: MagicMock,
) -> None:
"""Ensure we can create the backup folder."""
mock_onedrive_client.create_folder.side_effect = OneDriveException()
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.FORM
assert result["errors"] == {"base": "folder_creation_error"}
mock_onedrive_client.create_folder.side_effect = None
# clear error and try again
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "myFolder"}
)
assert result["type"] is FlowResultType.CREATE_ENTRY
assert result["title"] == "John Doe's OneDrive"
assert result["result"].unique_id == "mock_drive_id"
assert result["data"][CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
assert result["data"][CONF_FOLDER_NAME] == "myFolder"
assert result["data"][CONF_FOLDER_ID] == "my_folder_id"
@pytest.mark.usefixtures("current_request_with_host") @pytest.mark.usefixtures("current_request_with_host")
...@@ -205,11 +309,11 @@ async def test_reauth_flow_id_changed( ...@@ -205,11 +309,11 @@ async def test_reauth_flow_id_changed(
mock_setup_entry: AsyncMock, mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock, mock_onedrive_client: MagicMock,
mock_approot: AppRoot,
) -> None: ) -> None:
"""Test that the reauth flow fails on a different drive id.""" """Test that the reauth flow fails on a different drive id."""
app_root = MOCK_APPROOT
app_root.parent_reference.drive_id = "other_drive_id" mock_approot.parent_reference.drive_id = "other_drive_id"
mock_onedrive_client.get_approot.return_value = app_root
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
...@@ -226,6 +330,104 @@ async def test_reauth_flow_id_changed( ...@@ -226,6 +330,104 @@ async def test_reauth_flow_id_changed(
assert result["reason"] == "wrong_drive" assert result["reason"] == "wrong_drive"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Testing reconfgure flow."""
await setup_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "newFolder"}
)
assert result["type"] is FlowResultType.ABORT
mock_onedrive_client.update_drive_item.assert_called_once_with(
mock_config_entry.data[CONF_FOLDER_ID], ItemUpdate(name="newFolder")
)
assert mock_config_entry.data[CONF_FOLDER_NAME] == "newFolder"
assert mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow_error(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_setup_entry: AsyncMock,
) -> None:
"""Testing reconfgure flow errors."""
mock_config_entry.add_to_hass(hass)
await hass.async_block_till_done()
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
mock_onedrive_client.update_drive_item.side_effect = OneDriveException()
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "newFolder"}
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure_folder"
assert result["errors"] == {"base": "folder_rename_error"}
# clear side effect
mock_onedrive_client.update_drive_item.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"], {CONF_FOLDER_NAME: "newFolder"}
)
assert mock_config_entry.data[CONF_FOLDER_NAME] == "newFolder"
assert mock_config_entry.data[CONF_TOKEN][CONF_ACCESS_TOKEN] == "mock-access-token"
assert mock_config_entry.data[CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host")
async def test_reconfigure_flow_id_changed(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock,
mock_approot: AppRoot,
) -> None:
"""Test that the reconfigure flow fails on a different drive id."""
mock_approot.parent_reference.drive_id = "other_drive_id"
mock_config_entry.add_to_hass(hass)
await hass.async_block_till_done()
result = await mock_config_entry.start_reconfigure_flow(hass)
await _do_get_token(hass, result, hass_client_no_auth, aioclient_mock)
result = await hass.config_entries.flow.async_configure(result["flow_id"])
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "wrong_drive"
async def test_options_flow( async def test_options_flow(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
......
"""Test the OneDrive setup.""" """Test the OneDrive setup."""
from copy import deepcopy from copy import copy
from html import escape from html import escape
from json import dumps from json import dumps
from unittest.mock import MagicMock from unittest.mock import MagicMock
from onedrive_personal_sdk.const import DriveState from onedrive_personal_sdk.const import DriveState
from onedrive_personal_sdk.exceptions import AuthenticationError, OneDriveException from onedrive_personal_sdk.exceptions import (
AuthenticationError,
NotFoundError,
OneDriveException,
)
from onedrive_personal_sdk.models.items import AppRoot, Drive, Folder, ItemUpdate
import pytest import pytest
from syrupy import SnapshotAssertion from syrupy import SnapshotAssertion
from homeassistant.components.onedrive.const import DOMAIN from homeassistant.components.onedrive.const import (
CONF_FOLDER_ID,
CONF_FOLDER_NAME,
DOMAIN,
)
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, issue_registry as ir from homeassistant.helpers import device_registry as dr, issue_registry as ir
from . import setup_integration from . import setup_integration
from .const import BACKUP_METADATA, MOCK_BACKUP_FILE, MOCK_DRIVE from .const import BACKUP_METADATA, INSTANCE_ID, MOCK_BACKUP_FILE
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
...@@ -72,11 +81,64 @@ async def test_get_integration_folder_error( ...@@ -72,11 +81,64 @@ async def test_get_integration_folder_error(
mock_onedrive_client: MagicMock, mock_onedrive_client: MagicMock,
caplog: pytest.LogCaptureFixture, caplog: pytest.LogCaptureFixture,
) -> None: ) -> None:
"""Test faulty approot retrieval.""" """Test faulty integration folder retrieval."""
mock_onedrive_client.get_drive_item.side_effect = OneDriveException()
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to get backups_123 folder" in caplog.text
async def test_get_integration_folder_creation(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock,
mock_approot: AppRoot,
mock_folder: Folder,
) -> None:
"""Test faulty integration folder creation."""
folder_name = copy(mock_config_entry.data[CONF_FOLDER_NAME])
mock_onedrive_client.get_drive_item.side_effect = NotFoundError(404, "Not found")
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED
mock_onedrive_client.create_folder.assert_called_once_with(
parent_id=mock_approot.id,
name=folder_name,
)
# ensure the folder id and name are updated
assert mock_config_entry.data[CONF_FOLDER_ID] == mock_folder.id
assert mock_config_entry.data[CONF_FOLDER_NAME] == mock_folder.name
async def test_get_integration_folder_creation_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test faulty integration folder creation error."""
mock_onedrive_client.get_drive_item.side_effect = NotFoundError(404, "Not found")
mock_onedrive_client.create_folder.side_effect = OneDriveException() mock_onedrive_client.create_folder.side_effect = OneDriveException()
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to get backups_9f86d081 folder" in caplog.text assert "Failed to get backups_123 folder" in caplog.text
async def test_update_instance_id_description(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock,
mock_folder: Folder,
) -> None:
"""Test we write the instance id to the folder."""
mock_folder.description = ""
await setup_integration(hass, mock_config_entry)
await hass.async_block_till_done()
mock_onedrive_client.update_drive_item.assert_called_with(
mock_folder.id, ItemUpdate(description=INSTANCE_ID)
)
async def test_migrate_metadata_files( async def test_migrate_metadata_files(
...@@ -125,12 +187,13 @@ async def test_device( ...@@ -125,12 +187,13 @@ async def test_device(
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
device_registry: dr.DeviceRegistry, device_registry: dr.DeviceRegistry,
snapshot: SnapshotAssertion, snapshot: SnapshotAssertion,
mock_drive: Drive,
) -> None: ) -> None:
"""Test the device.""" """Test the device."""
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
device = device_registry.async_get_device({(DOMAIN, MOCK_DRIVE.id)}) device = device_registry.async_get_device({(DOMAIN, mock_drive.id)})
assert device assert device
assert device == snapshot assert device == snapshot
...@@ -154,17 +217,62 @@ async def test_data_cap_issues( ...@@ -154,17 +217,62 @@ async def test_data_cap_issues(
hass: HomeAssistant, hass: HomeAssistant,
mock_config_entry: MockConfigEntry, mock_config_entry: MockConfigEntry,
mock_onedrive_client: MagicMock, mock_onedrive_client: MagicMock,
mock_drive: Drive,
drive_state: DriveState, drive_state: DriveState,
issue_key: str, issue_key: str,
issue_exists: bool, issue_exists: bool,
) -> None: ) -> None:
"""Make sure we get issues for high data usage.""" """Make sure we get issues for high data usage."""
mock_drive = deepcopy(MOCK_DRIVE)
assert mock_drive.quota assert mock_drive.quota
mock_drive.quota.state = drive_state mock_drive.quota.state = drive_state
mock_onedrive_client.get_drive.return_value = mock_drive
await setup_integration(hass, mock_config_entry) await setup_integration(hass, mock_config_entry)
issue_registry = ir.async_get(hass) issue_registry = ir.async_get(hass)
issue = issue_registry.async_get_issue(DOMAIN, issue_key) issue = issue_registry.async_get_issue(DOMAIN, issue_key)
assert (issue is not None) == issue_exists assert (issue is not None) == issue_exists
async def test_1_1_to_1_2_migration(
hass: HomeAssistant,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
mock_folder: Folder,
) -> None:
"""Test migration from 1.1 to 1.2."""
old_config_entry = MockConfigEntry(
unique_id="mock_drive_id",
title="John Doe's OneDrive",
domain=DOMAIN,
data={
"auth_implementation": mock_config_entry.data["auth_implementation"],
"token": mock_config_entry.data["token"],
},
)
# will always 404 after migration, because of dummy id
mock_onedrive_client.get_drive_item.side_effect = NotFoundError(404, "Not found")
await setup_integration(hass, old_config_entry)
assert old_config_entry.data[CONF_FOLDER_ID] == mock_folder.id
assert old_config_entry.data[CONF_FOLDER_NAME] == mock_folder.name
async def test_migration_guard_against_major_downgrade(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test migration guards against major downgrades."""
old_config_entry = MockConfigEntry(
unique_id="mock_drive_id",
title="John Doe's OneDrive",
domain=DOMAIN,
data={
"auth_implementation": mock_config_entry.data["auth_implementation"],
"token": mock_config_entry.data["token"],
},
version=2,
)
await setup_integration(hass, old_config_entry)
assert old_config_entry.state is ConfigEntryState.MIGRATION_ERROR
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment