Skip to content

Commit

Permalink
Add async_register_backup_agents_listener to cloud/backup (#133584)
Browse files Browse the repository at this point in the history
* Add async_register_backup_agents_listener to cloud/backup

* Coverage

* more coverage
  • Loading branch information
ludeeus authored Dec 20, 2024
1 parent ad34bc8 commit 10191e7
Show file tree
Hide file tree
Showing 5 changed files with 126 additions and 2 deletions.
30 changes: 28 additions & 2 deletions homeassistant/components/cloud/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from __future__ import annotations

import base64
from collections.abc import AsyncIterator, Callable, Coroutine
from collections.abc import AsyncIterator, Callable, Coroutine, Mapping
import hashlib
from typing import Any, Self

Expand All @@ -18,9 +18,10 @@

from homeassistant.components.backup import AgentBackup, BackupAgent, BackupAgentError
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect

from .client import CloudClient
from .const import DATA_CLOUD, DOMAIN
from .const import DATA_CLOUD, DOMAIN, EVENT_CLOUD_EVENT

_STORAGE_BACKUP = "backup"

Expand All @@ -45,6 +46,31 @@ async def async_get_backup_agents(
return [CloudBackupAgent(hass=hass, cloud=cloud)]


@callback
def async_register_backup_agents_listener(
hass: HomeAssistant,
*,
listener: Callable[[], None],
**kwargs: Any,
) -> Callable[[], None]:
"""Register a listener to be called when agents are added or removed."""

@callback
def unsub() -> None:
"""Unsubscribe from events."""
unsub_signal()

@callback
def handle_event(data: Mapping[str, Any]) -> None:
"""Handle event."""
if data["type"] not in ("login", "logout"):
return
listener()

unsub_signal = async_dispatcher_connect(hass, EVENT_CLOUD_EVENT, handle_event)
return unsub


class ChunkAsyncStreamIterator:
"""Async iterator for chunked streams.
Expand Down
2 changes: 2 additions & 0 deletions homeassistant/components/cloud/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
DATA_PLATFORMS_SETUP: HassKey[dict[str, asyncio.Event]] = HassKey(
"cloud_platforms_setup"
)
EVENT_CLOUD_EVENT = "cloud_event"

REQUEST_TIMEOUT = 10

PREF_ENABLE_ALEXA = "alexa_enabled"
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/cloud/http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,15 @@
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.util.location import async_detect_location_info

from .alexa_config import entity_supported as entity_supported_by_alexa
from .assist_pipeline import async_create_cloud_pipeline
from .client import CloudClient
from .const import (
DATA_CLOUD,
EVENT_CLOUD_EVENT,
LOGIN_MFA_TIMEOUT,
PREF_ALEXA_REPORT_STATE,
PREF_DISABLE_2FA,
Expand Down Expand Up @@ -278,6 +280,8 @@ async def post(self, request: web.Request, data: dict[str, Any]) -> web.Response
new_cloud_pipeline_id = await async_create_cloud_pipeline(hass)
else:
new_cloud_pipeline_id = None

async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "login"})
return self.json({"success": True, "cloud_pipeline": new_cloud_pipeline_id})


Expand All @@ -297,6 +301,7 @@ async def post(self, request: web.Request) -> web.Response:
async with asyncio.timeout(REQUEST_TIMEOUT):
await cloud.logout()

async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "logout"})
return self.json_message("ok")


Expand Down
49 changes: 49 additions & 0 deletions tests/components/cloud/test_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@
Folder,
)
from homeassistant.components.cloud import DOMAIN
from homeassistant.components.cloud.backup import async_register_backup_agents_listener
from homeassistant.components.cloud.const import EVENT_CLOUD_EVENT
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.setup import async_setup_component

from tests.test_util.aiohttp import AiohttpClientMocker
Expand Down Expand Up @@ -576,3 +579,49 @@ async def test_agents_delete_not_found(

assert response["success"]
assert response["result"] == {"agent_errors": {}}


@pytest.mark.parametrize("event_type", ["login", "logout"])
async def test_calling_listener_on_login_logout(
hass: HomeAssistant,
event_type: str,
) -> None:
"""Test calling listener for login and logout events."""
listener = MagicMock()
async_register_backup_agents_listener(hass, listener=listener)

assert listener.call_count == 0
async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": event_type})
await hass.async_block_till_done()

assert listener.call_count == 1


async def test_not_calling_listener_after_unsub(hass: HomeAssistant) -> None:
"""Test only calling listener until unsub."""
listener = MagicMock()
unsub = async_register_backup_agents_listener(hass, listener=listener)

assert listener.call_count == 0
async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "login"})
await hass.async_block_till_done()
assert listener.call_count == 1

unsub()

async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "login"})
await hass.async_block_till_done()
assert listener.call_count == 1


async def test_not_calling_listener_with_unknown_event_type(
hass: HomeAssistant,
) -> None:
"""Test not calling listener if we did not get the expected event type."""
listener = MagicMock()
async_register_backup_agents_listener(hass, listener=listener)

assert listener.call_count == 0
async_dispatcher_send(hass, EVENT_CLOUD_EVENT, {"type": "unknown"})
await hass.async_block_till_done()
assert listener.call_count == 0
42 changes: 42 additions & 0 deletions tests/components/cloud/test_http_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1819,3 +1819,45 @@ async def test_api_calls_require_admin(
resp = await client.post(endpoint, json=data)

assert resp.status == HTTPStatus.UNAUTHORIZED


async def test_login_view_dispatch_event(
hass: HomeAssistant,
cloud: MagicMock,
hass_client: ClientSessionGenerator,
) -> None:
"""Test dispatching event while logging in."""
assert await async_setup_component(hass, "homeassistant", {})
assert await async_setup_component(hass, DOMAIN, {"cloud": {}})
await hass.async_block_till_done()

cloud_client = await hass_client()

with patch(
"homeassistant.components.cloud.http_api.async_dispatcher_send"
) as async_dispatcher_send_mock:
await cloud_client.post(
"/api/cloud/login", json={"email": "my_username", "password": "my_password"}
)

assert async_dispatcher_send_mock.call_count == 1
assert async_dispatcher_send_mock.mock_calls[0][1][1] == "cloud_event"
assert async_dispatcher_send_mock.mock_calls[0][1][2] == {"type": "login"}


async def test_logout_view_dispatch_event(
cloud: MagicMock,
setup_cloud: None,
hass_client: ClientSessionGenerator,
) -> None:
"""Test dispatching event while logging out."""
cloud_client = await hass_client()

with patch(
"homeassistant.components.cloud.http_api.async_dispatcher_send"
) as async_dispatcher_send_mock:
await cloud_client.post("/api/cloud/logout")

assert async_dispatcher_send_mock.call_count == 1
assert async_dispatcher_send_mock.mock_calls[0][1][1] == "cloud_event"
assert async_dispatcher_send_mock.mock_calls[0][1][2] == {"type": "logout"}

0 comments on commit 10191e7

Please sign in to comment.