Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions kolibri/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import ipaddress
import os
import shutil
import socket

import pytest

Expand All @@ -8,6 +10,72 @@
# referenced in pytest.ini
TEMP_KOLIBRI_HOME = "./.pytest_kolibri_home"

# Network access guard
#
# Blocks all network access to non-loopback addresses for the entire test
# run, so that no test can ever depend on (or be broken by) an external
# service. Any attempt to resolve or connect to a non-local host raises
# BlockedNetworkAccessError pointing at the offending call.
#
# Tests that exercise code making outbound HTTP requests should mock at the
# requests session level, e.g. by patching ``requests.Session.request`` -
# see kolibri/core/discovery/test/helpers.py for reusable helpers.

LOOPBACK_HOSTNAMES = {"localhost", "localhost.localdomain", "ip6-localhost", ""}

_real_getaddrinfo = socket.getaddrinfo
_real_socket_connect = socket.socket.connect


class BlockedNetworkAccessError(RuntimeError):
pass


def _is_local_host(host):
if host is None:
return True
if isinstance(host, bytes):
host = host.decode("utf-8", "replace")
host = host.lower().rstrip(".")
if host in LOOPBACK_HOSTNAMES:
return True
try:
ip = ipaddress.ip_address(host)
except ValueError:
# A non-local hostname that would require DNS resolution
return False
return ip.is_loopback or ip.is_unspecified


def _block(api, host, port):
raise BlockedNetworkAccessError(
"A test attempted real network access via {api} to {host}:{port}. "
"Tests must not make requests to external services - mock them "
"instead, e.g. by patching requests.Session.request "
"(see kolibri/core/discovery/test/helpers.py).".format(
api=api, host=host, port=port
)
)


def _guarded_getaddrinfo(host, port, *args, **kwargs):
if not _is_local_host(host):
_block("socket.getaddrinfo", host, port)
return _real_getaddrinfo(host, port, *args, **kwargs)


def _guarded_socket_connect(sock, address):
if sock.family in (socket.AF_INET, socket.AF_INET6) and isinstance(address, tuple):
host = address[0]
if not _is_local_host(host):
_block("socket.connect", host, address[1] if len(address) > 1 else "")
return _real_socket_connect(sock, address)


def pytest_configure(config):
socket.getaddrinfo = _guarded_getaddrinfo
socket.socket.connect = _guarded_socket_connect


@pytest.fixture(autouse=True)
def clear_process_cache():
Expand Down
19 changes: 18 additions & 1 deletion kolibri/core/test/test_key_urls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import requests
from django.conf import settings
from django.urls import reverse
from django.urls.exceptions import NoReverseMatch
Expand All @@ -12,6 +13,20 @@
from kolibri.core.auth.test.test_api import FacilityFactory
from kolibri.core.auth.test.test_api import FacilityUserFactory
from kolibri.core.device.translation import get_settings_language
from kolibri.core.discovery.test.helpers import mock_response


def mock_external_request(session, method, url, *args, **kwargs):
"""
Give any outbound HTTP request a successful empty response, so that views
proxying to external services (the Kolibri Data Portal token validation,
the Studio remote channel lookup) can be smoke tested without depending
on those services.
"""
response = mock_response(200)
response.url = url
response.json.return_value = []
return response


class BeforeDeviceProvisionTests(APITestCase):
Expand Down Expand Up @@ -215,7 +230,9 @@ def check_urls(urlpatterns, prefix=""): # noqa: C901

with patch(
"kolibri.core.webpack.hooks.WebpackBundleHook.bundle", return_value=[]
), patch("kolibri.core.webpack.hooks.WebpackBundleHook.get_by_unique_id"):
), patch(
"kolibri.core.webpack.hooks.WebpackBundleHook.get_by_unique_id"
), patch.object(requests.Session, "request", mock_external_request):
from kolibri.deployment.default.urls import urlpatterns

check_urls(urlpatterns)
Expand Down
Loading