Skip to content

Commit ddd2b92

Browse files
committed
Updated tests for from_server_socket
Better coverage and refactored some previous ones
1 parent c9842c0 commit ddd2b92

1 file changed

Lines changed: 154 additions & 122 deletions

File tree

cheroot/test/test_ssl.py

Lines changed: 154 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import threading
1414
import time
1515
import traceback
16-
from collections import namedtuple
1716
from types import SimpleNamespace
1817

1918
import pytest
@@ -30,7 +29,6 @@
3029
)
3130

3231
from cheroot import (
33-
connections as _connections,
3432
errors,
3533
)
3634
from cheroot.connections import ConnectionManager
@@ -1177,73 +1175,42 @@ def conn_manager():
11771175
return mgr
11781176

11791177

1180-
def test_ignore_socket_oserror_increments_stats(conn_manager):
1181-
"""Test the socket OSError handler for interrupt errors."""
1182-
# Simulate an interrupt error
1183-
exc = OSError(errors.socket_error_eintr[0])
1184-
1185-
result = conn_manager._ignore_socket_oserror(exc)
1186-
1187-
assert result is True
1188-
assert conn_manager.server.stats['Socket Errors'] == 1
1189-
1190-
1191-
def test_ignore_socket_oserror_disabled_stats(conn_manager):
1192-
"""Test the socket OSError handler for disabled_stats."""
1193-
conn_manager.server.stats['Enabled'] = False
1194-
exc = OSError(errors.socket_error_eintr[0])
1195-
1196-
conn_manager._ignore_socket_oserror(exc)
1197-
1198-
# Should NOT increment since 'Enabled' is False
1199-
assert conn_manager.server.stats['Socket Errors'] == 0
1200-
1201-
12021178
@pytest.mark.parametrize(
1203-
('err_code', 'expected'),
1179+
('err_code', 'should_ignore', 'stats_enabled', 'expect_error'),
12041180
(
1205-
(errors.socket_error_eintr[0], True),
1206-
(errors.socket_errors_nonblocking[0], True),
1207-
(errors.socket_errors_to_ignore[0], True),
1208-
(999, False),
1181+
(errors.socket_error_eintr[0], True, True, True),
1182+
(errors.socket_errors_nonblocking[0], True, True, True),
1183+
(errors.socket_errors_to_ignore[0], True, True, True),
1184+
(999, False, True, True),
1185+
(errors.socket_error_eintr[0], True, False, False), # stats disabled
12091186
),
1187+
ids=['eintr', 'nonblocking', 'to-ignore', 'unknown', 'stats-disabled'],
12101188
)
1211-
def test_ignore_socket_oserror_logic_branches(
1189+
def test_ignore_socket_oserror(
12121190
conn_manager,
12131191
err_code,
1214-
expected,
1192+
should_ignore,
1193+
stats_enabled,
1194+
expect_error,
12151195
):
1216-
"""Test the socket OSError handler for ignorable errors."""
1196+
"""OSError handler returns correct result and increments stats appropriately."""
1197+
conn_manager.server.stats['Enabled'] = stats_enabled
12171198
exc = OSError(err_code)
1218-
assert conn_manager._ignore_socket_oserror(exc) is expected
1219-
1220-
1221-
def _raise_eintr(*args, **kwargs):
1222-
"""Raise an interrupt error."""
1223-
raise OSError(errno.EINTR, 'Interrupted system call')
1224-
12251199

1226-
def test_from_server_socket_interrupt_error(conn_manager_with_server):
1227-
"""Verify that _from_server_socket returns None on ignorable OS errors."""
1228-
# Assign that function to 'accept'
1229-
fake_server_socket = SimpleNamespace(
1230-
accept=_raise_eintr,
1231-
)
1200+
result = conn_manager._ignore_socket_oserror(exc)
12321201

1233-
# Execute
1234-
conn = conn_manager_with_server._from_server_socket(fake_server_socket)
1235-
assert conn is None
1202+
assert result is should_ignore
1203+
assert conn_manager.server.stats['Socket Errors'] == expect_error
12361204

12371205

12381206
@pytest.fixture
1239-
def conn_manager_with_server():
1207+
def conn_manager_with_server(mocker):
12401208
"""Create a ConnectionManager with a stub server."""
12411209
mgr = ConnectionManager.__new__(ConnectionManager)
12421210
mgr.server = SimpleNamespace(
12431211
stats={'Enabled': True, 'Accepts': 0, 'Socket Errors': 0},
12441212
ssl_adapter=None,
12451213
timeout=10,
1246-
# Explicitly 3 args to match the modern pipeline
12471214
ConnectionClass=lambda server, s, mf: SimpleNamespace(
12481215
server=server,
12491216
sock=s,
@@ -1252,99 +1219,164 @@ def conn_manager_with_server():
12521219
remote_port=None,
12531220
),
12541221
bind_addr=('127.0.0.1', 8080),
1222+
close=mocker.Mock(),
12551223
)
12561224
return mgr
12571225

12581226

1259-
@pytest.fixture
1260-
def fake_socket():
1261-
"""Provide a basic mock socket."""
1227+
@contextlib.contextmanager
1228+
def pipe_fd():
1229+
"""Open an OS pipe and ensure both ends are closed."""
1230+
read_fd, write_fd = os.pipe()
1231+
try:
1232+
yield read_fd
1233+
finally:
1234+
os.close(read_fd)
1235+
os.close(write_fd)
1236+
1237+
1238+
def _make_fake_socket(error, read_fd):
1239+
def _accept(): # noqa: WPS430
1240+
raise error
1241+
12621242
return SimpleNamespace(
1263-
settimeout=lambda t: None,
1264-
fileno=lambda: 10,
1265-
close=lambda: None,
1266-
getsockname=lambda: ('127.0.0.1', 8080),
1243+
accept=_accept,
1244+
fileno=lambda: read_fd,
12671245
)
12681246

12691247

1270-
def _dummy_fcntl(fd, op, arg=0):
1271-
"""Return nothing instead of a real file control."""
1272-
return 0
1248+
@pytest.mark.parametrize(
1249+
('error', 'expected_exception'),
1250+
(
1251+
(socket.timeout(), None),
1252+
(OSError(errno.EAGAIN, 'Resource temporarily unavailable'), None),
1253+
(OSError(errno.EINTR, 'Interrupted system call'), None),
1254+
(OSError(errno.EIO, 'Critical kernel error'), OSError),
1255+
),
1256+
ids=['timeout', 'EAGAIN-ignored', 'EINTR-ignored', 'oserror-critical'],
1257+
)
1258+
def test_from_server_socket_transport_errors(
1259+
conn_manager_with_server,
1260+
error,
1261+
expected_exception,
1262+
):
1263+
"""Test errors raised during initial socket accept are handled correctly."""
1264+
with pipe_fd() as read_fd:
1265+
fake_socket = _make_fake_socket(error, read_fd)
12731266

1267+
if expected_exception:
1268+
with pytest.raises(
1269+
expected_exception,
1270+
match='Critical kernel error',
1271+
):
1272+
conn_manager_with_server._from_server_socket(fake_socket)
1273+
else:
1274+
assert (
1275+
conn_manager_with_server._from_server_socket(fake_socket)
1276+
is None
1277+
)
12741278

1275-
def raise_os_error(*args, **kwargs):
1276-
"""Raise OSError in a mock."""
1277-
raise OSError('Broken')
12781279

1280+
def _make_connection(
1281+
conn_manager,
1282+
monkeypatch,
1283+
provided_addr=('1.2.3.4', 80),
1284+
sock_name=('127.0.0.1', 80),
1285+
):
1286+
"""Set up a fake listener and call _from_server_socket."""
1287+
if sys.platform != 'win32':
1288+
monkeypatch.setattr(
1289+
'fcntl.fcntl',
1290+
lambda fd, cmd, *args: 0,
1291+
raising=False,
1292+
)
12791293

1280-
Scenario = namedtuple(
1281-
'Scenario',
1282-
['client_s', 'provided_addr', 'expected_addr', 'expect_error'],
1283-
)
1294+
conn_manager.ConnectionClass = lambda sock, addr, server: SimpleNamespace(
1295+
sock=sock,
1296+
server=server,
1297+
)
1298+
accepted_socket = SimpleNamespace(
1299+
fileno=lambda: 10,
1300+
setblocking=lambda x: None,
1301+
settimeout=lambda t: None,
1302+
setsockopt=lambda *a: None,
1303+
getsockname=lambda: sock_name,
1304+
)
1305+
fake_listener = SimpleNamespace(
1306+
accept=lambda: (accepted_socket, provided_addr),
1307+
)
1308+
return conn_manager._from_server_socket(fake_listener)
12841309

12851310

12861311
@pytest.mark.parametrize(
1287-
'scenario',
1312+
('provided_addr', 'sock_name', 'expected_ip', 'expected_port'),
12881313
(
1289-
Scenario(None, ('127.0.0.1', 123), ('127.0.0.1', 123), False),
1290-
Scenario(None, None, ('0.0.0.0', 0), False),
1291-
Scenario(
1292-
SimpleNamespace(
1293-
settimeout=raise_os_error,
1294-
fileno=lambda: 11,
1295-
close=lambda: None,
1296-
),
1297-
('10.0.0.1', 456),
1298-
('10.0.0.1', 456),
1299-
True,
1300-
),
1301-
),
1302-
ids=(
1303-
'standard-success',
1304-
'missing-addr-fallback',
1305-
'broken-socket-oserror',
1314+
(('1.2.3.4', 80), ('127.0.0.1', 80), '1.2.3.4', 80),
1315+
(None, ('127.0.0.1', 80), '0.0.0.0', 0),
1316+
(None, ('::1', 80, 0, 0), '::', 0),
13061317
),
1318+
ids=['explicit-addr', 'ipv4-fallback', 'ipv6-fallback'],
13071319
)
1308-
def test_from_server_socket_scenarios(
1320+
def test_from_server_socket_address_resolution(
13091321
conn_manager_with_server,
1310-
fake_socket,
13111322
monkeypatch,
1312-
scenario,
1313-
):
1314-
"""
1315-
Verify high-level connection orchestration from sockets.
1316-
1317-
This test ensures that the ``_from_server_socket()``
1318-
pipeline correctly:
1319-
1. Accepts a connection from the server socket.
1320-
2. Configures the resulting client socket.
1321-
3. Successfully increments 'Accepts' stats on successful configuration.
1322-
4. Wraps the socket into a Connection object.
1323-
"""
1324-
# 1. Neuter fcntl right here inside the function
1325-
if hasattr(_connections, 'fcntl'):
1326-
# Patch the function
1327-
monkeypatch.setattr(_connections.fcntl, 'fcntl', _dummy_fcntl)
1328-
monkeypatch.setattr(_connections.fcntl, 'F_GETFD', 1)
1329-
1330-
# Use the provided socket or fall back to the fixture
1331-
actual_client = scenario.client_s or fake_socket
1332-
1333-
# Mock the server socket to return our scenario-specific data
1334-
fake_server_socket = SimpleNamespace(
1335-
accept=lambda: (actual_client, scenario.provided_addr),
1323+
provided_addr,
1324+
sock_name,
1325+
expected_ip,
1326+
expected_port,
1327+
): # pylint: disable=too-many-positional-arguments
1328+
"""Remote address is resolved correctly from accepted socket or fallback."""
1329+
conn = _make_connection(
1330+
conn_manager_with_server,
1331+
monkeypatch,
1332+
provided_addr,
1333+
sock_name,
1334+
)
1335+
assert conn is not None
1336+
assert conn.remote_addr == expected_ip
1337+
assert conn.remote_port == expected_port
1338+
1339+
1340+
def _fatal_ssl_wrap(sock):
1341+
raise errors.FatalSSLAlert('Simulated handshake drop')
1342+
1343+
1344+
def test_from_server_socket_ssl_failure(conn_manager_with_server, monkeypatch):
1345+
"""A FatalSSLAlert during TLS wrap closes the connection and logs it."""
1346+
server = conn_manager_with_server.server
1347+
monkeypatch.setattr(
1348+
server,
1349+
'ssl_adapter',
1350+
SimpleNamespace(wrap=_fatal_ssl_wrap),
13361351
)
13371352

1338-
if scenario.expect_error:
1339-
with pytest.raises(OSError, match='Broken'):
1340-
conn_manager_with_server._from_server_socket(fake_server_socket)
1341-
else:
1342-
conn = conn_manager_with_server._from_server_socket(fake_server_socket)
1353+
logged_messages = []
1354+
server.error_log = lambda msg, **kwargs: logged_messages.append(msg)
1355+
1356+
conn = _make_connection(conn_manager_with_server, monkeypatch)
1357+
assert conn is None
1358+
assert any('lost' in msg.lower() for msg in logged_messages)
1359+
1360+
1361+
def test_connection_manager_close_logic(conn_manager_with_server, mocker):
1362+
"""Verify close() shuts down client connections but not server."""
1363+
mgr = conn_manager_with_server
1364+
1365+
mock_conn_1 = mocker.Mock()
1366+
mock_conn_2 = mocker.Mock()
1367+
1368+
selector = mocker.Mock()
1369+
selector.connections = [
1370+
(None, mock_conn_1),
1371+
(None, mock_conn_2),
1372+
(None, mgr.server),
1373+
]
13431374

1344-
assert conn is not None
1345-
assert conn.sock is actual_client
1375+
mgr._selector = selector
1376+
mgr.close()
13461377

1347-
expected_ip, expected_port = scenario.expected_addr
1348-
assert conn.remote_addr == expected_ip
1349-
assert conn.remote_port == expected_port
1350-
assert conn_manager_with_server.server.stats['Accepts'] == 1
1378+
mock_conn_1.close.assert_called_once()
1379+
mock_conn_2.close.assert_called_once()
1380+
# Server is excluded from close() — only client connections are closed
1381+
mgr.server.close.assert_not_called()
1382+
selector.close.assert_called_once()

0 commit comments

Comments
 (0)