Add support for deleting To-do items in Google Tasks (#102967)
* Add support for deleting To-do items in Google Tasks * Cleanup multipart test * Fix comments * Add additional error checking to increase coverage * Apply suggestions and fix tests
This commit is contained in:
parent
5901f6f7e7
commit
cec617cfbb
5 changed files with 470 additions and 13 deletions
|
@ -1,18 +1,36 @@
|
||||||
"""API for Google Tasks bound to Home Assistant OAuth."""
|
"""API for Google Tasks bound to Home Assistant OAuth."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from google.oauth2.credentials import Credentials
|
from google.oauth2.credentials import Credentials
|
||||||
from googleapiclient.discovery import Resource, build
|
from googleapiclient.discovery import Resource, build
|
||||||
from googleapiclient.http import HttpRequest
|
from googleapiclient.errors import HttpError
|
||||||
|
from googleapiclient.http import BatchHttpRequest, HttpRequest
|
||||||
|
|
||||||
from homeassistant.const import CONF_ACCESS_TOKEN
|
from homeassistant.const import CONF_ACCESS_TOKEN
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers import config_entry_oauth2_flow
|
from homeassistant.helpers import config_entry_oauth2_flow
|
||||||
|
|
||||||
|
from .exceptions import GoogleTasksApiError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
MAX_TASK_RESULTS = 100
|
MAX_TASK_RESULTS = 100
|
||||||
|
|
||||||
|
|
||||||
|
def _raise_if_error(result: Any | dict[str, Any]) -> None:
|
||||||
|
"""Raise a GoogleTasksApiError if the response contains an error."""
|
||||||
|
if not isinstance(result, dict):
|
||||||
|
raise GoogleTasksApiError(
|
||||||
|
f"Google Tasks API replied with unexpected response: {result}"
|
||||||
|
)
|
||||||
|
if error := result.get("error"):
|
||||||
|
message = error.get("message", "Unknown Error")
|
||||||
|
raise GoogleTasksApiError(f"Google Tasks API response: {message}")
|
||||||
|
|
||||||
|
|
||||||
class AsyncConfigEntryAuth:
|
class AsyncConfigEntryAuth:
|
||||||
"""Provide Google Tasks authentication tied to an OAuth2 based config entry."""
|
"""Provide Google Tasks authentication tied to an OAuth2 based config entry."""
|
||||||
|
|
||||||
|
@ -40,7 +58,7 @@ class AsyncConfigEntryAuth:
|
||||||
"""Get all TaskList resources."""
|
"""Get all TaskList resources."""
|
||||||
service = await self._get_service()
|
service = await self._get_service()
|
||||||
cmd: HttpRequest = service.tasklists().list()
|
cmd: HttpRequest = service.tasklists().list()
|
||||||
result = await self._hass.async_add_executor_job(cmd.execute)
|
result = await self._execute(cmd)
|
||||||
return result["items"]
|
return result["items"]
|
||||||
|
|
||||||
async def list_tasks(self, task_list_id: str) -> list[dict[str, Any]]:
|
async def list_tasks(self, task_list_id: str) -> list[dict[str, Any]]:
|
||||||
|
@ -49,7 +67,7 @@ class AsyncConfigEntryAuth:
|
||||||
cmd: HttpRequest = service.tasks().list(
|
cmd: HttpRequest = service.tasks().list(
|
||||||
tasklist=task_list_id, maxResults=MAX_TASK_RESULTS
|
tasklist=task_list_id, maxResults=MAX_TASK_RESULTS
|
||||||
)
|
)
|
||||||
result = await self._hass.async_add_executor_job(cmd.execute)
|
result = await self._execute(cmd)
|
||||||
return result["items"]
|
return result["items"]
|
||||||
|
|
||||||
async def insert(
|
async def insert(
|
||||||
|
@ -63,7 +81,7 @@ class AsyncConfigEntryAuth:
|
||||||
tasklist=task_list_id,
|
tasklist=task_list_id,
|
||||||
body=task,
|
body=task,
|
||||||
)
|
)
|
||||||
await self._hass.async_add_executor_job(cmd.execute)
|
await self._execute(cmd)
|
||||||
|
|
||||||
async def patch(
|
async def patch(
|
||||||
self,
|
self,
|
||||||
|
@ -78,4 +96,43 @@ class AsyncConfigEntryAuth:
|
||||||
task=task_id,
|
task=task_id,
|
||||||
body=task,
|
body=task,
|
||||||
)
|
)
|
||||||
await self._hass.async_add_executor_job(cmd.execute)
|
await self._execute(cmd)
|
||||||
|
|
||||||
|
async def delete(
|
||||||
|
self,
|
||||||
|
task_list_id: str,
|
||||||
|
task_ids: list[str],
|
||||||
|
) -> None:
|
||||||
|
"""Delete a task resources."""
|
||||||
|
service = await self._get_service()
|
||||||
|
batch: BatchHttpRequest = service.new_batch_http_request()
|
||||||
|
|
||||||
|
def response_handler(_, response, exception: HttpError) -> None:
|
||||||
|
if exception is not None:
|
||||||
|
raise GoogleTasksApiError(
|
||||||
|
f"Google Tasks API responded with error ({exception.status_code})"
|
||||||
|
) from exception
|
||||||
|
data = json.loads(response)
|
||||||
|
_raise_if_error(data)
|
||||||
|
|
||||||
|
for task_id in task_ids:
|
||||||
|
batch.add(
|
||||||
|
service.tasks().delete(
|
||||||
|
tasklist=task_list_id,
|
||||||
|
task=task_id,
|
||||||
|
),
|
||||||
|
request_id=task_id,
|
||||||
|
callback=response_handler,
|
||||||
|
)
|
||||||
|
await self._execute(batch)
|
||||||
|
|
||||||
|
async def _execute(self, request: HttpRequest | BatchHttpRequest) -> Any:
|
||||||
|
try:
|
||||||
|
result = await self._hass.async_add_executor_job(request.execute)
|
||||||
|
except HttpError as err:
|
||||||
|
raise GoogleTasksApiError(
|
||||||
|
f"Google Tasks API responded with error ({err.status_code})"
|
||||||
|
) from err
|
||||||
|
if result:
|
||||||
|
_raise_if_error(result)
|
||||||
|
return result
|
||||||
|
|
7
homeassistant/components/google_tasks/exceptions.py
Normal file
7
homeassistant/components/google_tasks/exceptions.py
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
"""Exceptions for Google Tasks api calls."""
|
||||||
|
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
|
||||||
|
class GoogleTasksApiError(HomeAssistantError):
|
||||||
|
"""Error talking to the Google Tasks API."""
|
|
@ -65,7 +65,9 @@ class GoogleTaskTodoListEntity(
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
_attr_supported_features = (
|
_attr_supported_features = (
|
||||||
TodoListEntityFeature.CREATE_TODO_ITEM | TodoListEntityFeature.UPDATE_TODO_ITEM
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
|
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
)
|
)
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
@ -114,3 +116,8 @@ class GoogleTaskTodoListEntity(
|
||||||
task=_convert_todo_item(item),
|
task=_convert_todo_item(item),
|
||||||
)
|
)
|
||||||
await self.coordinator.async_refresh()
|
await self.coordinator.async_refresh()
|
||||||
|
|
||||||
|
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||||
|
"""Delete To-do items."""
|
||||||
|
await self.coordinator.api.delete(self._task_list_id, uids)
|
||||||
|
await self.coordinator.async_refresh()
|
||||||
|
|
|
@ -8,6 +8,12 @@
|
||||||
# name: test_create_todo_list_item[api_responses0].1
|
# name: test_create_todo_list_item[api_responses0].1
|
||||||
'{"title": "Soda", "status": "needsAction"}'
|
'{"title": "Soda", "status": "needsAction"}'
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_delete_todo_list_item[_handler]
|
||||||
|
tuple(
|
||||||
|
'https://tasks.googleapis.com/batch',
|
||||||
|
'POST',
|
||||||
|
)
|
||||||
|
# ---
|
||||||
# name: test_partial_update_status[api_responses0]
|
# name: test_partial_update_status[api_responses0]
|
||||||
tuple(
|
tuple(
|
||||||
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
'https://tasks.googleapis.com/tasks/v1/lists/task-list-id-1/tasks/some-task-id?alt=json',
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
from http import HTTPStatus
|
||||||
import json
|
import json
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
@ -13,6 +14,7 @@ from syrupy.assertion import SnapshotAssertion
|
||||||
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
|
@ -29,12 +31,30 @@ EMPTY_RESPONSE = {}
|
||||||
LIST_TASKS_RESPONSE = {
|
LIST_TASKS_RESPONSE = {
|
||||||
"items": [],
|
"items": [],
|
||||||
}
|
}
|
||||||
|
ERROR_RESPONSE = {
|
||||||
|
"error": {
|
||||||
|
"code": 400,
|
||||||
|
"message": "Invalid task ID",
|
||||||
|
"errors": [
|
||||||
|
{"message": "Invalid task ID", "domain": "global", "reason": "invalid"}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CONTENT_ID = "Content-ID"
|
||||||
|
BOUNDARY = "batch_00972cc8-75bd-11ee-9692-0242ac110002" # Arbitrary uuid
|
||||||
|
|
||||||
LIST_TASKS_RESPONSE_WATER = {
|
LIST_TASKS_RESPONSE_WATER = {
|
||||||
"items": [
|
"items": [
|
||||||
{"id": "some-task-id", "title": "Water", "status": "needsAction"},
|
{"id": "some-task-id", "title": "Water", "status": "needsAction"},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
LIST_TASKS_RESPONSE_MULTIPLE = {
|
||||||
|
"items": [
|
||||||
|
{"id": "some-task-id-1", "title": "Water", "status": "needsAction"},
|
||||||
|
{"id": "some-task-id-2", "title": "Milk", "status": "needsAction"},
|
||||||
|
{"id": "some-task-id-3", "title": "Cheese", "status": "needsAction"},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -88,14 +108,87 @@ def mock_api_responses() -> list[dict | list]:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def create_response_object(api_response: dict | list) -> tuple[Response, bytes]:
|
||||||
|
"""Create an http response."""
|
||||||
|
return (
|
||||||
|
Response({"Content-Type": "application/json"}),
|
||||||
|
json.dumps(api_response).encode(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_batch_response_object(
|
||||||
|
content_ids: list[str], api_responses: list[dict | list | Response]
|
||||||
|
) -> tuple[Response, bytes]:
|
||||||
|
"""Create a batch response in the multipart/mixed format."""
|
||||||
|
assert len(api_responses) == len(content_ids)
|
||||||
|
content = []
|
||||||
|
for api_response in api_responses:
|
||||||
|
status = 200
|
||||||
|
body = ""
|
||||||
|
if isinstance(api_response, Response):
|
||||||
|
status = api_response.status
|
||||||
|
else:
|
||||||
|
body = json.dumps(api_response)
|
||||||
|
content.extend(
|
||||||
|
[
|
||||||
|
f"--{BOUNDARY}",
|
||||||
|
"Content-Type: application/http",
|
||||||
|
f"{CONTENT_ID}: {content_ids.pop()}",
|
||||||
|
"",
|
||||||
|
f"HTTP/1.1 {status} OK",
|
||||||
|
"Content-Type: application/json; charset=UTF-8",
|
||||||
|
"",
|
||||||
|
body,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
content.append(f"--{BOUNDARY}--")
|
||||||
|
body = ("\r\n".join(content)).encode()
|
||||||
|
return (
|
||||||
|
Response(
|
||||||
|
{
|
||||||
|
"Content-Type": f"multipart/mixed; boundary={BOUNDARY}",
|
||||||
|
"Content-ID": "1",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
body,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_batch_response_handler(
|
||||||
|
api_responses: list[dict | list | Response],
|
||||||
|
) -> Callable[[Any], tuple[Response, bytes]]:
|
||||||
|
"""Create a fake http2lib response handler that supports generating batch responses.
|
||||||
|
|
||||||
|
Multi-part response objects are dynamically generated since they
|
||||||
|
need to match the Content-ID of the incoming request.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _handler(url, method, **kwargs) -> tuple[Response, bytes]:
|
||||||
|
next_api_response = api_responses.pop(0)
|
||||||
|
if method == "POST" and (body := kwargs.get("body")):
|
||||||
|
content_ids = [
|
||||||
|
line[len(CONTENT_ID) + 2 :]
|
||||||
|
for line in body.splitlines()
|
||||||
|
if line.startswith(f"{CONTENT_ID}:")
|
||||||
|
]
|
||||||
|
if content_ids:
|
||||||
|
return create_batch_response_object(content_ids, next_api_response)
|
||||||
|
return create_response_object(next_api_response)
|
||||||
|
|
||||||
|
return _handler
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="response_handler")
|
||||||
|
def mock_response_handler(api_responses: list[dict | list]) -> list:
|
||||||
|
"""Create a mock http2lib response handler."""
|
||||||
|
return [create_response_object(api_response) for api_response in api_responses]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(autouse=True)
|
@pytest.fixture(autouse=True)
|
||||||
def mock_http_response(api_responses: list[dict | list]) -> Mock:
|
def mock_http_response(response_handler: list | Callable) -> Mock:
|
||||||
"""Fixture to fake out http2lib responses."""
|
"""Fixture to fake out http2lib responses."""
|
||||||
responses = [
|
|
||||||
(Response({}), bytes(json.dumps(api_response), encoding="utf-8"))
|
with patch("httplib2.Http.request", side_effect=response_handler) as mock_response:
|
||||||
for api_response in api_responses
|
|
||||||
]
|
|
||||||
with patch("httplib2.Http.request", side_effect=responses) as mock_response:
|
|
||||||
yield mock_response
|
yield mock_response
|
||||||
|
|
||||||
|
|
||||||
|
@ -146,6 +239,29 @@ async def test_get_items(
|
||||||
assert state.state == "1"
|
assert state.state == "1"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
([(Response({"status": HTTPStatus.INTERNAL_SERVER_ERROR}), b"")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_list_items_server_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
|
||||||
|
) -> None:
|
||||||
|
"""Test an error returned by the server when setting up the platform."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
await hass_ws_client(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state is None
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
"api_responses",
|
||||||
[
|
[
|
||||||
|
@ -176,6 +292,33 @@ async def test_empty_todo_list(
|
||||||
assert state.state == "0"
|
assert state.state == "0"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
ERROR_RESPONSE,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_task_items_error_response(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
hass_ws_client: WebSocketGenerator,
|
||||||
|
ws_get_items: Callable[[], Awaitable[dict[str, str]]],
|
||||||
|
) -> None:
|
||||||
|
"""Test an error while getting todo list items."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
await hass_ws_client(hass)
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "unavailable"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
"api_responses",
|
||||||
[
|
[
|
||||||
|
@ -183,7 +326,7 @@ async def test_empty_todo_list(
|
||||||
LIST_TASK_LIST_RESPONSE,
|
LIST_TASK_LIST_RESPONSE,
|
||||||
LIST_TASKS_RESPONSE,
|
LIST_TASKS_RESPONSE,
|
||||||
EMPTY_RESPONSE, # create
|
EMPTY_RESPONSE, # create
|
||||||
LIST_TASKS_RESPONSE, # refresh after create
|
LIST_TASKS_RESPONSE, # refresh after delete
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -216,6 +359,41 @@ async def test_create_todo_list_item(
|
||||||
assert call.kwargs.get("body") == snapshot
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_WATER,
|
||||||
|
ERROR_RESPONSE,
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_create_todo_list_item_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Mock,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for an error response when creating a To-do Item."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "1"
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="Invalid task ID"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"add_item",
|
||||||
|
{"item": "Soda"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
"api_responses",
|
||||||
[
|
[
|
||||||
|
@ -256,6 +434,41 @@ async def test_update_todo_list_item(
|
||||||
assert call.kwargs.get("body") == snapshot
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"api_responses",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_WATER,
|
||||||
|
ERROR_RESPONSE, # update fails
|
||||||
|
]
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_todo_list_item_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for an error response when updating a To-do Item."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "1"
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="Invalid task ID"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{"item": "some-task-id", "rename": "Soda", "status": "completed"},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"api_responses",
|
"api_responses",
|
||||||
[
|
[
|
||||||
|
@ -334,3 +547,170 @@ async def test_partial_update_status(
|
||||||
assert call
|
assert call
|
||||||
assert call.args == snapshot
|
assert call.args == snapshot
|
||||||
assert call.kwargs.get("body") == snapshot
|
assert call.kwargs.get("body") == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
create_batch_response_handler(
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_MULTIPLE,
|
||||||
|
[EMPTY_RESPONSE, EMPTY_RESPONSE, EMPTY_RESPONSE], # Delete batch
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after create
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_delete_todo_list_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for deleting multiple To-do Items."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "3"
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": ["some-task-id-1", "some-task-id-2", "some-task-id-3"]},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
assert len(mock_http_response.call_args_list) == 4
|
||||||
|
call = mock_http_response.call_args_list[2]
|
||||||
|
assert call
|
||||||
|
assert call.args == snapshot
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
create_batch_response_handler(
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_MULTIPLE,
|
||||||
|
[
|
||||||
|
EMPTY_RESPONSE,
|
||||||
|
ERROR_RESPONSE, # one item is a failure
|
||||||
|
EMPTY_RESPONSE,
|
||||||
|
],
|
||||||
|
LIST_TASKS_RESPONSE, # refresh after create
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_delete_partial_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test for partial failure when deleting multiple To-do Items."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "3"
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="Invalid task ID"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": ["some-task-id-1", "some-task-id-2", "some-task-id-3"]},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
create_batch_response_handler(
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_MULTIPLE,
|
||||||
|
[
|
||||||
|
"1234-invalid-json",
|
||||||
|
],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_delete_invalid_json_response(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test delete with an invalid json response."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "3"
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="unexpected response"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": ["some-task-id-1"]},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
create_batch_response_handler(
|
||||||
|
[
|
||||||
|
LIST_TASK_LIST_RESPONSE,
|
||||||
|
LIST_TASKS_RESPONSE_MULTIPLE,
|
||||||
|
[Response({"status": HTTPStatus.INTERNAL_SERVER_ERROR})],
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_delete_server_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Any,
|
||||||
|
snapshot: SnapshotAssertion,
|
||||||
|
) -> None:
|
||||||
|
"""Test delete with an invalid json response."""
|
||||||
|
|
||||||
|
assert await integration_setup()
|
||||||
|
|
||||||
|
state = hass.states.get("todo.my_tasks")
|
||||||
|
assert state
|
||||||
|
assert state.state == "3"
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="responded with error"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": ["some-task-id-1"]},
|
||||||
|
target={"entity_id": "todo.my_tasks"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
Loading…
Add table
Reference in a new issue