Test the google tasks api connection in setup (#132657)

Improve google tasks setup
pull/132895/head
Allen Porter 2024-12-10 19:01:50 -08:00 committed by GitHub
parent 77debcbe8b
commit 355e80aa56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 115 additions and 73 deletions

View File

@ -4,7 +4,6 @@ from __future__ import annotations
from aiohttp import ClientError, ClientResponseError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
@ -12,11 +11,17 @@ from homeassistant.helpers import config_entry_oauth2_flow
from . import api
from .const import DOMAIN
from .exceptions import GoogleTasksApiError
from .types import GoogleTasksConfigEntry, GoogleTasksData
__all__ = [
"DOMAIN",
]
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."""
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:
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)
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."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -11,15 +11,13 @@ from homeassistant.components.todo import (
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .api import AsyncConfigEntryAuth
from .const import DOMAIN
from .coordinator import TaskUpdateCoordinator
from .types import GoogleTasksConfigEntry
SCAN_INTERVAL = timedelta(minutes=15)
@ -69,20 +67,20 @@ def _convert_api_item(item: dict[str, str]) -> TodoItem:
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: GoogleTasksConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""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(
(
GoogleTaskTodoListEntity(
TaskUpdateCoordinator(hass, api, task_list["id"]),
TaskUpdateCoordinator(hass, entry.runtime_data.api, task_list["id"]),
task_list["title"],
entry.entry_id,
task_list["id"],
)
for task_list in task_lists
for task_list in entry.runtime_data.task_lists
),
True,
)

View 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]

View File

@ -1,10 +1,12 @@
"""Test fixtures for Google Tasks."""
from collections.abc import Awaitable, Callable
import json
import time
from typing import Any
from unittest.mock import patch
from unittest.mock import Mock, patch
from httplib2 import Response
import pytest
from homeassistant.components.application_credentials import (
@ -24,6 +26,14 @@ FAKE_ACCESS_TOKEN = "some-access-token"
FAKE_REFRESH_TOKEN = "some-refresh-token"
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
def platforms() -> list[Platform]:
@ -89,3 +99,31 @@ async def mock_integration_setup(
return result
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

View File

@ -2,8 +2,11 @@
from collections.abc import Awaitable, Callable
import http
from http import HTTPStatus
import time
from unittest.mock import Mock
from httplib2 import Response
import pytest
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.core import HomeAssistant
from .conftest import LIST_TASK_LIST_RESPONSE
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@pytest.mark.parametrize("api_responses", [[LIST_TASK_LIST_RESPONSE]])
async def test_setup(
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],
config_entry: MockConfigEntry,
setup_credentials: None,
mock_http_response: Mock,
) -> None:
"""Test successful setup and unload."""
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("api_responses", [[LIST_TASK_LIST_RESPONSE]])
async def test_expired_token_refresh_success(
hass: HomeAssistant,
integration_setup: Callable[[], Awaitable[bool]],
aioclient_mock: AiohttpClientMocker,
config_entry: MockConfigEntry,
setup_credentials: None,
mock_http_response: Mock,
) -> None:
"""Test expired token is refreshed."""
@ -98,3 +107,22 @@ async def test_expired_token_refresh_failure(
await integration_setup()
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

View File

@ -4,7 +4,7 @@ from collections.abc import Awaitable, Callable
from http import HTTPStatus
import json
from typing import Any
from unittest.mock import Mock, patch
from unittest.mock import Mock
from httplib2 import Response
import pytest
@ -23,16 +23,11 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from .conftest import LIST_TASK_LIST_RESPONSE, create_response_object
from tests.typing import WebSocketGenerator
ENTITY_ID = "todo.my_tasks"
ITEM = {
"id": "task-list-id-1",
"title": "My tasks",
}
LIST_TASK_LIST_RESPONSE = {
"items": [ITEM],
}
EMPTY_RESPONSE = {}
LIST_TASKS_RESPONSE = {
"items": [],
@ -149,20 +144,6 @@ async def ws_get_items(
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(
content_ids: list[str], api_responses: list[dict | list | Response | None]
) -> tuple[Response, bytes]:
@ -225,18 +206,10 @@ def create_batch_response_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)
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
def setup_http_response(mock_http_response: Mock) -> None:
"""Fixture to load the http response mock."""
return
@pytest.mark.parametrize("timezone", ["America/Regina", "UTC", "Asia/Tokyo"])
@ -303,29 +276,6 @@ async def test_get_items(
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(
"api_responses",
[