diff --git a/requirements_test.txt b/requirements_test.txt index 22ffaa60a0f..713c8820bc1 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -18,6 +18,7 @@ pipdeptree==2.1.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 pytest-cov==2.12.1 +pytest-socket==0.4.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 diff --git a/tests/components/auth/conftest.py b/tests/components/auth/conftest.py new file mode 100644 index 00000000000..867f44d9f15 --- /dev/null +++ b/tests/components/auth/conftest.py @@ -0,0 +1,8 @@ +"""Test configuration for auth.""" +import pytest + + +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 8ea65380359..d918b378614 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -28,6 +28,12 @@ class MockTransport: self.sends.append((response, addr)) +@pytest.fixture +def aiohttp_client(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.""" diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 9746fc6d838..c508175a846 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -79,6 +79,12 @@ async def frontend_themes(hass): ) +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client + + @pytest.fixture async def mock_http_client(hass, aiohttp_client, frontend): """Start the Home Assistant HTTP component.""" diff --git a/tests/components/http/conftest.py b/tests/components/http/conftest.py new file mode 100644 index 00000000000..c796ec50b51 --- /dev/null +++ b/tests/components/http/conftest.py @@ -0,0 +1,8 @@ +"""Test configuration for http.""" +import pytest + + +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index c0c57b17a7c..ed8d49e8ddb 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -1,6 +1,8 @@ """The tests for the image_processing component.""" from unittest.mock import PropertyMock, patch +import pytest + import homeassistant.components.http as http import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE @@ -11,6 +13,12 @@ from tests.common import assert_setup_component, async_capture_events from tests.components.image_processing import common +@pytest.fixture +def aiohttp_unused_port(loop, aiohttp_unused_port, socket_enabled): + """Return aiohttp_unused_port and allow opening sockets.""" + return aiohttp_unused_port + + def get_url(hass): """Return camera url.""" state = hass.states.get("camera.demo_camera") diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 70c2d44436a..b2264e78556 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -1,6 +1,5 @@ """Test the motionEye camera.""" import copy -import logging from typing import Any, cast from unittest.mock import AsyncMock, Mock @@ -48,7 +47,11 @@ from . import ( from tests.common import async_fire_time_changed -_LOGGER = logging.getLogger(__name__) + +@pytest.fixture +def aiohttp_server(loop, aiohttp_server, socket_enabled): + """Return aiohttp_server and allow opening sockets.""" + return aiohttp_server async def test_setup_camera(hass: HomeAssistant) -> None: diff --git a/tests/components/nest/conftest.py b/tests/components/nest/conftest.py index 764f037d181..988d9d761fe 100644 --- a/tests/components/nest/conftest.py +++ b/tests/components/nest/conftest.py @@ -48,6 +48,12 @@ class FakeAuth(AbstractAuth): return aiohttp.web.json_response() +@pytest.fixture +def aiohttp_client(loop, aiohttp_client, socket_enabled): + """Return aiohttp_client and allow opening sockets.""" + return aiohttp_client + + @pytest.fixture async def auth(aiohttp_client): """Fixture for an AbstractAuth.""" diff --git a/tests/conftest.py b/tests/conftest.py index 9ee6bbc680b..845145c2ec2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,6 +3,7 @@ import asyncio import datetime import functools import logging +import socket import ssl import threading from unittest.mock import MagicMock, patch @@ -10,6 +11,7 @@ from unittest.mock import MagicMock, patch from aiohttp.test_utils import make_mocked_request import multidict import pytest +import pytest_socket import requests_mock as _requests_mock from homeassistant import core as ha, loader, runner, util @@ -61,6 +63,70 @@ def pytest_configure(config): ) +def pytest_runtest_setup(): + """Throw if tests attempt to open sockets. + + allow_unix_socket is set to True because it's needed by asyncio. + Important: socket_allow_hosts must be called before disable_socket, otherwise all + destinations will be allowed. + """ + pytest_socket.socket_allow_hosts(["127.0.0.1"]) + disable_socket(allow_unix_socket=True) + + +@pytest.fixture +def socket_disabled(pytestconfig): + """Disable socket.socket for duration of this test function. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76 + and hardcodes allow_unix_socket to True because it's not passed on the command line. + """ + socket_was_enabled = socket.socket == pytest_socket._true_socket + disable_socket(allow_unix_socket=True) + yield + if socket_was_enabled: + pytest_socket.enable_socket() + + +@pytest.fixture +def socket_enabled(pytestconfig): + """Enable socket.socket for duration of this test function. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/76 + and hardcodes allow_unix_socket to True because it's not passed on the command line. + """ + socket_was_disabled = socket.socket != pytest_socket._true_socket + pytest_socket.enable_socket() + yield + if socket_was_disabled: + disable_socket(allow_unix_socket=True) + + +def disable_socket(allow_unix_socket=False): + """Disable socket.socket to disable the Internet. useful in testing. + + This incorporates changes from https://github.com/miketheman/pytest-socket/pull/75 + """ + + class GuardedSocket(socket.socket): + """socket guard to disable socket creation (from pytest-socket).""" + + def __new__(cls, *args, **kwargs): + try: + if len(args) > 0: + is_unix_socket = args[0] == socket.AF_UNIX + else: + is_unix_socket = kwargs.get("family") == socket.AF_UNIX + except AttributeError: + # AF_UNIX not supported on Windows https://bugs.python.org/issue33408 + is_unix_socket = False + if is_unix_socket and allow_unix_socket: + return super().__new__(cls, *args, **kwargs) + raise pytest_socket.SocketBlockedError() + + socket.socket = GuardedSocket + + def check_real(func): """Force a function to require a keyword _test_real to be passed in.""" @@ -319,7 +385,7 @@ def local_auth(hass): @pytest.fixture -def hass_client(hass, aiohttp_client, hass_access_token): +def hass_client(hass, aiohttp_client, hass_access_token, socket_enabled): """Return an authenticated HTTP client.""" async def auth_client(): @@ -332,7 +398,7 @@ def hass_client(hass, aiohttp_client, hass_access_token): @pytest.fixture -def hass_client_no_auth(hass, aiohttp_client): +def hass_client_no_auth(hass, aiohttp_client, socket_enabled): """Return an unauthenticated HTTP client.""" async def client(): @@ -367,7 +433,7 @@ def current_request_with_host(current_request): @pytest.fixture -def hass_ws_client(aiohttp_client, hass_access_token, hass): +def hass_ws_client(aiohttp_client, hass_access_token, hass, socket_enabled): """Websocket client fixture connected to websocket server.""" async def create_client(hass=hass, access_token=hass_access_token): diff --git a/tests/test_test_fixtures.py b/tests/test_test_fixtures.py new file mode 100644 index 00000000000..90362e95819 --- /dev/null +++ b/tests/test_test_fixtures.py @@ -0,0 +1,18 @@ +"""Test test fixture configuration.""" +import socket + +import pytest +import pytest_socket + + +def test_sockets_disabled(): + """Test we can't open sockets.""" + with pytest.raises(pytest_socket.SocketBlockedError): + socket.socket() + + +def test_sockets_enabled(socket_enabled): + """Test we can't connect to an address different from 127.0.0.1.""" + mysocket = socket.socket() + with pytest.raises(pytest_socket.SocketConnectBlockedError): + mysocket.connect(("127.0.0.2", 1234))