Add tests for scrapy.utils.ssl

scrapy.utils.ssl builds the SSL/TLS context used by the HTTPS download handler
but had no dedicated test coverage. Add tests/test_utils_ssl.py covering
_make_ssl_context (verification on/off, TLS min/max version handling, ciphers,
protocol, and combinations), _make_insecure_ssl_ctx, the _get_tls_version_limit
and _get_tls_version_limits helpers (valid/invalid/unset values), the no-limits
case of _get_cert_options_version_kwargs, and get_openssl_version.
This commit is contained in:
r1cksync 2026-06-23 08:46:13 +05:30
parent f605defefc
commit 3de3b8ada3
1 changed files with 183 additions and 0 deletions

183
tests/test_utils_ssl.py Normal file
View File

@ -0,0 +1,183 @@
import ssl
import pytest
from scrapy.settings import Settings
from scrapy.utils.ssl import (
_get_cert_options_version_kwargs,
_get_tls_version_limit,
_get_tls_version_limits,
_make_insecure_ssl_ctx,
_make_ssl_context,
get_openssl_version,
)
_VERSION_MAP = {
"TLSv1.0": ssl.TLSVersion.TLSv1,
"TLSv1.1": ssl.TLSVersion.TLSv1_1,
"TLSv1.2": ssl.TLSVersion.TLSv1_2,
"TLSv1.3": ssl.TLSVersion.TLSv1_3,
}
class TestGetTLSVersionLimit:
def test_returns_none_when_unset(self):
settings = Settings() # DOWNLOAD_TLS_MIN_VERSION defaults to None
assert (
_get_tls_version_limit(
settings, "DOWNLOAD_TLS_MIN_VERSION", _VERSION_MAP.__getitem__
)
is None
)
@pytest.mark.parametrize(("name", "expected"), list(_VERSION_MAP.items()))
def test_converts_known_value(self, name, expected):
settings = Settings({"DOWNLOAD_TLS_MIN_VERSION": name})
assert (
_get_tls_version_limit(
settings, "DOWNLOAD_TLS_MIN_VERSION", _VERSION_MAP.__getitem__
)
== expected
)
def test_unknown_value_raises_value_error(self):
settings = Settings({"DOWNLOAD_TLS_MIN_VERSION": "TLSv9.9"})
with pytest.raises(ValueError, match="Unknown DOWNLOAD_TLS_MIN_VERSION value"):
_get_tls_version_limit(
settings, "DOWNLOAD_TLS_MIN_VERSION", _VERSION_MAP.__getitem__
)
def test_returns_both_limits(self):
settings = Settings(
{
"DOWNLOAD_TLS_MIN_VERSION": "TLSv1.1",
"DOWNLOAD_TLS_MAX_VERSION": "TLSv1.3",
}
)
low, high = _get_tls_version_limits(settings, _VERSION_MAP.__getitem__)
assert low == ssl.TLSVersion.TLSv1_1
assert high == ssl.TLSVersion.TLSv1_3
def test_converts_max_version_setting(self):
settings = Settings({"DOWNLOAD_TLS_MAX_VERSION": "TLSv1.3"})
assert (
_get_tls_version_limit(
settings, "DOWNLOAD_TLS_MAX_VERSION", _VERSION_MAP.__getitem__
)
== ssl.TLSVersion.TLSv1_3
)
def test_both_limits_none_by_default(self):
low, high = _get_tls_version_limits(Settings(), _VERSION_MAP.__getitem__)
assert low is None
assert high is None
def test_only_min_limit_set(self):
settings = Settings({"DOWNLOAD_TLS_MIN_VERSION": "TLSv1.2"})
low, high = _get_tls_version_limits(settings, _VERSION_MAP.__getitem__)
assert low == ssl.TLSVersion.TLSv1_2
assert high is None
class TestMakeSSLContext:
def test_returns_tls_client_context(self):
ctx = _make_ssl_context(Settings())
assert isinstance(ctx, ssl.SSLContext)
assert ctx.protocol == ssl.PROTOCOL_TLS_CLIENT
def test_default_settings_skip_verification(self):
ctx = _make_ssl_context(Settings())
assert ctx.check_hostname is False
assert ctx.verify_mode == ssl.CERT_NONE
def test_verification_enabled(self):
ctx = _make_ssl_context(Settings({"DOWNLOAD_VERIFY_CERTIFICATES": True}))
assert ctx.check_hostname is True
assert ctx.verify_mode == ssl.CERT_REQUIRED
@pytest.mark.parametrize(
("name", "expected"),
[("TLSv1.2", ssl.TLSVersion.TLSv1_2), ("TLSv1.3", ssl.TLSVersion.TLSv1_3)],
)
def test_minimum_version_applied(self, name, expected):
ctx = _make_ssl_context(Settings({"DOWNLOAD_TLS_MIN_VERSION": name}))
assert ctx.minimum_version == expected
@pytest.mark.parametrize(
("name", "expected"),
[("TLSv1.2", ssl.TLSVersion.TLSv1_2), ("TLSv1.3", ssl.TLSVersion.TLSv1_3)],
)
def test_maximum_version_applied(self, name, expected):
ctx = _make_ssl_context(Settings({"DOWNLOAD_TLS_MAX_VERSION": name}))
assert ctx.maximum_version == expected
def test_version_left_at_default_when_unset(self):
default_ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
ctx = _make_ssl_context(Settings())
assert ctx.minimum_version == default_ctx.minimum_version
assert ctx.maximum_version == default_ctx.maximum_version
def test_unknown_version_raises_value_error(self):
with pytest.raises(ValueError, match="Unknown DOWNLOAD_TLS_MIN_VERSION"):
_make_ssl_context(Settings({"DOWNLOAD_TLS_MIN_VERSION": "bogus"}))
def test_valid_ciphers_accepted(self):
ctx = _make_ssl_context(Settings({"DOWNLOADER_CLIENT_TLS_CIPHERS": "DEFAULT"}))
assert isinstance(ctx, ssl.SSLContext)
def test_invalid_ciphers_raise_ssl_error(self):
with pytest.raises(ssl.SSLError):
_make_ssl_context(
Settings({"DOWNLOADER_CLIENT_TLS_CIPHERS": "NOT-A-REAL-CIPHER"})
)
def test_empty_ciphers_setting_is_skipped(self):
# An empty string is falsy, so ``set_ciphers`` is not called.
ctx = _make_ssl_context(Settings({"DOWNLOADER_CLIENT_TLS_CIPHERS": ""}))
assert isinstance(ctx, ssl.SSLContext)
def test_min_and_max_versions_together(self):
ctx = _make_ssl_context(
Settings(
{
"DOWNLOAD_TLS_MIN_VERSION": "TLSv1.2",
"DOWNLOAD_TLS_MAX_VERSION": "TLSv1.3",
}
)
)
assert ctx.minimum_version == ssl.TLSVersion.TLSv1_2
assert ctx.maximum_version == ssl.TLSVersion.TLSv1_3
def test_verification_and_version_together(self):
ctx = _make_ssl_context(
Settings(
{
"DOWNLOAD_VERIFY_CERTIFICATES": True,
"DOWNLOAD_TLS_MIN_VERSION": "TLSv1.2",
}
)
)
assert ctx.verify_mode == ssl.CERT_REQUIRED
assert ctx.minimum_version == ssl.TLSVersion.TLSv1_2
class TestGetCertOptionsVersionKwargs:
def test_no_limits_returns_empty_kwargs(self):
assert _get_cert_options_version_kwargs(None, None) == {}
class TestMakeInsecureSSLContext:
def test_does_not_verify(self):
ctx = _make_insecure_ssl_ctx()
assert isinstance(ctx, ssl.SSLContext)
assert ctx.check_hostname is False
assert ctx.verify_mode == ssl.CERT_NONE
class TestGetOpenSSLVersion:
def test_returns_descriptive_string(self):
version = get_openssl_version()
assert isinstance(version, str)
# Format: "<pyOpenSSL version> (<system OpenSSL build string>)"
assert "(" in version
assert version.endswith(")")