Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bump aiohttp to 3.10.6rc2 #126468

Merged
merged 20 commits into from
Sep 24, 2024
22 changes: 8 additions & 14 deletions homeassistant/components/media_player/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
import logging
import secrets
from typing import Any, Final, Required, TypedDict, final
from urllib.parse import quote, urlparse
from urllib.parse import urlparse

import aiohttp
from aiohttp import web
Expand Down Expand Up @@ -1187,12 +1187,10 @@ def get_browse_image_url(
"""Generate an url for a media browser image."""
url_path = (
f"/api/media_player_proxy/{self.entity_id}/browse_media"
# quote the media_content_id as it may contain url unsafe characters
bdraco marked this conversation as resolved.
Show resolved Hide resolved
# aiohttp will unquote the path automatically
f"/{media_content_type}/{quote(media_content_id)}"
f"/{media_content_type}"
)

url_query = {"token": self.access_token}
url_query = {"token": self.access_token, "media_content_id": media_content_id}
if media_image_id:
url_query["media_image_id"] = media_image_id

Expand All @@ -1205,11 +1203,7 @@ class MediaPlayerImageView(HomeAssistantView):
requires_auth = False
url = "/api/media_player_proxy/{entity_id}"
name = "api:media_player:image"
extra_urls = [
# Need to modify the default regex for media_content_id as it may
# include arbitrary characters including '/','{', or '}'
url + "/browse_media/{media_content_type}/{media_content_id:.+}",
]
extra_urls = [url + "/browse_media/{media_content_type}"]

def __init__(self, component: EntityComponent[MediaPlayerEntity]) -> None:
"""Initialize a media player view."""
Expand All @@ -1220,7 +1214,6 @@ async def get(
request: web.Request,
entity_id: str,
media_content_type: MediaType | str | None = None,
media_content_id: str | None = None,
) -> web.Response:
"""Start a get request."""
if (player := self.component.get_entity(entity_id)) is None:
Expand All @@ -1232,16 +1225,17 @@ async def get(
return web.Response(status=status)

assert isinstance(player, MediaPlayerEntity)
query = request.query
media_content_id = query.get("media_content_id")
authenticated = (
request[KEY_AUTHENTICATED]
or request.query.get("token") == player.access_token
request[KEY_AUTHENTICATED] or query.get("token") == player.access_token
)

if not authenticated:
return web.Response(status=HTTPStatus.UNAUTHORIZED)

if media_content_type and media_content_id:
media_image_id = request.query.get("media_image_id")
media_image_id = query.get("media_image_id")
data, content_type = await player.async_get_browse_image(
media_content_type, media_content_id, media_image_id
)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/package_constraints.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ aiodiscover==2.1.0
aiodns==3.2.0
aiohasupervisor==0.1.0b1
aiohttp-fast-zlib==0.1.1
aiohttp==3.10.5
aiohttp==3.10.6rc0
aiohttp_cors==0.7.0
aiozoneinfo==0.2.1
astral==2.2
Expand Down
21 changes: 20 additions & 1 deletion homeassistant/util/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,19 @@ async def read(self, byte_count: int = -1) -> bytes:
return self._content.read(byte_count)


class MockPayloadWriter:
"""Small mock to imitate payload writer."""

def enable_chunking(self) -> None:
"""Enable chunking."""

async def write_headers(self, *args: Any, **kwargs: Any) -> None:
"""Write headers."""


_MOCK_PAYLOAD_WRITER = MockPayloadWriter()


class MockRequest:
"""Mock an aiohttp request."""

Expand All @@ -49,8 +62,14 @@ def __init__(
self.status = status
self.headers: CIMultiDict[str] = CIMultiDict(headers or {})
self.query_string = query_string or ""
self.keep_alive = False
self.version = (1, 1)
self._content = content
self.mock_source = mock_source
self._payload_writer = _MOCK_PAYLOAD_WRITER

async def _prepare_hook(self, response: Any) -> None:
"""Prepare hook."""

@property
def query(self) -> MultiDict[str]:
Expand Down Expand Up @@ -90,7 +109,7 @@ def serialize_response(response: web.Response) -> dict[str, Any]:
if (body := response.body) is None:
body_decoded = None
elif isinstance(body, payload.StringPayload):
body_decoded = body._value.decode(body.encoding) # noqa: SLF001
body_decoded = body._value.decode(body.encoding or "utf-8") # noqa: SLF001
elif isinstance(body, bytes):
body_decoded = body.decode(response.charset or "utf-8")
else:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ dependencies = [
# Integrations may depend on hassio integration without listing it to
# change behavior based on presence of supervisor
"aiohasupervisor==0.1.0b1",
"aiohttp==3.10.5",
"aiohttp==3.10.6rc0",
"aiohttp_cors==0.7.0",
"aiohttp-fast-zlib==0.1.1",
"aiozoneinfo==0.2.1",
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
# Home Assistant Core
aiodns==3.2.0
aiohasupervisor==0.1.0b1
aiohttp==3.10.5
aiohttp==3.10.6rc0
aiohttp_cors==0.7.0
aiohttp-fast-zlib==0.1.1
aiozoneinfo==0.2.1
Expand Down
8 changes: 4 additions & 4 deletions tests/components/hassio/test_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ async def test_forward_request_onboarded_user_unallowed_methods(
("bad_path", "expected_status"),
[
# Caught by bullshit filter
("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
("app/%252E./entrypoint.js", HTTPStatus.INTERNAL_SERVER_ERROR),
bdraco marked this conversation as resolved.
Show resolved Hide resolved
# The .. is processed, making it an unauthenticated path
("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
Expand Down Expand Up @@ -145,7 +145,7 @@ async def test_forward_request_onboarded_noauth_unallowed_methods(
("bad_path", "expected_status"),
[
# Caught by bullshit filter
("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
("app/%252E./entrypoint.js", HTTPStatus.INTERNAL_SERVER_ERROR),
# The .. is processed, making it an unauthenticated path
("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
Expand Down Expand Up @@ -258,7 +258,7 @@ async def test_forward_request_not_onboarded_unallowed_methods(
("bad_path", "expected_status"),
[
# Caught by bullshit filter
("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
("app/%252E./entrypoint.js", HTTPStatus.INTERNAL_SERVER_ERROR),
# The .. is processed, making it an unauthenticated path
("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
Expand Down Expand Up @@ -374,7 +374,7 @@ async def test_forward_request_admin_unallowed_methods(
("bad_path", "expected_status"),
[
# Caught by bullshit filter
("app/%252E./entrypoint.js", HTTPStatus.BAD_REQUEST),
("app/%252E./entrypoint.js", HTTPStatus.INTERNAL_SERVER_ERROR),
# The .. is processed, making it an unauthenticated path
("app/../entrypoint.js", HTTPStatus.UNAUTHORIZED),
("app/%2E%2E/entrypoint.js", HTTPStatus.UNAUTHORIZED),
Expand Down
22 changes: 8 additions & 14 deletions tests/components/motioneye/test_camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from asyncio import AbstractEventLoop
from collections.abc import Callable
import copy
from typing import cast
from unittest.mock import AsyncMock, Mock, call

from aiohttp import web
Expand Down Expand Up @@ -46,6 +45,7 @@
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.util.aiohttp import MockRequest
import homeassistant.util.dt as dt_util

from . import (
Expand Down Expand Up @@ -231,7 +231,7 @@ async def test_get_still_image_from_camera(
) -> None:
"""Test getting a still image."""

image_handler = AsyncMock(return_value="")
image_handler = AsyncMock(return_value=web.Response(body=""))
bdraco marked this conversation as resolved.
Show resolved Hide resolved

app = web.Application()
app.add_routes(
Expand Down Expand Up @@ -273,7 +273,8 @@ async def test_get_stream_from_camera(
) -> None:
"""Test getting a stream."""

stream_handler = AsyncMock(return_value="")
stream_handler = AsyncMock(return_value=web.Response(body=""))

app = web.Application()
app.add_routes([web.get("/", stream_handler)])
stream_server = await aiohttp_server(app)
Expand All @@ -297,12 +298,7 @@ async def test_get_stream_from_camera(
)
await hass.async_block_till_done()

# It won't actually get a stream from the dummy handler, so just catch
# the expected exception, then verify the right handler was called.
with pytest.raises(HTTPBadGateway):
await async_get_mjpeg_stream(
hass, cast(web.Request, None), TEST_CAMERA_ENTITY_ID
)
await async_get_mjpeg_stream(hass, MockRequest(b"", "test"), TEST_CAMERA_ENTITY_ID)
assert stream_handler.called


Expand Down Expand Up @@ -358,7 +354,8 @@ async def test_camera_option_stream_url_template(
"""Verify camera with a stream URL template option."""
client = create_mock_motioneye_client()

stream_handler = AsyncMock(return_value="")
stream_handler = AsyncMock(return_value=web.Response(body=""))

app = web.Application()
app.add_routes([web.get(f"/{TEST_CAMERA_NAME}/{TEST_CAMERA_ID}", stream_handler)])
stream_server = await aiohttp_server(app)
Expand All @@ -384,10 +381,7 @@ async def test_camera_option_stream_url_template(
)
await hass.async_block_till_done()

# It won't actually get a stream from the dummy handler, so just catch
# the expected exception, then verify the right handler was called.
with pytest.raises(HTTPBadGateway):
await async_get_mjpeg_stream(hass, Mock(), TEST_CAMERA_ENTITY_ID)
await async_get_mjpeg_stream(hass, MockRequest(b"", "test"), TEST_CAMERA_ENTITY_ID)
assert AsyncMock.called
assert not client.get_camera_stream_url.called

Expand Down
15 changes: 14 additions & 1 deletion tests/test_util/aiohttp.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from contextlib import contextmanager
from http import HTTPStatus
import re
from types import TracebackType
from typing import Any
from unittest import mock
from urllib.parse import parse_qs
Expand Down Expand Up @@ -166,7 +167,7 @@ class AiohttpClientMockResponse:
def __init__(
self,
method,
url,
url: URL,
status=HTTPStatus.OK,
response=None,
json=None,
Expand Down Expand Up @@ -297,6 +298,18 @@ def response(self):
raise ClientConnectionError("Connection closed")
return self._response

async def __aenter__(self):
"""Enter the context manager."""
return self

async def __aexit__(
self,
exc_type: type[BaseException] | None,
exc_val: BaseException | None,
exc_tb: TracebackType | None,
) -> None:
"""Exit the context manager."""


@contextmanager
def mock_aiohttp_client() -> Iterator[AiohttpClientMocker]:
Expand Down
Loading