From f6682ba99dd1f1981f5e755011562f729ba86369 Mon Sep 17 00:00:00 2001
From: Erik Montnemery <erik@montnemery.com>
Date: Wed, 6 Oct 2021 02:46:09 +0200
Subject: [PATCH] Block tests from opening sockets (#55516)

---
 requirements_test.txt                         |  1 +
 tests/components/auth/conftest.py             |  8 +++
 tests/components/emulated_hue/test_upnp.py    |  6 ++
 tests/components/frontend/test_init.py        |  6 ++
 tests/components/http/conftest.py             |  8 +++
 .../components/image_processing/test_init.py  |  8 +++
 tests/components/motioneye/test_camera.py     |  7 +-
 tests/components/nest/conftest.py             |  6 ++
 tests/conftest.py                             | 72 ++++++++++++++++++-
 tests/test_test_fixtures.py                   | 18 +++++
 10 files changed, 135 insertions(+), 5 deletions(-)
 create mode 100644 tests/components/auth/conftest.py
 create mode 100644 tests/components/http/conftest.py
 create mode 100644 tests/test_test_fixtures.py

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))
-- 
GitLab