diff --git a/homeassistant/components/google/diagnostics.py b/homeassistant/components/google/diagnostics.py new file mode 100644 index 00000000000..0313e61bc8e --- /dev/null +++ b/homeassistant/components/google/diagnostics.py @@ -0,0 +1,55 @@ +"""Provides diagnostics for google calendar.""" + +import datetime +from typing import Any + +from homeassistant.components.diagnostics import async_redact_data +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from .const import DATA_STORE, DOMAIN + +TO_REDACT = { + "id", + "ical_uuid", + "summary", + "description", + "location", + "attendees", + "recurring_event_id", +} + + +def redact_store(data: dict[str, Any]) -> dict[str, Any]: + """Redact personal information from calendar events in the store.""" + id_num = 0 + diagnostics = {} + for store_data in data.values(): + local_store: dict[str, Any] = store_data.get("event_sync", {}) + for calendar_data in local_store.values(): + id_num += 1 + items: dict[str, Any] = calendar_data.get("items", {}) + diagnostics[f"calendar#{id_num}"] = { + "events": [ + async_redact_data(item, TO_REDACT) for item in items.values() + ], + "sync_token_version": calendar_data.get("sync_token_version"), + } + return diagnostics + + +async def async_get_config_entry_diagnostics( + hass: HomeAssistant, config_entry: ConfigEntry +) -> dict[str, Any]: + """Return diagnostics for a config entry.""" + payload: dict[str, Any] = { + "now": dt_util.now().isoformat(), + "timezone": str(dt_util.DEFAULT_TIME_ZONE), + "system_timezone": str(datetime.datetime.now().astimezone().tzinfo), + } + + store = hass.data[DOMAIN][config_entry.entry_id][DATA_STORE] + data = await store.async_load() + payload["store"] = redact_store(data) + return payload diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 57e542e8a21..d938a2f3291 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -58,6 +58,35 @@ TEST_API_CALENDAR = { "defaultReminders": [], } +TEST_EVENT = { + "summary": "Test All Day Event", + "start": {}, + "end": {}, + "location": "Test Cases", + "description": "test event", + "kind": "calendar#event", + "created": "2016-06-23T16:37:57.000Z", + "transparency": "transparent", + "updated": "2016-06-24T01:57:21.045Z", + "reminders": {"useDefault": True}, + "organizer": { + "email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com", + "displayName": "Organizer Name", + "self": True, + }, + "sequence": 0, + "creator": { + "email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com", + "displayName": "Organizer Name", + "self": True, + }, + "id": "_c8rinwq863h45qnucyoi43ny8", + "etag": '"2933466882090000"', + "htmlLink": "https://www.google.com/calendar/event?eid=*******", + "iCalUID": "cydrevtfuybguinhomj@google.com", + "status": "confirmed", +} + CLIENT_ID = "client-id" CLIENT_SECRET = "client-secret" @@ -232,7 +261,7 @@ def mock_events_list( @pytest.fixture def mock_events_list_items( mock_events_list: Callable[[dict[str, Any]], None] -) -> Callable[list[[dict[str, Any]]], None]: +) -> Callable[[list[dict[str, Any]]], None]: """Fixture to construct an API response containing event items.""" def _put_items(items: list[dict[str, Any]]) -> None: diff --git a/tests/components/google/snapshots/test_diagnostics.ambr b/tests/components/google/snapshots/test_diagnostics.ambr new file mode 100644 index 00000000000..c19d3a82f74 --- /dev/null +++ b/tests/components/google/snapshots/test_diagnostics.ambr @@ -0,0 +1,70 @@ +# serializer version: 1 +# name: test_diagnostics + dict({ + 'now': '2023-03-13T13:05:00-06:00', + 'store': dict({ + 'calendar#1': dict({ + 'events': list([ + dict({ + 'attendees': '**REDACTED**', + 'attendees_omitted': False, + 'description': '**REDACTED**', + 'end': dict({ + 'date': None, + 'date_time': '2023-03-13T12:30:00-07:00', + 'timezone': None, + }), + 'event_type': 'default', + 'ical_uuid': '**REDACTED**', + 'id': '**REDACTED**', + 'location': '**REDACTED**', + 'original_start_time': None, + 'recurrence': list([ + ]), + 'recurring_event_id': None, + 'start': dict({ + 'date': None, + 'date_time': '2023-03-13T12:00:00-07:00', + 'timezone': None, + }), + 'status': 'confirmed', + 'summary': '**REDACTED**', + 'transparency': 'transparent', + 'visibility': 'default', + }), + dict({ + 'attendees': '**REDACTED**', + 'attendees_omitted': False, + 'description': '**REDACTED**', + 'end': dict({ + 'date': '2022-10-09', + 'date_time': None, + 'timezone': None, + }), + 'event_type': 'default', + 'ical_uuid': '**REDACTED**', + 'id': '**REDACTED**', + 'location': '**REDACTED**', + 'original_start_time': None, + 'recurrence': list([ + 'RRULE:FREQ=WEEKLY', + ]), + 'recurring_event_id': None, + 'start': dict({ + 'date': '2022-10-08', + 'date_time': None, + 'timezone': None, + }), + 'status': 'confirmed', + 'summary': '**REDACTED**', + 'transparency': 'transparent', + 'visibility': 'default', + }), + ]), + 'sync_token_version': 2, + }), + }), + 'system_timezone': 'tzlocal()', + 'timezone': 'America/Regina', + }) +# --- diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index d6431700fca..3a9673441c0 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -23,6 +23,7 @@ from .conftest import ( CALENDAR_ID, TEST_API_ENTITY, TEST_API_ENTITY_NAME, + TEST_EVENT, TEST_YAML_ENTITY, TEST_YAML_ENTITY_NAME, ApiResult, @@ -36,35 +37,6 @@ from tests.typing import ClientSessionGenerator, WebSocketGenerator TEST_ENTITY = TEST_API_ENTITY TEST_ENTITY_NAME = TEST_API_ENTITY_NAME -TEST_EVENT = { - "summary": "Test All Day Event", - "start": {}, - "end": {}, - "location": "Test Cases", - "description": "test event", - "kind": "calendar#event", - "created": "2016-06-23T16:37:57.000Z", - "transparency": "transparent", - "updated": "2016-06-24T01:57:21.045Z", - "reminders": {"useDefault": True}, - "organizer": { - "email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com", - "displayName": "Organizer Name", - "self": True, - }, - "sequence": 0, - "creator": { - "email": "uvrttabwegnui4gtia3vyqb@import.calendar.google.com", - "displayName": "Organizer Name", - "self": True, - }, - "id": "_c8rinwq863h45qnucyoi43ny8", - "etag": '"2933466882090000"', - "htmlLink": "https://www.google.com/calendar/event?eid=*******", - "iCalUID": "cydrevtfuybguinhomj@google.com", - "status": "confirmed", -} - @pytest.fixture(autouse=True) def mock_test_setup( diff --git a/tests/components/google/test_diagnostics.py b/tests/components/google/test_diagnostics.py new file mode 100644 index 00000000000..5ebc683485b --- /dev/null +++ b/tests/components/google/test_diagnostics.py @@ -0,0 +1,106 @@ +"""Tests for diagnostics platform of google calendar.""" +from collections.abc import Callable +from typing import Any + +from aiohttp.test_utils import TestClient +from freezegun import freeze_time +import pytest +from syrupy.assertion import SnapshotAssertion + +from homeassistant.auth.models import Credentials +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from .conftest import TEST_EVENT, ComponentSetup + +from tests.common import CLIENT_ID, MockConfigEntry, MockUser +from tests.components.diagnostics import get_diagnostics_for_config_entry +from tests.typing import ClientSessionGenerator + + +@pytest.fixture(autouse=True) +def mock_test_setup( + test_api_calendar, + mock_calendars_list, +): + """Fixture that sets up the default API responses during integration setup.""" + mock_calendars_list({"items": [test_api_calendar]}) + + +async def generate_new_hass_access_token( + hass: HomeAssistant, hass_admin_user: MockUser, hass_admin_credential: Credentials +) -> str: + """Return an access token to access Home Assistant.""" + await hass.auth.async_link_user(hass_admin_user, hass_admin_credential) + + refresh_token = await hass.auth.async_create_refresh_token( + hass_admin_user, CLIENT_ID, credential=hass_admin_credential + ) + return hass.auth.async_create_access_token(refresh_token) + + +def _get_test_client_generator( + hass: HomeAssistant, aiohttp_client: ClientSessionGenerator, new_token: str +): + """Return a test client generator."".""" + + async def auth_client() -> TestClient: + return await aiohttp_client( + hass.http.app, headers={"Authorization": f"Bearer {new_token}"} + ) + + return auth_client + + +@pytest.fixture(autouse=True) +async def setup_diag(hass): + """Set up diagnostics platform.""" + assert await async_setup_component(hass, "diagnostics", {}) + + +@freeze_time("2023-03-13 12:05:00-07:00") +async def test_diagnostics( + hass: HomeAssistant, + component_setup: ComponentSetup, + mock_events_list_items: Callable[[list[dict[str, Any]]], None], + hass_admin_user: MockUser, + hass_admin_credential: Credentials, + config_entry: MockConfigEntry, + aiohttp_client: ClientSessionGenerator, + socket_enabled: None, + snapshot: SnapshotAssertion, +) -> None: + """Test diagnostics for the calendar.""" + mock_events_list_items( + [ + { + **TEST_EVENT, + "id": "event-id-1", + "iCalUID": "event-id-1@google.com", + "start": {"dateTime": "2023-03-13 12:00:00-07:00"}, + "end": {"dateTime": "2023-03-13 12:30:00-07:00"}, + }, + { + **TEST_EVENT, + "id": "event-id-2", + "iCalUID": "event-id-2@google.com", + "summary": "All Day Event", + "start": {"date": "2022-10-08"}, + "end": {"date": "2022-10-09"}, + "recurrence": ["RRULE:FREQ=WEEKLY"], + }, + ] + ) + + assert await component_setup() + + # Since we are freezing time only when we enter this test, we need to + # manually create a new token and clients since the token created by + # the fixtures would not be valid. + new_token = await generate_new_hass_access_token( + hass, hass_admin_user, hass_admin_credential + ) + data = await get_diagnostics_for_config_entry( + hass, _get_test_client_generator(hass, aiohttp_client, new_token), config_entry + ) + assert data == snapshot