diff --git a/homeassistant/components/cloud/client.py b/homeassistant/components/cloud/client.py index a17f536db72..5a70948555d 100644 --- a/homeassistant/components/cloud/client.py +++ b/homeassistant/components/cloud/client.py @@ -19,7 +19,7 @@ from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.aiohttp import MockRequest from . import alexa_config, google_config, utils -from .const import DISPATCHER_REMOTE_UPDATE +from .const import DISPATCHER_REMOTE_UPDATE, DOMAIN from .prefs import CloudPreferences _LOGGER = logging.getLogger(__name__) @@ -182,6 +182,7 @@ class CloudClient(Interface): headers=payload["headers"], method=payload["method"], query_string=payload["query"], + mock_source=DOMAIN, ) response = await self._hass.components.webhook.async_handle_webhook( diff --git a/homeassistant/components/webhook/__init__.py b/homeassistant/components/webhook/__init__.py index 47358d5008e..99226cabaa7 100644 --- a/homeassistant/components/webhook/__init__.py +++ b/homeassistant/components/webhook/__init__.py @@ -12,6 +12,7 @@ from homeassistant.const import HTTP_OK from homeassistant.core import callback from homeassistant.helpers.network import get_url from homeassistant.loader import bind_hass +from homeassistant.util.aiohttp import MockRequest _LOGGER = logging.getLogger(__name__) @@ -76,9 +77,15 @@ async def async_handle_webhook(hass, webhook_id, request): # Always respond successfully to not give away if a hook exists or not. if webhook is None: - peer_ip = request[KEY_REAL_IP] + if isinstance(request, MockRequest): + received_from = request.mock_source + else: + received_from = request[KEY_REAL_IP] + _LOGGER.warning( - "Received message for unregistered webhook %s from %s", webhook_id, peer_ip + "Received message for unregistered webhook %s from %s", + webhook_id, + received_from, ) # Look at content to provide some context for received webhook # Limit to 64 chars to avoid flooding the log diff --git a/homeassistant/util/aiohttp.py b/homeassistant/util/aiohttp.py index d43929dd777..36cdc0f25e2 100644 --- a/homeassistant/util/aiohttp.py +++ b/homeassistant/util/aiohttp.py @@ -1,4 +1,5 @@ """Utilities to help with aiohttp.""" +import io import json from typing import Any, Dict, Optional from urllib.parse import parse_qsl @@ -8,12 +9,29 @@ from multidict import CIMultiDict, MultiDict from homeassistant.const import HTTP_OK +class MockStreamReader: + """Small mock to imitate stream reader.""" + + def __init__(self, content: bytes) -> None: + """Initialize mock stream reader.""" + self._content = io.BytesIO(content) + + async def read(self, byte_count: int = -1) -> bytes: + """Read bytes.""" + if byte_count == -1: + return self._content.read() + return self._content.read(byte_count) + + class MockRequest: """Mock an aiohttp request.""" + mock_source: Optional[str] = None + def __init__( self, content: bytes, + mock_source: str, method: str = "GET", status: int = HTTP_OK, headers: Optional[Dict[str, str]] = None, @@ -27,6 +45,7 @@ class MockRequest: self.headers: CIMultiDict[str] = CIMultiDict(headers or {}) self.query_string = query_string or "" self._content = content + self.mock_source = mock_source @property def query(self) -> "MultiDict[str]": @@ -38,6 +57,11 @@ class MockRequest: """Return the body as text.""" return self._content.decode("utf-8") + @property + def content(self) -> MockStreamReader: + """Return the body as text.""" + return MockStreamReader(self._content) + async def json(self) -> Any: """Return the body as JSON.""" return json.loads(self._text) diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 2ff7942f8dd..35c2ef69bb3 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -114,12 +114,14 @@ async def test_view(hass): """Test view.""" hass.config_entries.flow.async_init = AsyncMock() - request = aiohttp.MockRequest(b"", query_string="code=test_code") + request = aiohttp.MockRequest( + b"", query_string="code=test_code", mock_source="test" + ) request.app = {"hass": hass} view = config_flow.AmbiclimateAuthCallbackView() assert await view.get(request) == "OK!" - request = aiohttp.MockRequest(b"", query_string="") + request = aiohttp.MockRequest(b"", query_string="", mock_source="test") request.app = {"hass": hass} view = config_flow.AmbiclimateAuthCallbackView() assert await view.get(request) == "No code" diff --git a/tests/components/cloud/test_client.py b/tests/components/cloud/test_client.py index 21eb59ddc03..d0d9c4b25b7 100644 --- a/tests/components/cloud/test_client.py +++ b/tests/components/cloud/test_client.py @@ -141,7 +141,7 @@ async def test_handler_google_actions_disabled(hass, mock_cloud_fixture): assert resp["payload"]["errorCode"] == "deviceTurnedOff" -async def test_webhook_msg(hass): +async def test_webhook_msg(hass, caplog): """Test webhook msg.""" with patch("hass_nabucasa.Cloud.start"): setup = await async_setup_component(hass, "cloud", {"cloud": {}}) @@ -151,7 +151,14 @@ async def test_webhook_msg(hass): await cloud.client.prefs.async_initialize() await cloud.client.prefs.async_update( cloudhooks={ - "hello": {"webhook_id": "mock-webhook-id", "cloudhook_id": "mock-cloud-id"} + "mock-webhook-id": { + "webhook_id": "mock-webhook-id", + "cloudhook_id": "mock-cloud-id", + }, + "no-longere-existing": { + "webhook_id": "no-longere-existing", + "cloudhook_id": "mock-nonexisting-id", + }, } ) @@ -183,6 +190,31 @@ async def test_webhook_msg(hass): assert len(received) == 1 assert await received[0].json() == {"hello": "world"} + # Non existing webhook + caplog.clear() + + response = await cloud.client.async_webhook_message( + { + "cloudhook_id": "mock-nonexisting-id", + "body": '{"nonexisting": "payload"}', + "headers": {"content-type": "application/json"}, + "method": "POST", + "query": None, + } + ) + + assert response == { + "status": 200, + "body": None, + "headers": {"Content-Type": "application/octet-stream"}, + } + + assert ( + "Received message for unregistered webhook no-longere-existing from cloud" + in caplog.text + ) + assert '{"nonexisting": "payload"}' in caplog.text + async def test_google_config_expose_entity(hass, mock_cloud_setup, mock_cloud_login): """Test Google config exposing entity method uses latest config.""" diff --git a/tests/util/test_aiohttp.py b/tests/util/test_aiohttp.py index 870ed81c2e2..e7b5ef73c32 100644 --- a/tests/util/test_aiohttp.py +++ b/tests/util/test_aiohttp.py @@ -5,14 +5,14 @@ from homeassistant.util import aiohttp async def test_request_json(): """Test a JSON request.""" - request = aiohttp.MockRequest(b'{"hello": 2}') + request = aiohttp.MockRequest(b'{"hello": 2}', mock_source="test") assert request.status == 200 assert await request.json() == {"hello": 2} async def test_request_text(): """Test a JSON request.""" - request = aiohttp.MockRequest(b"hello", status=201) + request = aiohttp.MockRequest(b"hello", status=201, mock_source="test") assert request.status == 201 assert await request.text() == "hello" @@ -20,7 +20,7 @@ async def test_request_text(): async def test_request_post_query(): """Test a JSON request.""" request = aiohttp.MockRequest( - b"hello=2&post=true", query_string="get=true", method="POST" + b"hello=2&post=true", query_string="get=true", method="POST", mock_source="test" ) assert request.method == "POST" assert await request.post() == {"hello": "2", "post": "true"}