"""The tests for the emulated Hue component.""" from asyncio import AbstractEventLoop from http import HTTPStatus import json import unittest from unittest.mock import patch from aiohttp import web from aiohttp.test_utils import TestClient import defusedxml.ElementTree as ET import pytest from typing_extensions import Generator from homeassistant import setup from homeassistant.components import emulated_hue from homeassistant.components.emulated_hue import upnp from homeassistant.const import CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant from tests.common import get_test_instance_port from tests.typing import ClientSessionGenerator BRIDGE_SERVER_PORT = get_test_instance_port() class MockTransport: """Mock asyncio transport.""" def __init__(self) -> None: """Create a place to store the sends.""" self.sends = [] def sendto(self, response, addr): """Mock sendto.""" self.sends.append((response, addr)) @pytest.fixture def aiohttp_client( event_loop: AbstractEventLoop, aiohttp_client: ClientSessionGenerator, socket_enabled: None, ) -> ClientSessionGenerator: """Return aiohttp_client and allow opening sockets.""" return aiohttp_client @pytest.fixture def hue_client( aiohttp_client: ClientSessionGenerator, ) -> Generator[TestClient]: """Return a hue API client.""" app = web.Application() with unittest.mock.patch( "homeassistant.components.emulated_hue.web.Application", return_value=app ): async def client(): """Return an authenticated client.""" return await aiohttp_client(app) yield client async def setup_hue(hass: HomeAssistant) -> None: """Set up the emulated_hue integration.""" with patch( "homeassistant.components.emulated_hue.async_create_upnp_datagram_endpoint" ): assert await setup.async_setup_component( hass, emulated_hue.DOMAIN, {emulated_hue.DOMAIN: {emulated_hue.CONF_LISTEN_PORT: BRIDGE_SERVER_PORT}}, ) await hass.async_block_till_done() def test_upnp_discovery_basic() -> None: """Tests the UPnP basic discovery response.""" upnp_responder_protocol = upnp.UPNPResponderProtocol(None, None, "192.0.2.42", 8080) mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport # Original request emitted by the Hue Bridge v1 app. request = """M-SEARCH * HTTP/1.1 HOST:239.255.255.250:1900 ST:ssdp:all Man:"ssdp:discover" MX:3 """ encoded_request = request.replace("\n", "\r\n").encode("utf-8") upnp_responder_protocol.datagram_received(encoded_request, 1234) expected_response = """HTTP/1.1 200 OK CACHE-CONTROL: max-age=60 EXT: LOCATION: http://192.0.2.42:8080/description.xml SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 hue-bridgeid: 001788FFFE23BFC2 ST: urn:schemas-upnp-org:device:basic:1 USN: uuid:2f402f80-da50-11e1-9b23-001788255acc """ expected_send = expected_response.replace("\n", "\r\n").encode("utf-8") assert mock_transport.sends == [(expected_send, 1234)] def test_upnp_discovery_rootdevice() -> None: """Tests the UPnP rootdevice discovery response.""" upnp_responder_protocol = upnp.UPNPResponderProtocol(None, None, "192.0.2.42", 8080) mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport # Original request emitted by Busch-Jaeger free@home SysAP. request = """M-SEARCH * HTTP/1.1 HOST: 239.255.255.250:1900 MAN: "ssdp:discover" MX: 40 ST: upnp:rootdevice """ encoded_request = request.replace("\n", "\r\n").encode("utf-8") upnp_responder_protocol.datagram_received(encoded_request, 1234) expected_response = """HTTP/1.1 200 OK CACHE-CONTROL: max-age=60 EXT: LOCATION: http://192.0.2.42:8080/description.xml SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 hue-bridgeid: 001788FFFE23BFC2 ST: upnp:rootdevice USN: uuid:2f402f80-da50-11e1-9b23-001788255acc::upnp:rootdevice """ expected_send = expected_response.replace("\n", "\r\n").encode("utf-8") assert mock_transport.sends == [(expected_send, 1234)] def test_upnp_no_response() -> None: """Tests the UPnP does not response on an invalid request.""" upnp_responder_protocol = upnp.UPNPResponderProtocol(None, None, "192.0.2.42", 8080) mock_transport = MockTransport() upnp_responder_protocol.transport = mock_transport # Original request emitted by the Hue Bridge v1 app. request = """INVALID * HTTP/1.1 HOST:239.255.255.250:1900 ST:ssdp:all Man:"ssdp:discover" MX:3 """ encoded_request = request.replace("\n", "\r\n").encode("utf-8") upnp_responder_protocol.datagram_received(encoded_request, 1234) assert not mock_transport.sends async def test_description_xml(hass: HomeAssistant, hue_client) -> None: """Test the description.""" await setup_hue(hass) client = await hue_client() result = await client.get("/description.xml", timeout=5) assert result.status == HTTPStatus.OK assert "text/xml" in result.headers["content-type"] try: root = ET.fromstring(await result.text()) ns = {"s": "urn:schemas-upnp-org:device-1-0"} assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2" except Exception: # noqa: BLE001 pytest.fail("description.xml is not valid XML!") async def test_create_username(hass: HomeAssistant, hue_client) -> None: """Test the creation of an username.""" await setup_hue(hass) client = await hue_client() request_json = {"devicetype": "my_device"} result = await client.post("/api", data=json.dumps(request_json), timeout=5) assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = await result.json() success_json = resp_json[0] assert "success" in success_json assert "username" in success_json["success"] async def test_unauthorized_view(hass: HomeAssistant, hue_client) -> None: """Test unauthorized view.""" await setup_hue(hass) client = await hue_client() request_json = {"devicetype": "my_device"} result = await client.get( "/api/unauthorized", data=json.dumps(request_json), timeout=5 ) assert result.status == HTTPStatus.OK assert CONTENT_TYPE_JSON in result.headers["content-type"] resp_json = await result.json() assert len(resp_json) == 1 success_json = resp_json[0] assert len(success_json) == 1 assert "error" in success_json error_json = success_json["error"] assert len(error_json) == 3 assert "/" in error_json["address"] assert "unauthorized user" in error_json["description"] assert "1" in error_json["type"] async def test_valid_username_request(hass: HomeAssistant, hue_client) -> None: """Test request with a valid username.""" await setup_hue(hass) client = await hue_client() request_json = {"invalid_key": "my_device"} result = await client.post("/api", data=json.dumps(request_json), timeout=5) assert result.status == HTTPStatus.BAD_REQUEST