Migrate OneDrive to onedrive_personal_sdk library (#137064)

This commit is contained in:
Josef Zweck
2025-02-03 16:25:58 +01:00
committed by GitHub
parent 05ca80f4ba
commit 628e1ffb84
13 changed files with 307 additions and 724 deletions

View File

@ -5,34 +5,33 @@ from __future__ import annotations
from dataclasses import dataclass
import logging
from kiota_abstractions.api_error import APIError
from kiota_abstractions.authentication import BaseBearerTokenAuthenticationProvider
from msgraph import GraphRequestAdapter, GraphServiceClient
from msgraph.generated.drives.item.items.items_request_builder import (
ItemsRequestBuilder,
from onedrive_personal_sdk import OneDriveClient
from onedrive_personal_sdk.exceptions import (
AuthenticationError,
HttpRequestException,
OneDriveException,
)
from msgraph.generated.models.drive_item import DriveItem
from msgraph.generated.models.folder import Folder
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import (
OAuth2Session,
async_get_config_entry_implementation,
)
from homeassistant.helpers.httpx_client import create_async_httpx_client
from homeassistant.helpers.instance_id import async_get as async_get_instance_id
from .api import OneDriveConfigEntryAccessTokenProvider
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN, OAUTH_SCOPES
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
@dataclass
class OneDriveRuntimeData:
"""Runtime data for the OneDrive integration."""
items: ItemsRequestBuilder
client: OneDriveClient
token_provider: OneDriveConfigEntryAccessTokenProvider
backup_folder_id: str
@ -47,29 +46,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
session = OAuth2Session(hass, entry, implementation)
auth_provider = BaseBearerTokenAuthenticationProvider(
access_token_provider=OneDriveConfigEntryAccessTokenProvider(session)
)
adapter = GraphRequestAdapter(
auth_provider=auth_provider,
client=create_async_httpx_client(hass, follow_redirects=True),
)
token_provider = OneDriveConfigEntryAccessTokenProvider(session)
graph_client = GraphServiceClient(
request_adapter=adapter,
scopes=OAUTH_SCOPES,
)
assert entry.unique_id
drive_item = graph_client.drives.by_drive_id(entry.unique_id)
client = OneDriveClient(token_provider, async_get_clientsession(hass))
# get approot, will be created automatically if it does not exist
try:
approot = await drive_item.special.by_drive_item_id("approot").get()
except APIError as err:
if err.response_status_code == 403:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="authentication_failed"
) from err
approot = await client.get_approot()
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,
@ -77,24 +65,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: OneDriveConfigEntry) ->
translation_placeholders={"folder": "approot"},
) from err
if approot is None or not approot.id:
_LOGGER.debug("Failed to get approot, was None")
instance_id = await async_get_instance_id(hass)
backup_folder_name = f"backups_{instance_id[:8]}"
try:
backup_folder = await client.create_folder(
parent_id=approot.id, 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": "approot"},
)
instance_id = await async_get_instance_id(hass)
backup_folder_id = await _async_create_folder_if_not_exists(
items=drive_item.items,
base_folder_id=approot.id,
folder=f"backups_{instance_id[:8]}",
)
translation_placeholders={"folder": backup_folder_name},
) from err
entry.runtime_data = OneDriveRuntimeData(
items=drive_item.items,
backup_folder_id=backup_folder_id,
client=client,
token_provider=token_provider,
backup_folder_id=backup_folder.id,
)
_async_notify_backup_listeners_soon(hass)
@ -116,54 +104,3 @@ def _async_notify_backup_listeners(hass: HomeAssistant) -> None:
@callback
def _async_notify_backup_listeners_soon(hass: HomeAssistant) -> None:
hass.loop.call_soon(_async_notify_backup_listeners, hass)
async def _async_create_folder_if_not_exists(
items: ItemsRequestBuilder,
base_folder_id: str,
folder: str,
) -> str:
"""Check if a folder exists and create it if it does not exist."""
folder_item: DriveItem | None = None
try:
folder_item = await items.by_drive_item_id(f"{base_folder_id}:/{folder}:").get()
except APIError as err:
if err.response_status_code != 404:
_LOGGER.debug("Failed to get folder %s", folder, exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_get_folder",
translation_placeholders={"folder": folder},
) from err
# is 404 not found, create folder
_LOGGER.debug("Creating folder %s", folder)
request_body = DriveItem(
name=folder,
folder=Folder(),
additional_data={
"@microsoft_graph_conflict_behavior": "fail",
},
)
try:
folder_item = await items.by_drive_item_id(base_folder_id).children.post(
request_body
)
except APIError as create_err:
_LOGGER.debug("Failed to create folder %s", folder, exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_create_folder",
translation_placeholders={"folder": folder},
) from create_err
_LOGGER.debug("Created folder %s", folder)
else:
_LOGGER.debug("Found folder %s", folder)
if folder_item is None or not folder_item.id:
_LOGGER.debug("Failed to get folder %s, was None", folder)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="failed_to_get_folder",
translation_placeholders={"folder": folder},
)
return folder_item.id

View File

@ -1,28 +1,14 @@
"""API for OneDrive bound to Home Assistant OAuth."""
from typing import Any, cast
from typing import cast
from kiota_abstractions.authentication import AccessTokenProvider, AllowedHostsValidator
from onedrive_personal_sdk import TokenProvider
from homeassistant.const import CONF_ACCESS_TOKEN
from homeassistant.helpers import config_entry_oauth2_flow
class OneDriveAccessTokenProvider(AccessTokenProvider):
"""Provide OneDrive authentication tied to an OAuth2 based config entry."""
def __init__(self) -> None:
"""Initialize OneDrive auth."""
super().__init__()
# currently allowing all hosts
self._allowed_hosts_validator = AllowedHostsValidator(allowed_hosts=[])
def get_allowed_hosts_validator(self) -> AllowedHostsValidator:
"""Retrieve the allowed hosts validator."""
return self._allowed_hosts_validator
class OneDriveConfigFlowAccessTokenProvider(OneDriveAccessTokenProvider):
class OneDriveConfigFlowAccessTokenProvider(TokenProvider):
"""Provide OneDrive authentication tied to an OAuth2 based config entry."""
def __init__(self, token: str) -> None:
@ -30,14 +16,12 @@ class OneDriveConfigFlowAccessTokenProvider(OneDriveAccessTokenProvider):
super().__init__()
self._token = token
async def get_authorization_token( # pylint: disable=dangerous-default-value
self, uri: str, additional_authentication_context: dict[str, Any] = {}
) -> str:
"""Return a valid authorization token."""
def async_get_access_token(self) -> str:
"""Return a valid access token."""
return self._token
class OneDriveConfigEntryAccessTokenProvider(OneDriveAccessTokenProvider):
class OneDriveConfigEntryAccessTokenProvider(TokenProvider):
"""Provide OneDrive authentication tied to an OAuth2 based config entry."""
def __init__(self, oauth_session: config_entry_oauth2_flow.OAuth2Session) -> None:
@ -45,9 +29,6 @@ class OneDriveConfigEntryAccessTokenProvider(OneDriveAccessTokenProvider):
super().__init__()
self._oauth_session = oauth_session
async def get_authorization_token( # pylint: disable=dangerous-default-value
self, uri: str, additional_authentication_context: dict[str, Any] = {}
) -> str:
"""Return a valid authorization token."""
await self._oauth_session.async_ensure_token_valid()
def async_get_access_token(self) -> str:
"""Return a valid access token."""
return cast(str, self._oauth_session.token[CONF_ACCESS_TOKEN])

View File

@ -2,37 +2,22 @@
from __future__ import annotations
import asyncio
from collections.abc import AsyncIterator, Callable, Coroutine
from functools import wraps
import html
import json
import logging
from typing import Any, Concatenate, cast
from typing import Any, Concatenate
from httpx import Response, TimeoutException
from kiota_abstractions.api_error import APIError
from kiota_abstractions.authentication import AnonymousAuthenticationProvider
from kiota_abstractions.headers_collection import HeadersCollection
from kiota_abstractions.method import Method
from kiota_abstractions.native_response_handler import NativeResponseHandler
from kiota_abstractions.request_information import RequestInformation
from kiota_http.middleware.options import ResponseHandlerOption
from msgraph import GraphRequestAdapter
from msgraph.generated.drives.item.items.item.content.content_request_builder import (
ContentRequestBuilder,
from aiohttp import ClientTimeout
from onedrive_personal_sdk.clients.large_file_upload import LargeFileUploadClient
from onedrive_personal_sdk.exceptions import (
AuthenticationError,
HashMismatchError,
OneDriveException,
)
from msgraph.generated.drives.item.items.item.create_upload_session.create_upload_session_post_request_body import (
CreateUploadSessionPostRequestBody,
)
from msgraph.generated.drives.item.items.item.drive_item_item_request_builder import (
DriveItemItemRequestBuilder,
)
from msgraph.generated.models.drive_item import DriveItem
from msgraph.generated.models.drive_item_uploadable_properties import (
DriveItemUploadableProperties,
)
from msgraph_core.models import LargeFileUploadSession
from onedrive_personal_sdk.models.items import File, Folder, ItemUpdate
from onedrive_personal_sdk.models.upload import FileInfo
from homeassistant.components.backup import (
AgentBackup,
@ -41,14 +26,14 @@ from homeassistant.components.backup import (
suggested_filename,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.httpx_client import get_async_client
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from . import OneDriveConfigEntry
from .const import DATA_BACKUP_AGENT_LISTENERS, DOMAIN
_LOGGER = logging.getLogger(__name__)
UPLOAD_CHUNK_SIZE = 16 * 320 * 1024 # 5.2MB
MAX_RETRIES = 5
TIMEOUT = ClientTimeout(connect=10, total=43200) # 12 hours
async def async_get_backup_agents(
@ -92,18 +77,18 @@ def handle_backup_errors[_R, **P](
) -> _R:
try:
return await func(self, *args, **kwargs)
except APIError as err:
if err.response_status_code == 403:
self._entry.async_start_reauth(self._hass)
except AuthenticationError as err:
self._entry.async_start_reauth(self._hass)
raise BackupAgentError("Authentication error") from err
except OneDriveException as err:
_LOGGER.error(
"Error during backup in %s: Status %s, message %s",
"Error during backup in %s:, message %s",
func.__name__,
err.response_status_code,
err.message,
err,
)
_LOGGER.debug("Full error: %s", err, exc_info=True)
raise BackupAgentError("Backup operation failed") from err
except TimeoutException as err:
except TimeoutError as err:
_LOGGER.error(
"Error during backup in %s: Timeout",
func.__name__,
@ -123,7 +108,8 @@ class OneDriveBackupAgent(BackupAgent):
super().__init__()
self._hass = hass
self._entry = entry
self._items = entry.runtime_data.items
self._client = entry.runtime_data.client
self._token_provider = entry.runtime_data.token_provider
self._folder_id = entry.runtime_data.backup_folder_id
self.name = entry.title
assert entry.unique_id
@ -134,24 +120,12 @@ class OneDriveBackupAgent(BackupAgent):
self, backup_id: str, **kwargs: Any
) -> AsyncIterator[bytes]:
"""Download a backup file."""
# this forces the query to return a raw httpx response, but breaks typing
backup = await self._find_item_by_backup_id(backup_id)
if backup is None or backup.id is None:
item = await self._find_item_by_backup_id(backup_id)
if item is None:
raise BackupAgentError("Backup not found")
request_config = (
ContentRequestBuilder.ContentRequestBuilderGetRequestConfiguration(
options=[ResponseHandlerOption(NativeResponseHandler())],
)
)
response = cast(
Response,
await self._items.by_drive_item_id(backup.id).content.get(
request_configuration=request_config
),
)
return response.aiter_bytes(chunk_size=1024)
stream = await self._client.download_drive_item(item.id, timeout=TIMEOUT)
return stream.iter_chunked(1024)
@handle_backup_errors
async def async_upload_backup(
@ -163,27 +137,20 @@ class OneDriveBackupAgent(BackupAgent):
) -> None:
"""Upload a backup."""
# upload file in chunks to support large files
upload_session_request_body = CreateUploadSessionPostRequestBody(
item=DriveItemUploadableProperties(
additional_data={
"@microsoft.graph.conflictBehavior": "fail",
},
file = FileInfo(
suggested_filename(backup),
backup.size,
self._folder_id,
await open_stream(),
)
try:
item = await LargeFileUploadClient.upload(
self._token_provider, file, session=async_get_clientsession(self._hass)
)
)
file_item = self._get_backup_file_item(suggested_filename(backup))
upload_session = await file_item.create_upload_session.post(
upload_session_request_body
)
if upload_session is None or upload_session.upload_url is None:
except HashMismatchError as err:
raise BackupAgentError(
translation_domain=DOMAIN, translation_key="backup_no_upload_session"
)
await self._upload_file(
upload_session.upload_url, await open_stream(), backup.size
)
"Hash validation failed, backup file might be corrupt"
) from err
# store metadata in description
backup_dict = backup.as_dict()
@ -191,7 +158,10 @@ class OneDriveBackupAgent(BackupAgent):
description = json.dumps(backup_dict)
_LOGGER.debug("Creating metadata: %s", description)
await file_item.patch(DriveItem(description=description))
await self._client.update_drive_item(
path_or_id=item.id,
data=ItemUpdate(description=description),
)
@handle_backup_errors
async def async_delete_backup(
@ -200,35 +170,31 @@ class OneDriveBackupAgent(BackupAgent):
**kwargs: Any,
) -> None:
"""Delete a backup file."""
backup = await self._find_item_by_backup_id(backup_id)
if backup is None or backup.id is None:
item = await self._find_item_by_backup_id(backup_id)
if item is None:
return
await self._items.by_drive_item_id(backup.id).delete()
await self._client.delete_drive_item(item.id)
@handle_backup_errors
async def async_list_backups(self, **kwargs: Any) -> list[AgentBackup]:
"""List backups."""
backups: list[AgentBackup] = []
items = await self._items.by_drive_item_id(f"{self._folder_id}").children.get()
if items and (values := items.value):
for item in values:
if (description := item.description) is None:
continue
if "homeassistant_version" in description:
backups.append(self._backup_from_description(description))
return backups
return [
self._backup_from_description(item.description)
for item in await self._client.list_drive_items(self._folder_id)
if item.description and "homeassistant_version" in item.description
]
@handle_backup_errors
async def async_get_backup(
self, backup_id: str, **kwargs: Any
) -> AgentBackup | None:
"""Return a backup."""
backup = await self._find_item_by_backup_id(backup_id)
if backup is None:
return None
assert backup.description # already checked in _find_item_by_backup_id
return self._backup_from_description(backup.description)
item = await self._find_item_by_backup_id(backup_id)
return (
self._backup_from_description(item.description)
if item and item.description
else None
)
def _backup_from_description(self, description: str) -> AgentBackup:
"""Create a backup object from a description."""
@ -237,91 +203,13 @@ class OneDriveBackupAgent(BackupAgent):
) # OneDrive encodes the description on save automatically
return AgentBackup.from_dict(json.loads(description))
async def _find_item_by_backup_id(self, backup_id: str) -> DriveItem | None:
"""Find a backup item by its backup ID."""
items = await self._items.by_drive_item_id(f"{self._folder_id}").children.get()
if items and (values := items.value):
for item in values:
if (description := item.description) is None:
continue
if backup_id in description:
return item
return None
def _get_backup_file_item(self, backup_id: str) -> DriveItemItemRequestBuilder:
return self._items.by_drive_item_id(f"{self._folder_id}:/{backup_id}:")
async def _upload_file(
self, upload_url: str, stream: AsyncIterator[bytes], total_size: int
) -> None:
"""Use custom large file upload; SDK does not support stream."""
adapter = GraphRequestAdapter(
auth_provider=AnonymousAuthenticationProvider(),
client=get_async_client(self._hass),
async def _find_item_by_backup_id(self, backup_id: str) -> File | Folder | None:
"""Find an item by backup ID."""
return next(
(
item
for item in await self._client.list_drive_items(self._folder_id)
if item.description and backup_id in item.description
),
None,
)
async def async_upload(
start: int, end: int, chunk_data: bytes
) -> LargeFileUploadSession:
info = RequestInformation()
info.url = upload_url
info.http_method = Method.PUT
info.headers = HeadersCollection()
info.headers.try_add("Content-Range", f"bytes {start}-{end}/{total_size}")
info.headers.try_add("Content-Length", str(len(chunk_data)))
info.headers.try_add("Content-Type", "application/octet-stream")
_LOGGER.debug(info.headers.get_all())
info.set_stream_content(chunk_data)
result = await adapter.send_async(info, LargeFileUploadSession, {})
_LOGGER.debug("Next expected range: %s", result.next_expected_ranges)
return result
start = 0
buffer: list[bytes] = []
buffer_size = 0
retries = 0
async for chunk in stream:
buffer.append(chunk)
buffer_size += len(chunk)
if buffer_size >= UPLOAD_CHUNK_SIZE:
chunk_data = b"".join(buffer)
uploaded_chunks = 0
while (
buffer_size > UPLOAD_CHUNK_SIZE
): # Loop in case the buffer is >= UPLOAD_CHUNK_SIZE * 2
slice_start = uploaded_chunks * UPLOAD_CHUNK_SIZE
try:
await async_upload(
start,
start + UPLOAD_CHUNK_SIZE - 1,
chunk_data[slice_start : slice_start + UPLOAD_CHUNK_SIZE],
)
except APIError as err:
if (
err.response_status_code and err.response_status_code < 500
): # no retry on 4xx errors
raise
if retries < MAX_RETRIES:
await asyncio.sleep(2**retries)
retries += 1
continue
raise
except TimeoutException:
if retries < MAX_RETRIES:
retries += 1
continue
raise
retries = 0
start += UPLOAD_CHUNK_SIZE
uploaded_chunks += 1
buffer_size -= UPLOAD_CHUNK_SIZE
buffer = [chunk_data[UPLOAD_CHUNK_SIZE * uploaded_chunks :]]
# upload the remaining bytes
if buffer:
_LOGGER.debug("Last chunk")
chunk_data = b"".join(buffer)
await async_upload(start, start + len(chunk_data) - 1, chunk_data)

View File

@ -4,16 +4,13 @@ from collections.abc import Mapping
import logging
from typing import Any, cast
from kiota_abstractions.api_error import APIError
from kiota_abstractions.authentication import BaseBearerTokenAuthenticationProvider
from kiota_abstractions.method import Method
from kiota_abstractions.request_information import RequestInformation
from msgraph import GraphRequestAdapter, GraphServiceClient
from onedrive_personal_sdk.clients.client import OneDriveClient
from onedrive_personal_sdk.exceptions import OneDriveException
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlowResult
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_TOKEN
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.config_entry_oauth2_flow import AbstractOAuth2FlowHandler
from homeassistant.helpers.httpx_client import get_async_client
from .api import OneDriveConfigFlowAccessTokenProvider
from .const import DOMAIN, OAUTH_SCOPES
@ -39,48 +36,24 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
data: dict[str, Any],
) -> ConfigFlowResult:
"""Handle the initial step."""
auth_provider = BaseBearerTokenAuthenticationProvider(
access_token_provider=OneDriveConfigFlowAccessTokenProvider(
cast(str, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
)
)
adapter = GraphRequestAdapter(
auth_provider=auth_provider,
client=get_async_client(self.hass),
token_provider = OneDriveConfigFlowAccessTokenProvider(
cast(str, data[CONF_TOKEN][CONF_ACCESS_TOKEN])
)
graph_client = GraphServiceClient(
request_adapter=adapter,
scopes=OAUTH_SCOPES,
graph_client = OneDriveClient(
token_provider, async_get_clientsession(self.hass)
)
# need to get adapter from client, as client changes it
request_adapter = cast(GraphRequestAdapter, graph_client.request_adapter)
request_info = RequestInformation(
method=Method.GET,
url_template="{+baseurl}/me/drive/special/approot",
path_parameters={},
)
parent_span = request_adapter.start_tracing_span(request_info, "get_approot")
# get the OneDrive id
# use low level methods, to avoid files.read permissions
# which would be required by drives.me.get()
try:
response = await request_adapter.get_http_response_message(
request_info=request_info, parent_span=parent_span
)
except APIError:
approot = await graph_client.get_approot()
except OneDriveException:
self.logger.exception("Failed to connect to OneDrive")
return self.async_abort(reason="connection_error")
except Exception:
self.logger.exception("Unknown error")
return self.async_abort(reason="unknown")
drive: dict = response.json()
await self.async_set_unique_id(drive["parentReference"]["driveId"])
await self.async_set_unique_id(approot.parent_reference.drive_id)
if self.source == SOURCE_REAUTH:
reauth_entry = self._get_reauth_entry()
@ -94,10 +67,11 @@ class OneDriveConfigFlow(AbstractOAuth2FlowHandler, domain=DOMAIN):
self._abort_if_unique_id_configured()
user = drive.get("createdBy", {}).get("user", {}).get("displayName")
title = f"{user}'s OneDrive" if user else "OneDrive"
title = (
f"{approot.created_by.user.display_name}'s OneDrive"
if approot.created_by.user and approot.created_by.user.display_name
else "OneDrive"
)
return self.async_create_entry(title=title, data=data)
async def async_step_reauth(

View File

@ -7,7 +7,7 @@
"documentation": "https://www.home-assistant.io/integrations/onedrive",
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["msgraph", "msgraph-core", "kiota"],
"loggers": ["onedrive_personal_sdk"],
"quality_scale": "bronze",
"requirements": ["msgraph-sdk==1.16.0"]
"requirements": ["onedrive-personal-sdk==0.0.1"]
}

View File

@ -23,31 +23,18 @@
"connection_error": "Failed to connect to OneDrive.",
"wrong_drive": "New account does not contain previously configured OneDrive.",
"unknown": "[%key:common::config_flow::error::unknown%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"failed_to_create_folder": "Failed to create backup folder"
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"create_entry": {
"default": "[%key:common::config_flow::create_entry::authenticated%]"
}
},
"exceptions": {
"backup_not_found": {
"message": "Backup not found"
},
"backup_no_content": {
"message": "Backup has no content"
},
"backup_no_upload_session": {
"message": "Failed to start backup upload"
},
"authentication_failed": {
"message": "Authentication failed"
},
"failed_to_get_folder": {
"message": "Failed to get {folder} folder"
},
"failed_to_create_folder": {
"message": "Failed to create {folder} folder"
}
}
}

6
requirements_all.txt generated
View File

@ -1434,9 +1434,6 @@ motioneye-client==0.3.14
# homeassistant.components.bang_olufsen
mozart-api==4.1.1.116.4
# homeassistant.components.onedrive
msgraph-sdk==1.16.0
# homeassistant.components.mullvad
mullvad-api==1.0.0
@ -1558,6 +1555,9 @@ omnilogic==0.4.5
# homeassistant.components.ondilo_ico
ondilo==0.5.0
# homeassistant.components.onedrive
onedrive-personal-sdk==0.0.1
# homeassistant.components.onvif
onvif-zeep-async==3.2.5

View File

@ -1206,9 +1206,6 @@ motioneye-client==0.3.14
# homeassistant.components.bang_olufsen
mozart-api==4.1.1.116.4
# homeassistant.components.onedrive
msgraph-sdk==1.16.0
# homeassistant.components.mullvad
mullvad-api==1.0.0
@ -1306,6 +1303,9 @@ omnilogic==0.4.5
# homeassistant.components.ondilo_ico
ondilo==0.5.0
# homeassistant.components.onedrive
onedrive-personal-sdk==0.0.1
# homeassistant.components.onvif
onvif-zeep-async==3.2.5

View File

@ -1,18 +1,9 @@
"""Fixtures for OneDrive tests."""
from collections.abc import AsyncIterator, Generator
from html import escape
from json import dumps
import time
from unittest.mock import AsyncMock, MagicMock, patch
from httpx import Response
from msgraph.generated.models.drive_item import DriveItem
from msgraph.generated.models.drive_item_collection_response import (
DriveItemCollectionResponse,
)
from msgraph.generated.models.upload_session import UploadSession
from msgraph_core.models import LargeFileUploadSession
import pytest
from homeassistant.components.application_credentials import (
@ -23,7 +14,13 @@ from homeassistant.components.onedrive.const import DOMAIN, OAUTH_SCOPES
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import BACKUP_METADATA, CLIENT_ID, CLIENT_SECRET
from .const import (
CLIENT_ID,
CLIENT_SECRET,
MOCK_APPROOT,
MOCK_BACKUP_FILE,
MOCK_BACKUP_FOLDER,
)
from tests.common import MockConfigEntry
@ -70,96 +67,41 @@ def mock_config_entry(expires_at: int, scopes: list[str]) -> MockConfigEntry:
)
@pytest.fixture
def mock_adapter() -> Generator[MagicMock]:
"""Return a mocked GraphAdapter."""
with (
patch(
"homeassistant.components.onedrive.config_flow.GraphRequestAdapter",
autospec=True,
) as mock_adapter,
patch(
"homeassistant.components.onedrive.backup.GraphRequestAdapter",
new=mock_adapter,
),
):
adapter = mock_adapter.return_value
adapter.get_http_response_message.return_value = Response(
status_code=200,
json={
"parentReference": {"driveId": "mock_drive_id"},
"createdBy": {"user": {"displayName": "John Doe"}},
},
)
yield adapter
adapter.send_async.return_value = LargeFileUploadSession(
next_expected_ranges=["2-"]
)
@pytest.fixture(autouse=True)
def mock_graph_client(mock_adapter: MagicMock) -> Generator[MagicMock]:
def mock_onedrive_client() -> Generator[MagicMock]:
"""Return a mocked GraphServiceClient."""
with (
patch(
"homeassistant.components.onedrive.config_flow.GraphServiceClient",
"homeassistant.components.onedrive.config_flow.OneDriveClient",
autospec=True,
) as graph_client,
) as onedrive_client,
patch(
"homeassistant.components.onedrive.GraphServiceClient",
new=graph_client,
"homeassistant.components.onedrive.OneDriveClient",
new=onedrive_client,
),
):
client = graph_client.return_value
client = onedrive_client.return_value
client.get_approot.return_value = MOCK_APPROOT
client.create_folder.return_value = MOCK_BACKUP_FOLDER
client.list_drive_items.return_value = [MOCK_BACKUP_FILE]
client.get_drive_item.return_value = MOCK_BACKUP_FILE
client.request_adapter = mock_adapter
class MockStreamReader:
async def iter_chunked(self, chunk_size: int) -> AsyncIterator[bytes]:
yield b"backup data"
drives = client.drives.by_drive_id.return_value
drives.special.by_drive_item_id.return_value.get = AsyncMock(
return_value=DriveItem(id="approot")
)
drive_items = drives.items.by_drive_item_id.return_value
drive_items.get = AsyncMock(return_value=DriveItem(id="folder_id"))
drive_items.children.post = AsyncMock(return_value=DriveItem(id="folder_id"))
drive_items.children.get = AsyncMock(
return_value=DriveItemCollectionResponse(
value=[
DriveItem(
id=BACKUP_METADATA["backup_id"],
description=escape(dumps(BACKUP_METADATA)),
),
DriveItem(),
]
)
)
drive_items.delete = AsyncMock(return_value=None)
drive_items.create_upload_session.post = AsyncMock(
return_value=UploadSession(upload_url="https://test.tld")
)
drive_items.patch = AsyncMock(return_value=None)
async def generate_bytes() -> AsyncIterator[bytes]:
"""Asynchronous generator that yields bytes."""
yield b"backup data"
drive_items.content.get = AsyncMock(
return_value=Response(status_code=200, content=generate_bytes())
)
client.download_drive_item.return_value = MockStreamReader()
yield client
@pytest.fixture
def mock_drive_items(mock_graph_client: MagicMock) -> MagicMock:
"""Return a mocked DriveItems."""
return mock_graph_client.drives.by_drive_id.return_value.items.by_drive_item_id.return_value
@pytest.fixture
def mock_get_special_folder(mock_graph_client: MagicMock) -> MagicMock:
"""Mock the get special folder method."""
return mock_graph_client.drives.by_drive_id.return_value.special.by_drive_item_id.return_value.get
def mock_large_file_upload_client() -> Generator[AsyncMock]:
"""Return a mocked LargeFileUploadClient upload."""
with patch(
"homeassistant.components.onedrive.backup.LargeFileUploadClient.upload"
) as mock_upload:
yield mock_upload
@pytest.fixture
@ -179,10 +121,3 @@ def mock_instance_id() -> Generator[AsyncMock]:
return_value="9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0",
):
yield
@pytest.fixture(autouse=True)
def mock_asyncio_sleep() -> Generator[AsyncMock]:
"""Mock asyncio.sleep."""
with patch("homeassistant.components.onedrive.backup.asyncio.sleep", AsyncMock()):
yield

View File

@ -1,5 +1,18 @@
"""Consts for OneDrive tests."""
from html import escape
from json import dumps
from onedrive_personal_sdk.models.items import (
AppRoot,
Contributor,
File,
Folder,
Hashes,
ItemParentReference,
User,
)
CLIENT_ID = "1234"
CLIENT_SECRET = "5678"
@ -17,3 +30,48 @@ BACKUP_METADATA = {
"protected": False,
"size": 34519040,
}
CONTRIBUTOR = Contributor(
user=User(
display_name="John Doe",
id="id",
email="john@doe.com",
)
)
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=CONTRIBUTOR,
)
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=CONTRIBUTOR,
)
MOCK_BACKUP_FILE = File(
id="id",
name="23e64aec.tar",
size=34519040,
parent_reference=ItemParentReference(
drive_id="mock_drive_id", id="id", path="path"
),
hashes=Hashes(
quick_xor_hash="hash",
),
mime_type="application/x-tar",
description=escape(dumps(BACKUP_METADATA)),
created_by=CONTRIBUTOR,
)

View File

@ -3,15 +3,14 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from html import escape
from io import StringIO
from json import dumps
from unittest.mock import Mock, patch
from httpx import TimeoutException
from kiota_abstractions.api_error import APIError
from msgraph.generated.models.drive_item import DriveItem
from msgraph_core.models import LargeFileUploadSession
from onedrive_personal_sdk.exceptions import (
AuthenticationError,
HashMismatchError,
OneDriveException,
)
import pytest
from homeassistant.components.backup import DOMAIN as BACKUP_DOMAIN, AgentBackup
@ -102,14 +101,10 @@ async def test_agents_list_backups(
async def test_agents_get_backup(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test agent get backup."""
mock_drive_items.get = AsyncMock(
return_value=DriveItem(description=escape(dumps(BACKUP_METADATA)))
)
backup_id = BACKUP_METADATA["backup_id"]
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id})
@ -140,7 +135,7 @@ async def test_agents_get_backup(
async def test_agents_delete(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
) -> None:
"""Test agent delete backup."""
client = await hass_ws_client(hass)
@ -155,37 +150,15 @@ async def test_agents_delete(
assert response["success"]
assert response["result"] == {"agent_errors": {}}
mock_drive_items.delete.assert_called_once()
async def test_agents_delete_not_found_does_not_throw(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
) -> None:
"""Test agent delete backup."""
mock_drive_items.children.get = AsyncMock(return_value=[])
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "backup/delete",
"backup_id": BACKUP_METADATA["backup_id"],
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"agent_errors": {}}
assert mock_drive_items.delete.call_count == 0
mock_onedrive_client.delete_drive_item.assert_called_once()
async def test_agents_upload(
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
mock_large_file_upload_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_adapter: MagicMock,
) -> None:
"""Test agent upload backup."""
client = await hass_client()
@ -200,7 +173,6 @@ async def test_agents_upload(
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
patch("homeassistant.components.onedrive.backup.UPLOAD_CHUNK_SIZE", 3),
):
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
fetch_backup.return_value = test_backup
@ -211,31 +183,22 @@ async def test_agents_upload(
assert resp.status == 201
assert f"Uploading backup {test_backup.backup_id}" in caplog.text
mock_drive_items.create_upload_session.post.assert_called_once()
mock_drive_items.patch.assert_called_once()
assert mock_adapter.send_async.call_count == 2
assert mock_adapter.method_calls[0].args[0].content == b"tes"
assert mock_adapter.method_calls[0].args[0].headers.get("Content-Range") == {
"bytes 0-2/34519040"
}
assert mock_adapter.method_calls[1].args[0].content == b"t"
assert mock_adapter.method_calls[1].args[0].headers.get("Content-Range") == {
"bytes 3-3/34519040"
}
mock_large_file_upload_client.assert_called_once()
mock_onedrive_client.update_drive_item.assert_called_once()
async def test_broken_upload_session(
async def test_agents_upload_corrupt_upload(
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
mock_large_file_upload_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test broken upload session."""
"""Test hash validation fails."""
mock_large_file_upload_client.side_effect = HashMismatchError("test")
client = await hass_client()
test_backup = AgentBackup.from_dict(BACKUP_METADATA)
mock_drive_items.create_upload_session.post = AsyncMock(return_value=None)
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
@ -254,152 +217,18 @@ async def test_broken_upload_session(
)
assert resp.status == 201
assert "Failed to start backup upload" in caplog.text
@pytest.mark.parametrize(
"side_effect",
[
APIError(response_status_code=500),
TimeoutException("Timeout"),
],
)
async def test_agents_upload_errors_retried(
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
mock_drive_items: MagicMock,
mock_config_entry: MockConfigEntry,
mock_adapter: MagicMock,
side_effect: Exception,
) -> None:
"""Test agent upload backup."""
client = await hass_client()
test_backup = AgentBackup.from_dict(BACKUP_METADATA)
mock_adapter.send_async.side_effect = [
side_effect,
LargeFileUploadSession(next_expected_ranges=["2-"]),
LargeFileUploadSession(next_expected_ranges=["2-"]),
]
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
patch("homeassistant.components.onedrive.backup.UPLOAD_CHUNK_SIZE", 3),
):
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
fetch_backup.return_value = test_backup
resp = await client.post(
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.unique_id}",
data={"file": StringIO("test")},
)
assert resp.status == 201
assert mock_adapter.send_async.call_count == 3
assert f"Uploading backup {test_backup.backup_id}" in caplog.text
mock_drive_items.patch.assert_called_once()
async def test_agents_upload_4xx_errors_not_retried(
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
mock_drive_items: MagicMock,
mock_config_entry: MockConfigEntry,
mock_adapter: MagicMock,
) -> None:
"""Test agent upload backup."""
client = await hass_client()
test_backup = AgentBackup.from_dict(BACKUP_METADATA)
mock_adapter.send_async.side_effect = APIError(response_status_code=404)
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
patch("homeassistant.components.onedrive.backup.UPLOAD_CHUNK_SIZE", 3),
):
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
fetch_backup.return_value = test_backup
resp = await client.post(
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.unique_id}",
data={"file": StringIO("test")},
)
assert resp.status == 201
assert mock_adapter.send_async.call_count == 1
assert f"Uploading backup {test_backup.backup_id}" in caplog.text
assert mock_drive_items.patch.call_count == 0
assert "Backup operation failed" in caplog.text
@pytest.mark.parametrize(
("side_effect", "error"),
[
(APIError(response_status_code=500), "Backup operation failed"),
(TimeoutException("Timeout"), "Backup operation timed out"),
],
)
async def test_agents_upload_fails_after_max_retries(
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
mock_drive_items: MagicMock,
mock_config_entry: MockConfigEntry,
mock_adapter: MagicMock,
side_effect: Exception,
error: str,
) -> None:
"""Test agent upload backup."""
client = await hass_client()
test_backup = AgentBackup.from_dict(BACKUP_METADATA)
mock_adapter.send_async.side_effect = side_effect
with (
patch(
"homeassistant.components.backup.manager.BackupManager.async_get_backup",
) as fetch_backup,
patch(
"homeassistant.components.backup.manager.read_backup",
return_value=test_backup,
),
patch("pathlib.Path.open") as mocked_open,
patch("homeassistant.components.onedrive.backup.UPLOAD_CHUNK_SIZE", 3),
):
mocked_open.return_value.read = Mock(side_effect=[b"test", b""])
fetch_backup.return_value = test_backup
resp = await client.post(
f"/api/backup/upload?agent_id={DOMAIN}.{mock_config_entry.unique_id}",
data={"file": StringIO("test")},
)
assert resp.status == 201
assert mock_adapter.send_async.call_count == 6
assert f"Uploading backup {test_backup.backup_id}" in caplog.text
assert mock_drive_items.patch.call_count == 0
assert error in caplog.text
mock_large_file_upload_client.assert_called_once()
assert mock_onedrive_client.update_drive_item.call_count == 0
assert "Hash validation failed, backup file might be corrupt" in caplog.text
async def test_agents_download(
hass_client: ClientSessionGenerator,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test agent download backup."""
mock_drive_items.get = AsyncMock(
return_value=DriveItem(description=escape(dumps(BACKUP_METADATA)))
)
client = await hass_client()
backup_id = BACKUP_METADATA["backup_id"]
@ -408,29 +237,30 @@ async def test_agents_download(
)
assert resp.status == 200
assert await resp.content.read() == b"backup data"
mock_drive_items.content.get.assert_called_once()
@pytest.mark.parametrize(
("side_effect", "error"),
[
(
APIError(response_status_code=500),
OneDriveException(),
"Backup operation failed",
),
(TimeoutException("Timeout"), "Backup operation timed out"),
(TimeoutError(), "Backup operation timed out"),
],
)
async def test_delete_error(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
side_effect: Exception,
error: str,
) -> None:
"""Test error during delete."""
mock_drive_items.delete = AsyncMock(side_effect=side_effect)
mock_onedrive_client.delete_drive_item.side_effect = AsyncMock(
side_effect=side_effect
)
client = await hass_ws_client(hass)
@ -448,14 +278,35 @@ async def test_delete_error(
}
async def test_agents_delete_not_found_does_not_throw(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_onedrive_client: MagicMock,
) -> None:
"""Test agent delete backup."""
mock_onedrive_client.list_drive_items.return_value = []
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "backup/delete",
"backup_id": BACKUP_METADATA["backup_id"],
}
)
response = await client.receive_json()
assert response["success"]
assert response["result"] == {"agent_errors": {}}
async def test_agents_backup_not_found(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
) -> None:
"""Test backup not found."""
mock_drive_items.children.get = AsyncMock(return_value=[])
mock_onedrive_client.list_drive_items.return_value = []
backup_id = BACKUP_METADATA["backup_id"]
client = await hass_ws_client(hass)
await client.send_json_auto_id({"type": "backup/details", "backup_id": backup_id})
@ -468,13 +319,13 @@ async def test_agents_backup_not_found(
async def test_reauth_on_403(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
mock_drive_items: MagicMock,
mock_onedrive_client: MagicMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test we re-authenticate on 403."""
mock_drive_items.children.get = AsyncMock(
side_effect=APIError(response_status_code=403)
mock_onedrive_client.list_drive_items.side_effect = AuthenticationError(
403, "Auth failed"
)
backup_id = BACKUP_METADATA["backup_id"]
client = await hass_ws_client(hass)
@ -483,7 +334,7 @@ async def test_reauth_on_403(
assert response["success"]
assert response["result"]["agent_errors"] == {
f"{DOMAIN}.{mock_config_entry.unique_id}": "Backup operation failed"
f"{DOMAIN}.{mock_config_entry.unique_id}": "Authentication error"
}
await hass.async_block_till_done()

View File

@ -3,8 +3,7 @@
from http import HTTPStatus
from unittest.mock import AsyncMock, MagicMock
from httpx import Response
from kiota_abstractions.api_error import APIError
from onedrive_personal_sdk.exceptions import OneDriveException
import pytest
from homeassistant import config_entries
@ -20,7 +19,7 @@ from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers import config_entry_oauth2_flow
from . import setup_integration
from .const import CLIENT_ID
from .const import CLIENT_ID, MOCK_APPROOT
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@ -89,25 +88,52 @@ async def test_full_flow(
assert result["data"][CONF_TOKEN]["refresh_token"] == "mock-refresh-token"
@pytest.mark.usefixtures("current_request_with_host")
async def test_full_flow_with_owner_not_found(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_onedrive_client: MagicMock,
) -> None:
"""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
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.CREATE_ENTRY
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
assert len(mock_setup_entry.mock_calls) == 1
assert result["title"] == "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"
@pytest.mark.usefixtures("current_request_with_host")
@pytest.mark.parametrize(
("exception", "error"),
[
(Exception, "unknown"),
(APIError, "connection_error"),
(OneDriveException, "connection_error"),
],
)
async def test_flow_errors(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
aioclient_mock: AiohttpClientMocker,
mock_adapter: MagicMock,
mock_onedrive_client: MagicMock,
exception: Exception,
error: str,
) -> None:
"""Test errors during flow."""
mock_adapter.get_http_response_message.side_effect = exception
mock_onedrive_client.get_approot.side_effect = exception
result = await hass.config_entries.flow.async_init(
DOMAIN, context={"source": config_entries.SOURCE_USER}
@ -172,15 +198,12 @@ async def test_reauth_flow_id_changed(
aioclient_mock: AiohttpClientMocker,
mock_setup_entry: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_adapter: MagicMock,
mock_onedrive_client: MagicMock,
) -> None:
"""Test that the reauth flow fails on a different drive id."""
mock_adapter.get_http_response_message.return_value = Response(
status_code=200,
json={
"parentReference": {"driveId": "other_drive_id"},
},
)
app_root = MOCK_APPROOT
app_root.parent_reference.drive_id = "other_drive_id"
mock_onedrive_client.get_approot.return_value = app_root
await setup_integration(hass, mock_config_entry)

View File

@ -2,7 +2,7 @@
from unittest.mock import MagicMock
from kiota_abstractions.api_error import APIError
from onedrive_personal_sdk.exceptions import AuthenticationError, OneDriveException
import pytest
from homeassistant.config_entries import ConfigEntryState
@ -31,82 +31,31 @@ async def test_load_unload_config_entry(
@pytest.mark.parametrize(
("side_effect", "state"),
[
(APIError(response_status_code=403), ConfigEntryState.SETUP_ERROR),
(APIError(response_status_code=500), ConfigEntryState.SETUP_RETRY),
(AuthenticationError(403, "Auth failed"), ConfigEntryState.SETUP_ERROR),
(OneDriveException(), ConfigEntryState.SETUP_RETRY),
],
)
async def test_approot_errors(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_get_special_folder: MagicMock,
mock_onedrive_client: MagicMock,
side_effect: Exception,
state: ConfigEntryState,
) -> None:
"""Test errors during approot retrieval."""
mock_get_special_folder.side_effect = side_effect
mock_onedrive_client.get_approot.side_effect = side_effect
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is state
async def test_faulty_approot(
async def test_get_integration_folder_error(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_get_special_folder: MagicMock,
mock_onedrive_client: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test faulty approot retrieval."""
mock_get_special_folder.return_value = None
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to get approot folder" in caplog.text
async def test_faulty_integration_folder(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_drive_items: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test faulty approot retrieval."""
mock_drive_items.get.return_value = None
mock_onedrive_client.create_folder.side_effect = OneDriveException()
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to get backups_9f86d081 folder" in caplog.text
async def test_500_error_during_backup_folder_get(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_drive_items: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error during backup folder creation."""
mock_drive_items.get.side_effect = APIError(response_status_code=500)
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to get backups_9f86d081 folder" in caplog.text
async def test_error_during_backup_folder_creation(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_drive_items: MagicMock,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test error during backup folder creation."""
mock_drive_items.get.side_effect = APIError(response_status_code=404)
mock_drive_items.children.post.side_effect = APIError()
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY
assert "Failed to create backups_9f86d081 folder" in caplog.text
async def test_successful_backup_folder_creation(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_drive_items: MagicMock,
) -> None:
"""Test successful backup folder creation."""
mock_drive_items.get.side_effect = APIError(response_status_code=404)
await setup_integration(hass, mock_config_entry)
assert mock_config_entry.state is ConfigEntryState.LOADED