Skip to content

Commit

Permalink
Test the google tasks api connection in setup (home-assistant#132657)
Browse files Browse the repository at this point in the history
Improve google tasks setup
  • Loading branch information
allenporter authored Dec 11, 2024
1 parent 77debcb commit 355e80a
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 73 deletions.
25 changes: 17 additions & 8 deletions homeassistant/components/google_tasks/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,24 @@

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
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(
Expand All @@ -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)
14 changes: 6 additions & 8 deletions homeassistant/components/google_tasks/todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,13 @@
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)

Expand Down Expand Up @@ -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,
)
Expand Down
19 changes: 19 additions & 0 deletions homeassistant/components/google_tasks/types.py
Original file line number Diff line number Diff line change
@@ -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]
40 changes: 39 additions & 1 deletion tests/components/google_tasks/conftest.py
Original file line number Diff line number Diff line change
@@ -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 (
Expand All @@ -24,6 +26,14 @@
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]:
Expand Down Expand Up @@ -89,3 +99,31 @@ async def run() -> bool:
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
28 changes: 28 additions & 0 deletions tests/components/google_tasks/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,31 @@

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
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
Expand All @@ -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."""

Expand Down Expand Up @@ -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
62 changes: 6 additions & 56 deletions tests/components/google_tasks/test_todo.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
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
Expand All @@ -23,16 +23,11 @@
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": [],
Expand Down Expand Up @@ -149,20 +144,6 @@ async def get() -> list[dict[str, str]]:
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]:
Expand Down Expand Up @@ -225,18 +206,10 @@ def _handler(url, method, **kwargs) -> tuple[Response, bytes]:
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"])
Expand Down Expand Up @@ -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",
[
Expand Down

0 comments on commit 355e80a

Please sign in to comment.