mirror of
https://github.com/home-assistant/core.git
synced 2025-07-19 11:17:21 +00:00
Test the google tasks api connection in setup (#132657)
Improve google tasks setup
This commit is contained in:
parent
77debcbe8b
commit
355e80aa56
@ -4,7 +4,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from aiohttp import ClientError, ClientResponseError
|
from aiohttp import ClientError, ClientResponseError
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||||
@ -12,11 +11,17 @@ from homeassistant.helpers import config_entry_oauth2_flow
|
|||||||
|
|
||||||
from . import api
|
from . import api
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
from .exceptions import GoogleTasksApiError
|
||||||
|
from .types import GoogleTasksConfigEntry, GoogleTasksData
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"DOMAIN",
|
||||||
|
]
|
||||||
|
|
||||||
PLATFORMS: list[Platform] = [Platform.TODO]
|
PLATFORMS: list[Platform] = [Platform.TODO]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_setup_entry(hass: HomeAssistant, entry: GoogleTasksConfigEntry) -> bool:
|
||||||
"""Set up Google Tasks from a config entry."""
|
"""Set up Google Tasks from a config entry."""
|
||||||
implementation = (
|
implementation = (
|
||||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||||
@ -36,16 +41,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||||||
except ClientError as err:
|
except ClientError as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = auth
|
try:
|
||||||
|
task_lists = await auth.list_task_lists()
|
||||||
|
except GoogleTasksApiError as err:
|
||||||
|
raise ConfigEntryNotReady from err
|
||||||
|
|
||||||
|
entry.runtime_data = GoogleTasksData(auth, task_lists)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
async def async_unload_entry(
|
||||||
|
hass: HomeAssistant, entry: GoogleTasksConfigEntry
|
||||||
|
) -> bool:
|
||||||
"""Unload a config entry."""
|
"""Unload a config entry."""
|
||||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||||
hass.data[DOMAIN].pop(entry.entry_id)
|
|
||||||
|
|
||||||
return unload_ok
|
|
||||||
|
@ -11,15 +11,13 @@ from homeassistant.components.todo import (
|
|||||||
TodoListEntity,
|
TodoListEntity,
|
||||||
TodoListEntityFeature,
|
TodoListEntityFeature,
|
||||||
)
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .api import AsyncConfigEntryAuth
|
|
||||||
from .const import DOMAIN
|
|
||||||
from .coordinator import TaskUpdateCoordinator
|
from .coordinator import TaskUpdateCoordinator
|
||||||
|
from .types import GoogleTasksConfigEntry
|
||||||
|
|
||||||
SCAN_INTERVAL = timedelta(minutes=15)
|
SCAN_INTERVAL = timedelta(minutes=15)
|
||||||
|
|
||||||
@ -69,20 +67,20 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
|
|||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
|
hass: HomeAssistant,
|
||||||
|
entry: GoogleTasksConfigEntry,
|
||||||
|
async_add_entities: AddEntitiesCallback,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set up the Google Tasks todo platform."""
|
"""Set up the Google Tasks todo platform."""
|
||||||
api: AsyncConfigEntryAuth = hass.data[DOMAIN][entry.entry_id]
|
|
||||||
task_lists = await api.list_task_lists()
|
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
(
|
(
|
||||||
GoogleTaskTodoListEntity(
|
GoogleTaskTodoListEntity(
|
||||||
TaskUpdateCoordinator(hass, api, task_list["id"]),
|
TaskUpdateCoordinator(hass, entry.runtime_data.api, task_list["id"]),
|
||||||
task_list["title"],
|
task_list["title"],
|
||||||
entry.entry_id,
|
entry.entry_id,
|
||||||
task_list["id"],
|
task_list["id"],
|
||||||
)
|
)
|
||||||
for task_list in task_lists
|
for task_list in entry.runtime_data.task_lists
|
||||||
),
|
),
|
||||||
True,
|
True,
|
||||||
)
|
)
|
||||||
|
19
homeassistant/components/google_tasks/types.py
Normal file
19
homeassistant/components/google_tasks/types.py
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
"""Types for the Google Tasks integration."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
|
||||||
|
from .api import AsyncConfigEntryAuth
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GoogleTasksData:
|
||||||
|
"""Class to hold Google Tasks data."""
|
||||||
|
|
||||||
|
api: AsyncConfigEntryAuth
|
||||||
|
task_lists: list[dict[str, Any]]
|
||||||
|
|
||||||
|
|
||||||
|
type GoogleTasksConfigEntry = ConfigEntry[GoogleTasksData]
|
@ -1,10 +1,12 @@
|
|||||||
"""Test fixtures for Google Tasks."""
|
"""Test fixtures for Google Tasks."""
|
||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
|
import json
|
||||||
import time
|
import time
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from httplib2 import Response
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.application_credentials import (
|
from homeassistant.components.application_credentials import (
|
||||||
@ -24,6 +26,14 @@ FAKE_ACCESS_TOKEN = "some-access-token"
|
|||||||
FAKE_REFRESH_TOKEN = "some-refresh-token"
|
FAKE_REFRESH_TOKEN = "some-refresh-token"
|
||||||
FAKE_AUTH_IMPL = "conftest-imported-cred"
|
FAKE_AUTH_IMPL = "conftest-imported-cred"
|
||||||
|
|
||||||
|
TASK_LIST = {
|
||||||
|
"id": "task-list-id-1",
|
||||||
|
"title": "My tasks",
|
||||||
|
}
|
||||||
|
LIST_TASK_LIST_RESPONSE = {
|
||||||
|
"items": [TASK_LIST],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def platforms() -> list[Platform]:
|
def platforms() -> list[Platform]:
|
||||||
@ -89,3 +99,31 @@ async def mock_integration_setup(
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
return run
|
return run
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="api_responses")
|
||||||
|
def mock_api_responses() -> list[dict | list]:
|
||||||
|
"""Fixture forcreate_response_object API responses to return during test."""
|
||||||
|
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(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@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
|
||||||
|
def mock_http_response(response_handler: list | Callable) -> Mock:
|
||||||
|
"""Fixture to fake out http2lib responses."""
|
||||||
|
|
||||||
|
with patch("httplib2.Http.request", side_effect=response_handler) as mock_response:
|
||||||
|
yield mock_response
|
||||||
|
@ -2,8 +2,11 @@
|
|||||||
|
|
||||||
from collections.abc import Awaitable, Callable
|
from collections.abc import Awaitable, Callable
|
||||||
import http
|
import http
|
||||||
|
from http import HTTPStatus
|
||||||
import time
|
import time
|
||||||
|
from unittest.mock import Mock
|
||||||
|
|
||||||
|
from httplib2 import Response
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.google_tasks import DOMAIN
|
from homeassistant.components.google_tasks import DOMAIN
|
||||||
@ -11,15 +14,19 @@ from homeassistant.components.google_tasks.const import OAUTH2_TOKEN
|
|||||||
from homeassistant.config_entries import ConfigEntryState
|
from homeassistant.config_entries import ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from .conftest import LIST_TASK_LIST_RESPONSE
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("api_responses", [[LIST_TASK_LIST_RESPONSE]])
|
||||||
async def test_setup(
|
async def test_setup(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
integration_setup: Callable[[], Awaitable[bool]],
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
|
mock_http_response: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test successful setup and unload."""
|
"""Test successful setup and unload."""
|
||||||
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
assert config_entry.state is ConfigEntryState.NOT_LOADED
|
||||||
@ -35,12 +42,14 @@ async def test_setup(
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"])
|
@pytest.mark.parametrize("expires_at", [time.time() - 3600], ids=["expired"])
|
||||||
|
@pytest.mark.parametrize("api_responses", [[LIST_TASK_LIST_RESPONSE]])
|
||||||
async def test_expired_token_refresh_success(
|
async def test_expired_token_refresh_success(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
integration_setup: Callable[[], Awaitable[bool]],
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
config_entry: MockConfigEntry,
|
config_entry: MockConfigEntry,
|
||||||
setup_credentials: None,
|
setup_credentials: None,
|
||||||
|
mock_http_response: Mock,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test expired token is refreshed."""
|
"""Test expired token is refreshed."""
|
||||||
|
|
||||||
@ -98,3 +107,22 @@ async def test_expired_token_refresh_failure(
|
|||||||
await integration_setup()
|
await integration_setup()
|
||||||
|
|
||||||
assert config_entry.state is expected_state
|
assert config_entry.state is expected_state
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"response_handler",
|
||||||
|
[
|
||||||
|
([(Response({"status": HTTPStatus.INTERNAL_SERVER_ERROR}), b"")]),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_setup_error(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
setup_credentials: None,
|
||||||
|
integration_setup: Callable[[], Awaitable[bool]],
|
||||||
|
mock_http_response: Mock,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
) -> None:
|
||||||
|
"""Test an error returned by the server when setting up the platform."""
|
||||||
|
|
||||||
|
assert not await integration_setup()
|
||||||
|
assert config_entry.state is ConfigEntryState.SETUP_RETRY
|
||||||
|
@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
|
|||||||
from http import HTTPStatus
|
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
|
||||||
|
|
||||||
from httplib2 import Response
|
from httplib2 import Response
|
||||||
import pytest
|
import pytest
|
||||||
@ -23,16 +23,11 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
|||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import HomeAssistantError
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
|
from .conftest import LIST_TASK_LIST_RESPONSE, create_response_object
|
||||||
|
|
||||||
from tests.typing import WebSocketGenerator
|
from tests.typing import WebSocketGenerator
|
||||||
|
|
||||||
ENTITY_ID = "todo.my_tasks"
|
ENTITY_ID = "todo.my_tasks"
|
||||||
ITEM = {
|
|
||||||
"id": "task-list-id-1",
|
|
||||||
"title": "My tasks",
|
|
||||||
}
|
|
||||||
LIST_TASK_LIST_RESPONSE = {
|
|
||||||
"items": [ITEM],
|
|
||||||
}
|
|
||||||
EMPTY_RESPONSE = {}
|
EMPTY_RESPONSE = {}
|
||||||
LIST_TASKS_RESPONSE = {
|
LIST_TASKS_RESPONSE = {
|
||||||
"items": [],
|
"items": [],
|
||||||
@ -149,20 +144,6 @@ async def ws_get_items(
|
|||||||
return get
|
return get
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="api_responses")
|
|
||||||
def mock_api_responses() -> list[dict | list]:
|
|
||||||
"""Fixture for API responses to return during test."""
|
|
||||||
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(
|
def create_batch_response_object(
|
||||||
content_ids: list[str], api_responses: list[dict | list | Response | None]
|
content_ids: list[str], api_responses: list[dict | list | Response | None]
|
||||||
) -> tuple[Response, bytes]:
|
) -> tuple[Response, bytes]:
|
||||||
@ -225,18 +206,10 @@ def create_batch_response_handler(
|
|||||||
return _handler
|
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(response_handler: list | Callable) -> Mock:
|
def setup_http_response(mock_http_response: Mock) -> None:
|
||||||
"""Fixture to fake out http2lib responses."""
|
"""Fixture to load the http response mock."""
|
||||||
|
return
|
||||||
with patch("httplib2.Http.request", side_effect=response_handler) as mock_response:
|
|
||||||
yield mock_response
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
|
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
|
||||||
@ -303,29 +276,6 @@ 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",
|
||||||
[
|
[
|
||||||
|
Loading…
x
Reference in New Issue
Block a user