diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index 073974200a0..e8d04b1596d 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -20,6 +20,7 @@ from .auth import async_setup_auth from .discovery import async_setup_discovery from .handler import HassIO, HassioAPIError from .http import HassIOView +from .ingress import async_setup_ingress _LOGGER = logging.getLogger(__name__) @@ -270,4 +271,7 @@ async def async_setup(hass, config): # Init auth Hass.io feature async_setup_auth(hass) + # Init ingress Hass.io feature + async_setup_ingress(hass, host) + return True diff --git a/homeassistant/components/hassio/const.py b/homeassistant/components/hassio/const.py index 964f94bfb41..e4132562c31 100644 --- a/homeassistant/components/hassio/const.py +++ b/homeassistant/components/hassio/const.py @@ -9,6 +9,7 @@ ATTR_UUID = 'uuid' ATTR_USERNAME = 'username' ATTR_PASSWORD = 'password' -X_HASSIO = 'X-HASSIO-KEY' -X_HASS_USER_ID = 'X-HASS-USER-ID' -X_HASS_IS_ADMIN = 'X-HASS-IS-ADMIN' +X_HASSIO = 'X-Hassio-Key' +X_INGRESS_PATH = "X-Ingress-Path" +X_HASS_USER_ID = 'X-Hass-User-ID' +X_HASS_IS_ADMIN = 'X-Hass-Is-Admin' diff --git a/homeassistant/components/hassio/http.py b/homeassistant/components/hassio/http.py index 01ded9ca11d..7284004d72f 100644 --- a/homeassistant/components/hassio/http.py +++ b/homeassistant/components/hassio/http.py @@ -3,10 +3,11 @@ import asyncio import logging import os import re +from typing import Dict, Union import aiohttp from aiohttp import web -from aiohttp.hdrs import CONTENT_TYPE +from aiohttp.hdrs import CONTENT_TYPE, CONTENT_LENGTH from aiohttp.web_exceptions import HTTPBadGateway import async_timeout @@ -20,7 +21,8 @@ _LOGGER = logging.getLogger(__name__) NO_TIMEOUT = re.compile( r'^(?:' r'|homeassistant/update' - r'|host/update' + r'|hassos/update' + r'|hassos/update/cli' r'|supervisor/update' r'|addons/[^/]+/(?:update|install|rebuild)' r'|snapshots/.+/full' @@ -44,25 +46,26 @@ class HassIOView(HomeAssistantView): url = "/api/hassio/{path:.+}" requires_auth = False - def __init__(self, host, websession): + def __init__(self, host: str, websession: aiohttp.ClientSession): """Initialize a Hass.io base view.""" self._host = host self._websession = websession - async def _handle(self, request, path): + async def _handle( + self, request: web.Request, path: str + ) -> Union[web.Response, web.StreamResponse]: """Route data to Hass.io.""" if _need_auth(path) and not request[KEY_AUTHENTICATED]: return web.Response(status=401) - client = await self._command_proxy(path, request) - - data = await client.read() - return _create_response(client, data) + return await self._command_proxy(path, request) get = _handle post = _handle - async def _command_proxy(self, path, request): + async def _command_proxy( + self, path: str, request: web.Request + ) -> Union[web.Response, web.StreamResponse]: """Return a client request with proxy origin for Hass.io supervisor. This method is a coroutine. @@ -71,29 +74,38 @@ class HassIOView(HomeAssistantView): hass = request.app['hass'] data = None - headers = { - X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), - } - user = request.get('hass_user') - if user is not None: - headers[X_HASS_USER_ID] = request['hass_user'].id - headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + headers = _init_header(request) try: with async_timeout.timeout(10, loop=hass.loop): data = await request.read() - if data: - headers[CONTENT_TYPE] = request.content_type - else: - data = None method = getattr(self._websession, request.method.lower()) client = await method( "http://{}/{}".format(self._host, path), data=data, headers=headers, timeout=read_timeout ) + print(client.headers) - return client + # Simple request + if int(client.headers.get(CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await client.read() + return web.Response( + content_type=client.content_type, + status=client.status, + body=body, + ) + + # Stream response + response = web.StreamResponse(status=client.status) + response.content_type = client.content_type + + await response.prepare(request) + async for data in client.content.iter_chunked(4096): + await response.write(data) + + return response except aiohttp.ClientError as err: _LOGGER.error("Client error on api %s request %s", path, err) @@ -104,23 +116,30 @@ class HassIOView(HomeAssistantView): raise HTTPBadGateway() -def _create_response(client, data): - """Convert a response from client request.""" - return web.Response( - body=data, - status=client.status, - content_type=client.content_type, - ) +def _init_header(request: web.Request) -> Dict[str, str]: + """Create initial header.""" + headers = { + X_HASSIO: os.environ.get('HASSIO_TOKEN', ""), + CONTENT_TYPE: request.content_type, + } + + # Add user data + user = request.get('hass_user') + if user is not None: + headers[X_HASS_USER_ID] = request['hass_user'].id + headers[X_HASS_IS_ADMIN] = str(int(request['hass_user'].is_admin)) + + return headers -def _get_timeout(path): +def _get_timeout(path: str) -> int: """Return timeout for a URL path.""" if NO_TIMEOUT.match(path): return 0 return 300 -def _need_auth(path): +def _need_auth(path: str) -> bool: """Return if a path need authentication.""" if NO_AUTH.match(path): return False diff --git a/homeassistant/components/hassio/ingress.py b/homeassistant/components/hassio/ingress.py new file mode 100644 index 00000000000..6c1ef389712 --- /dev/null +++ b/homeassistant/components/hassio/ingress.py @@ -0,0 +1,210 @@ +"""Hass.io Add-on ingress service.""" +import asyncio +from ipaddress import ip_address +import os +from typing import Dict, Union + +import aiohttp +from aiohttp import web +from aiohttp import hdrs +from aiohttp.web_exceptions import HTTPBadGateway +from multidict import CIMultiDict + +from homeassistant.core import callback +from homeassistant.components.http import HomeAssistantView +from homeassistant.helpers.typing import HomeAssistantType + +from .const import X_HASSIO, X_INGRESS_PATH + + +@callback +def async_setup_ingress(hass: HomeAssistantType, host: str): + """Auth setup.""" + websession = hass.helpers.aiohttp_client.async_get_clientsession() + + hassio_ingress = HassIOIngress(host, websession) + hass.http.register_view(hassio_ingress) + + +class HassIOIngress(HomeAssistantView): + """Hass.io view to handle base part.""" + + name = "api:hassio:ingress" + url = "/api/hassio_ingress/{addon}/{path:.+}" + requires_auth = False + + def __init__(self, host: str, websession: aiohttp.ClientSession): + """Initialize a Hass.io ingress view.""" + self._host = host + self._websession = websession + + def _create_url(self, addon: str, path: str) -> str: + """Create URL to service.""" + return "http://{}/addons/{}/web/{}".format(self._host, addon, path) + + async def _handle( + self, request: web.Request, addon: str, path: str + ) -> Union[web.Response, web.StreamResponse, web.WebSocketResponse]: + """Route data to Hass.io ingress service.""" + try: + # Websocket + if _is_websocket(request): + return await self._handle_websocket(request, addon, path) + + # Request + return await self._handle_request(request, addon, path) + + except aiohttp.ClientError: + pass + + raise HTTPBadGateway() from None + + get = _handle + post = _handle + put = _handle + delete = _handle + + async def _handle_websocket( + self, request: web.Request, addon: str, path: str + ) -> web.WebSocketResponse: + """Ingress route for websocket.""" + ws_server = web.WebSocketResponse() + await ws_server.prepare(request) + + url = self._create_url(addon, path) + source_header = _init_header(request, addon) + + # Start proxy + async with self._websession.ws_connect( + url, headers=source_header + ) as ws_client: + # Proxy requests + await asyncio.wait( + [ + _websocket_forward(ws_server, ws_client), + _websocket_forward(ws_client, ws_server), + ], + return_when=asyncio.FIRST_COMPLETED + ) + + return ws_server + + async def _handle_request( + self, request: web.Request, addon: str, path: str + ) -> Union[web.Response, web.StreamResponse]: + """Ingress route for request.""" + url = self._create_url(addon, path) + data = await request.read() + source_header = _init_header(request, addon) + + async with self._websession.request( + request.method, url, headers=source_header, + params=request.query, data=data, cookies=request.cookies + ) as result: + headers = _response_header(result) + + # Simple request + if hdrs.CONTENT_LENGTH in result.headers and \ + int(result.headers.get(hdrs.CONTENT_LENGTH, 0)) < 4194000: + # Return Response + body = await result.read() + return web.Response( + headers=headers, + status=result.status, + body=body + ) + + # Stream response + response = web.StreamResponse( + status=result.status, headers=headers) + response.content_type = result.content_type + + try: + await response.prepare(request) + async for data in result.content: + await response.write(data) + + except (aiohttp.ClientError, aiohttp.ClientPayloadError): + pass + + return response + + +def _init_header( + request: web.Request, addon: str +) -> Union[CIMultiDict, Dict[str, str]]: + """Create initial header.""" + headers = {} + + # filter flags + for name, value in request.headers.items(): + if name in (hdrs.CONTENT_LENGTH, hdrs.CONTENT_TYPE): + continue + headers[name] = value + + # Inject token / cleanup later on Supervisor + headers[X_HASSIO] = os.environ.get('HASSIO_TOKEN', "") + + # Ingress information + headers[X_INGRESS_PATH] = "/api/hassio_ingress/{}".format(addon) + + # Set X-Forwarded-For + forward_for = request.headers.get(hdrs.X_FORWARDED_FOR) + connected_ip = ip_address(request.transport.get_extra_info('peername')[0]) + if forward_for: + forward_for = "{}, {!s}".format(forward_for, connected_ip) + else: + forward_for = "{!s}".format(connected_ip) + headers[hdrs.X_FORWARDED_FOR] = forward_for + + # Set X-Forwarded-Host + forward_host = request.headers.get(hdrs.X_FORWARDED_HOST) + if not forward_host: + forward_host = request.host + headers[hdrs.X_FORWARDED_HOST] = forward_host + + # Set X-Forwarded-Proto + forward_proto = request.headers.get(hdrs.X_FORWARDED_PROTO) + if not forward_proto: + forward_proto = request.url.scheme + headers[hdrs.X_FORWARDED_PROTO] = forward_proto + + return headers + + +def _response_header(response: aiohttp.ClientResponse) -> Dict[str, str]: + """Create response header.""" + headers = {} + + for name, value in response.headers.items(): + if name in (hdrs.TRANSFER_ENCODING, hdrs.CONTENT_LENGTH, + hdrs.CONTENT_TYPE): + continue + headers[name] = value + + return headers + + +def _is_websocket(request: web.Request) -> bool: + """Return True if request is a websocket.""" + headers = request.headers + + if headers.get(hdrs.CONNECTION) == "Upgrade" and \ + headers.get(hdrs.UPGRADE) == "websocket": + return True + return False + + +async def _websocket_forward(ws_from, ws_to): + """Handle websocket message directly.""" + async for msg in ws_from: + if msg.type == aiohttp.WSMsgType.TEXT: + await ws_to.send_str(msg.data) + elif msg.type == aiohttp.WSMsgType.BINARY: + await ws_to.send_bytes(msg.data) + elif msg.type == aiohttp.WSMsgType.PING: + await ws_to.ping() + elif msg.type == aiohttp.WSMsgType.PONG: + await ws_to.pong() + elif ws_to.closed: + await ws_to.close(code=ws_to.close_code, message=msg.extra) diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index 3f58c6e697e..3a58048735b 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,29 +1,22 @@ """The tests for the hassio component.""" import asyncio -from unittest.mock import patch, Mock, MagicMock +from unittest.mock import patch import pytest from homeassistant.const import HTTP_HEADER_HA_AUTH -from tests.common import mock_coro from . import API_PASSWORD @asyncio.coroutine -def test_forward_request(hassio_client): +def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.post("http://127.0.0.1/beer", text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http' - '._create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.post('/api/hassio/beer', headers={ - HTTP_HEADER_HA_AUTH: API_PASSWORD - }) + resp = yield from hassio_client.post('/api/hassio/beer', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) # Check we got right response assert resp.status == 200 @@ -31,8 +24,7 @@ def test_forward_request(hassio_client): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine @@ -55,18 +47,13 @@ def test_auth_required_forward_request(hassio_noauth_client, build_type): 'app/index.html', 'app/hassio-app.html', 'app/index.html', 'app/hassio-app.html', 'app/some-chunk.js', 'app/app.js', ]) -def test_forward_request_no_auth_for_panel(hassio_client, build_type): +def test_forward_request_no_auth_for_panel( + hassio_client, build_type, aioclient_mock): """Test no auth needed for .""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/{}".format(build_type), text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.get( - '/api/hassio/{}'.format(build_type)) + resp = yield from hassio_client.get('/api/hassio/{}'.format(build_type)) # Check we got right response assert resp.status == 200 @@ -74,22 +61,16 @@ def test_forward_request_no_auth_for_panel(hassio_client, build_type): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine -def test_forward_request_no_auth_for_logo(hassio_client): +def test_forward_request_no_auth_for_logo(hassio_client, aioclient_mock): """Test no auth needed for .""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/addons/bl_b392/logo", text="response") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = 'response' - resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo') + resp = yield from hassio_client.get('/api/hassio/addons/bl_b392/logo') # Check we got right response assert resp.status == 200 @@ -97,24 +78,18 @@ def test_forward_request_no_auth_for_logo(hassio_client): assert body == 'response' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine -def test_forward_log_request(hassio_client): +def test_forward_log_request(hassio_client, aioclient_mock): """Test fetching normal log path doesn't remove ANSI color escape codes.""" - response = MagicMock() - response.read.return_value = mock_coro('data') + aioclient_mock.get( + "http://127.0.0.1/beer/logs", text="\033[32mresponse\033[0m") - with patch('homeassistant.components.hassio.HassIOView._command_proxy', - Mock(return_value=mock_coro(response))), \ - patch('homeassistant.components.hassio.http.' - '_create_response') as mresp: - mresp.return_value = '\033[32mresponse\033[0m' - resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={ - HTTP_HEADER_HA_AUTH: API_PASSWORD - }) + resp = yield from hassio_client.get('/api/hassio/beer/logs', headers={ + HTTP_HEADER_HA_AUTH: API_PASSWORD + }) # Check we got right response assert resp.status == 200 @@ -122,8 +97,7 @@ def test_forward_log_request(hassio_client): assert body == '\033[32mresponse\033[0m' # Check we forwarded command - assert len(mresp.mock_calls) == 1 - assert mresp.mock_calls[0][1] == (response, 'data') + assert len(aioclient_mock.mock_calls) == 1 @asyncio.coroutine @@ -151,5 +125,5 @@ async def test_forwarding_user_info(hassio_client, hass_admin_user, assert len(aioclient_mock.mock_calls) == 1 req_headers = aioclient_mock.mock_calls[0][-1] - req_headers['X-HASS-USER-ID'] == hass_admin_user.id - req_headers['X-HASS-IS-ADMIN'] == '1' + req_headers['X-Hass-User-ID'] == hass_admin_user.id + req_headers['X-Hass-Is-Admin'] == '1' diff --git a/tests/components/hassio/test_ingress.py b/tests/components/hassio/test_ingress.py new file mode 100644 index 00000000000..4e071ba24fd --- /dev/null +++ b/tests/components/hassio/test_ingress.py @@ -0,0 +1,162 @@ +"""The tests for the hassio component.""" + +from aiohttp.hdrs import X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO +from aiohttp.client_exceptions import WSServerHandshakeError +import pytest + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_get( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.get( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_post( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.post("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.post( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_put( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.put("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.put( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ping?index=1"), ("core", "index.html"), + ("local", "panel/config"), ("jk_921", "editor.php?idx=3&ping=5") + ]) +async def test_ingress_request_delete( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.delete("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1]), text="test") + + resp = await hassio_client.delete( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we got right response + assert resp.status == 200 + body = await resp.text() + assert body == "test" + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] + + +@pytest.mark.parametrize( + 'build_type', [ + ("a3_vl", "test/beer/ws"), ("core", "ws.php"), + ("local", "panel/config/stream"), ("jk_921", "hulk") + ]) +async def test_ingress_websocket( + hassio_client, build_type, aioclient_mock): + """Test no auth needed for .""" + aioclient_mock.get("http://127.0.0.1/addons/{}/web/{}".format( + build_type[0], build_type[1])) + + # Ignore error because we can setup a full IO infrastructure + with pytest.raises(WSServerHandshakeError): + await hassio_client.ws_connect( + '/api/hassio_ingress/{}/{}'.format(build_type[0], build_type[1]), + headers={"X-Test-Header": "beer"} + ) + + # Check we forwarded command + assert len(aioclient_mock.mock_calls) == 1 + assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" + assert aioclient_mock.mock_calls[-1][3]["X-Ingress-Path"] == \ + "/api/hassio_ingress/{}".format(build_type[0]) + assert aioclient_mock.mock_calls[-1][3]["X-Test-Header"] == "beer" + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_FOR] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_HOST] + assert aioclient_mock.mock_calls[-1][3][X_FORWARDED_PROTO] diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index fc4661e7544..f1f148f8495 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -207,7 +207,7 @@ def test_setup_hassio_no_additional_data(hass, aioclient_mock): assert result assert aioclient_mock.call_count == 3 - assert aioclient_mock.mock_calls[-1][3]['X-HASSIO-KEY'] == "123456" + assert aioclient_mock.mock_calls[-1][3]['X-Hassio-Key'] == "123456" @asyncio.coroutine diff --git a/tests/test_util/aiohttp.py b/tests/test_util/aiohttp.py index 8b3b057bfc0..ab759f03058 100644 --- a/tests/test_util/aiohttp.py +++ b/tests/test_util/aiohttp.py @@ -102,7 +102,7 @@ class AiohttpClientMocker: async def match_request(self, method, url, *, data=None, auth=None, params=None, headers=None, allow_redirects=None, - timeout=None, json=None): + timeout=None, json=None, cookies=None): """Match a request against pre-registered requests.""" data = data or json url = URL(url)