core/tests/components/emulated_hue/test_upnp.py

222 lines
6.5 KiB
Python
Raw Normal View History

"""The tests for the emulated Hue component."""
from http import HTTPStatus
import json
import unittest
from unittest.mock import patch
from aiohttp import web
import defusedxml.ElementTree as ET
import pytest
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 tests.common import get_test_instance_port
BRIDGE_SERVER_PORT = get_test_instance_port()
class MockTransport:
"""Mock asyncio transport."""
def __init__(self):
"""Create a place to store the sends."""
self.sends = []
def sendto(self, response, addr):
"""Mock sendto."""
self.sends.append((response, addr))
@pytest.fixture
Upgrade pytest-aiohttp (#82475) * Upgrade pytest-aiohttp * Make sure executors, tasks and timers are closed Some test will trigger warnings on garbage collect, these warnings spills over into next test. Some test trigger tasks that raise errors on shutdown, these spill over into next test. This is to mimic older pytest-aiohttp and it's behaviour on test cleanup. Discussions on similar changes for pytest-aiohttp are here: https://github.com/pytest-dev/pytest-asyncio/pull/309 * Replace loop with event_loop * Make sure time is frozen for tests * Make sure the ConditionType is not async /home-assistant/homeassistant/helpers/template.py:2082: RuntimeWarning: coroutine 'AsyncMockMixin._execute_mock_call' was never awaited def wrapper(*args, **kwargs): Enable tracemalloc to get traceback where the object was allocated. See https://docs.pytest.org/en/stable/how-to/capture-warnings.html#resource-warnings for more info. * Increase litejet press tests with a factor 10 The times are simulated anyway, and we can't stop the normal event from occuring. * Use async handlers for aiohttp tests/components/motioneye/test_camera.py::test_get_still_image_from_camera tests/components/motioneye/test_camera.py::test_get_still_image_from_camera tests/components/motioneye/test_camera.py::test_get_stream_from_camera tests/components/motioneye/test_camera.py::test_get_stream_from_camera tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template tests/components/motioneye/test_camera.py::test_camera_option_stream_url_template /Users/joakim/src/hass/home-assistant/venv/lib/python3.9/site-packages/aiohttp/web_urldispatcher.py:189: DeprecationWarning: Bare functions are deprecated, use async ones warnings.warn( * Switch to freezegun in modbus tests The tests allowed clock to tick in between steps * Make sure skybell object are fully mocked Old tests would trigger attempts to post to could services: ``` DEBUG:aioskybell:HTTP post https://cloud.myskybell.com/api/v3/login/ Request with headers: {'content-type': 'application/json', 'accept': '*/*', 'x-skybell-app-id': 'd2b542c7-a7e4-4e1e-b77d-2b76911c7c46', 'x-skybell-client-id': '1f36a3c0-6dee-4997-a6db-4e1c67338e57'} ``` * Fix sorting that broke after rebase
2022-11-29 21:36:36 +00:00
def aiohttp_client(event_loop, aiohttp_client, socket_enabled):
"""Return aiohttp_client and allow opening sockets."""
return aiohttp_client
@pytest.fixture
def hue_client(aiohttp_client):
"""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):
"""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 mock_transport.sends == []
async def test_description_xml(hass, hue_client):
"""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"
2022-06-02 11:58:04 +00:00
except Exception: # pylint: disable=broad-except
pytest.fail("description.xml is not valid XML!")
async def test_create_username(hass, hue_client):
"""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, hue_client):
"""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, hue_client):
"""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