Files
scylladb/test/alternator/test_https.py
Artsiom Mishuta b1e9c0b867 test/pylib: add typed skip markers plugin
Add skip_reason_plugin.py — a framework-agnostic pytest plugin that
provides typed skip markers (skip_bug, skip_not_implemented, skip_slow,
skip_env) so that the reason a test is skipped is machine-readable in
JUnit XML and Allure reports.  Bare untyped pytest.mark.skip now
triggers a warning (to become an error after full migration).  Runtime
skips via skip() are also enriched by parsing the [type] prefix from
the skip message.

The plugin is a class (SkipReasonPlugin) that receives the concrete
SkipType enum and an optional report_callback from conftest.py, keeping
it decoupled from allure and project-specific types.

Extract SkipType enum and convenience runtime skip wrappers (skip_bug,
skip_env, etc.) into test/pylib/skip_types.py so callers only need a
single import instead of importing both SkipType and skip() separately.
conftest.py imports SkipType from the new module and registers the
plugin instance unconditionally (for all test runners).

New files:
- test/pylib/skip_reason_plugin.py: core plugin — typed marker
  processing, bare-skip warnings, JUnit/Allure report enrichment
  (including runtime skip() parsing via _parse_skip_type helper)
- test/pylib/skip_types.py: SkipType enum and convenience wrappers
  (skip_bug, skip_not_implemented, skip_slow, skip_env)
- test/pylib_test/test_skip_reason_plugin.py: 17 pytester-based
  test functions (51 cases across 3 build modes) covering markers,
  warnings, reports, callbacks, and skip_mode interaction

Infrastructure changes:
- test/conftest.py: import SkipType from skip_types, register
  SkipReasonPlugin with allure report callback
- test/pylib/runner.py: set SKIP_TYPE_KEY/SKIP_REASON_KEY stash keys
  for skip_mode so the report hook can enrich JUnit/Allure with
  skip_type=mode without longrepr parsing
- test/pytest.ini: register typed marker definitions (required for
  --strict-markers even when plugin is not loaded)

Migrated test files (representative samples):
- test/cluster/test_tablet_repair_scheduler.py:
  skip -> skip_bug (#26844), skip -> skip_not_implemented
- test/cqlpy/.../timestamp_test.py: skip -> skip_slow
- test/cluster/dtest/schema_management_test.py: skip -> skip_not_implemented
- test/cluster/test_change_replication_factor_1_to_0.py: skip -> skip_bug (#20282)
- test/alternator/conftest.py: skip -> skip_env
- test/alternator/test_https.py: use skip_env() wrapper

Fixes SCYLLADB-79

Closes scylladb/scylladb#29235
2026-04-08 10:38:56 +03:00

117 lines
5.8 KiB
Python

# Copyright 2025-present ScyllaDB
#
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
#############################################################################
# Tests for Alternator (DynamoDB API) requests over HTTPS (a.k.a. SSL, TLS).
# These tests are skipped when the tests are *not* running over https, so
# run the tests with the "--https" flag to enable them. The "--https" flag,
# among other things, uses an https:// endpoint.
#############################################################################
import pytest
import urllib3
import urllib.parse
import ssl
from test.pylib.skip_types import skip_env
@pytest.fixture(scope="module")
def https_url(dynamodb):
url = dynamodb.meta.client._endpoint.host
if not url.startswith('https://'):
skip_env("HTTPS-specific tests are skipped without the '--https' option")
yield url
# Test which TLS versions are supported. We require that both TLS 1.2 and 1.3
# must be supported, and the older TLS 1.1 version may either work or be
# rejected by the server with the proper error ("no protocols available"),
# and not a broken connection as happened in issue #8827.
# To make it easier to understand what TLS setup the client-side stack is
# using, we avoid boto3 and instead create requests manually using Python's
# "urllib3" (which boto3 also uses in its implementation). Moreover, to
# avoid having to build and sign complex requests (see examples of that in
# test_manual_requests.py), we used the generic health-check request (the "/"
# request). This request does not need to be signed - but still needs to use
# TLS properly, so we can use it to test TLS.
@pytest.mark.parametrize("tls_version_and_support_required", [
('TLSv1_1', False),
('TLSv1_2', True),
('TLSv1_3', True)])
def test_tls_versions(https_url, tls_version_and_support_required):
tls_version, support_required = tls_version_and_support_required
context = ssl.create_default_context()
context.minimum_version = getattr(ssl.TLSVersion, tls_version)
context.maximum_version = context.minimum_version
# check_hostname and verify_mode is needed when we use self-signed
# certificates in tests.
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
with urllib3.PoolManager(ssl_context=context, retries=0) as pool:
try:
# preload_content=False tells the library not to read the content
# and not release the connection back to the pool, so we can
# still inspect this connection and its negoatiated TLS version.
res = pool.request('GET', https_url, preload_content=False)
except urllib3.exceptions.MaxRetryError as e:
if support_required or 'NO_PROTOCOLS_AVAILABLE' not in str(e):
# An error in a TLS version we are required to support,
# or an unexpected type of error in any version - this is
# a failure.
raise
return
# Check that the negotiated TLS version, res.connection.sock.version(),
# is the one we tried to set. Note that version() returns a string
# like TLSv1.2, while the ssl.TLSVersion attribute name we used for
# tls_version uses a underscore.
assert res.connection
assert res.connection.sock.version().replace('.', '_') == tls_version
# Finally read the response
res.read()
assert res.status == 200
# Test that if we send an unencrypted (HTTP) request to an HTTPS port,
# it doesn't work.
# Although in theory it is possible to implement a server that serves both
# HTTP and HTTPS on the same port (by looking at the first byte of the
# request) - neither DynamoDB nor Scylla do this, and it's not a good idea:
# A deployment that wants to allow only HTTPS, not HTTP, probably has a
# security reason for doing this, and doesn't want us to allow an unencrypted
# HTTP request to be sent over the HTTPS port. So let's verify that an HTTP
# request on an HTTPS port indeed does not work.
def test_http_on_https(https_url):
# Take an https URL, replace the https by http. If the port is not
# explicitly specified we need to assume it is the default https port
# (443) and need to add it explicitly, because it's not the default for
# http.
p = urllib.parse.urlparse(https_url)
assert p.scheme == 'https'
p = p._replace(scheme='http')
if p.port is None:
p = p._replace(netloc=p.netloc+':443')
http_url = p.geturl()
# CERT_NONE causes the client to not verify the server certificate,
# and is needed because we use self-signed certificates in tests.
with urllib3.PoolManager(retries=0, cert_reqs=ssl.CERT_NONE) as pool:
# Sanity check: the original https:// URL works. This is useful to
# know so that if the http:// URL does *not* work - as we expect -
# we can be sure this is a real success of the test and not some
# silly error causing every request to fail and not just the http one.
res = pool.request('GET', https_url)
assert res.status == 200
# In DynamoDB, sending an unencrypted request to the SSL port results
# in nice clean HTTP 400 Bad Request error, with a body explaining
# that "The plain HTTP request was sent to HTTPS port". In Scylla,
# the situation is not as clean - it replies with an SSL error which
# confuses urllib3 because doesn't look like an HTTP reply. Let's
# accept both. The important part is that the unencrypted request is
# not accepted.
try:
res = pool.request('GET', http_url)
assert res.status == 400
except urllib3.exceptions.MaxRetryError as e:
# This happens when the SSL server returns an error in SSL
# form, which doesn't look like an HTTP status line.
assert 'BadStatusLine' in str(e)