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
35 changes: 34 additions & 1 deletion tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,34 @@
"""Unit tests for Update-time."""
"""Unit tests for Update-time.

If a test reaches the network, the live registry answers it, so the test passes or fails on data nobody wrote
down. Importing this package refuses the network to every test in it. The refusal is a `RuntimeError` rather
than a connection error because `update_time.io.fetch` turns a `requests` exception into a logged `None`, so a
connection error would leave an unmocked request answered with `None` rather than failing the test.
"""

from typing import TYPE_CHECKING, NoReturn
from unittest.mock import patch

if TYPE_CHECKING:
import socket


def _refuse(address: object) -> NoReturn:
"""Raise the refusal, naming the address the test tried to reach."""
message = f"test tried to reach the network at {address}"
raise RuntimeError(message)


def _refuse_connection(_self: socket.socket, address: object) -> NoReturn:
_refuse(address)


def _refuse_resolution(host: object, *_args: object, **_kwargs: object) -> NoReturn:
"""Refuse to resolve the host name, whatever port, family, and flags the lookup passes with it."""
_refuse(host)


# A request to a host name is refused at the lookup, before it reaches the resolver; one to a literal address needs
# no lookup and is refused at the socket. Neither patch is ever stopped: no test may lift the refusal.
patch("socket.socket.connect", _refuse_connection).start()
patch("socket.getaddrinfo", _refuse_resolution).start()
31 changes: 31 additions & 0 deletions tests/test_network_guard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Unit tests for the guard that refuses the network."""

import socket
import unittest
from unittest.mock import Mock

from update_time.io.fetch import fetch


class NetworkGuardTest(unittest.TestCase):
"""Unit tests for the refusal of network access."""

def test_connecting_a_socket_is_refused(self):
"""Test that connecting a socket raises an error naming the address it tried to reach."""
with socket.socket() as sock, self.assertRaises(RuntimeError) as raised:
sock.connect(("127.0.0.1", 9))
self.assertEqual(str(raised.exception), "test tried to reach the network at ('127.0.0.1', 9)")

def test_resolving_a_host_name_is_refused(self):
"""Test that resolving a host name raises an error naming the host it tried to reach."""
with self.assertRaises(RuntimeError) as raised:
socket.getaddrinfo("update-time.invalid", 443)
self.assertEqual(str(raised.exception), "test tried to reach the network at update-time.invalid")

def test_an_unmocked_request_fails_the_test(self):
"""Test that a request raises the refusal instead of fetch logging it as a network error and returning None."""
logger = Mock()
with self.assertRaises(RuntimeError) as raised:
fetch("https://update-time.invalid/", logger)
self.assertEqual(str(raised.exception), "test tried to reach the network at update-time.invalid")
self.assertEqual(logger.mock_calls, [])