feat(admin-config-tool): drop ha/data/ entirely, match real client TLS
deploy-ha.sh no longer takes a <data-dir> argument or --build-config
flag. Every deploy now generates a fresh self-signed TLS cert/key
(openssl, RSA 2048, 10y validity) directly into the deploy dir's
certs/, and builds cluster.bin directly into controller/ctrl-1/conf/
via xceadmin -- no intermediate staging directory, nothing preserved
across runs. run-ha-soak.sh (/xce-test-ha) updated to match: dropped
its own XCE_HA_DATADIR-based precondition entirely. Updated 4 callers
(run-ha-soak.sh, test-heartbeat-tolerance.sh, test-commit-durability.sh,
both xce-deploy-ha/xce-test-ha SKILL.md docs).
Separately: xcecommon.wire.connect() was loading a client-side TLS
cert, diverging from the real reference client -- netio.c's
create_ssl_context() presents no certificate at all when acting as a
client (cert_file/key_file both NULL, bare TLS_client_method()).
Fixed connect() to match, and since nothing else consumed cert_dir
(verify_mode=CERT_NONE throughout, no CA check either), removed it
end-to-end: discovery.follow_redirects(), push.push_config()/
probe_config(), and the --cert-dir CLI flag.
Server-side cert loading (mock TLS test servers) is unaffected --
moved into a shared session-scoped tls_certs fixture (new
tests/conftest.py, openssl-generated) replacing 3 duplicated
REPO_HA_DATA_DIR/skipif blocks. Net result: those tests no longer skip
under a hermetic nix build sandbox (which never had ha/data/) --
verified 165/165 passed, 0 skipped, via both meson compile and a full
nix build --option sandbox false .#xcecore_debug rebuild (previously
128 passed / 37 skipped there).
ha/data/{xce.crt,xce.key} removed from git tracking. Requirements
2.3/2.4 and 8.3/8.4/8.5 marked superseded in requirements.md with
dated correction notes.
This commit is contained in:
parent
db2b5f17f0
commit
ee8473bd99
20 changed files with 244 additions and 297 deletions
|
|
@ -3,57 +3,46 @@ set -e
|
|||
|
||||
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||
|
||||
# --build-config may appear anywhere among the positional args.
|
||||
BUILD_CONFIG=0
|
||||
ARGS=()
|
||||
for arg in "$@"; do
|
||||
if [ "$arg" = "--build-config" ]; then
|
||||
BUILD_CONFIG=1
|
||||
else
|
||||
ARGS+=("$arg")
|
||||
fi
|
||||
done
|
||||
BINARY=$1
|
||||
TOP=$2
|
||||
PEERS=$3
|
||||
|
||||
BINARY=${ARGS[0]}
|
||||
DATADIR=${ARGS[1]}
|
||||
TOP=${ARGS[2]}
|
||||
PEERS=${ARGS[3]}
|
||||
|
||||
if [ -z "$BINARY" ] || [ -z "$DATADIR" ] || [ -z "$TOP" ] || [ -z "$PEERS" ]; then
|
||||
echo "Usage: $0 [--build-config] <binary> <data-dir> <top-dir> <peers>"
|
||||
echo " --build-config: rebuild <data-dir>/cluster.bin from configs/ before deploying,"
|
||||
echo " even if one already exists"
|
||||
if [ -z "$BINARY" ] || [ -z "$TOP" ] || [ -z "$PEERS" ]; then
|
||||
echo "Usage: $0 <binary> <top-dir> <peers>"
|
||||
echo " peers: comma-separated host:port list (e.g. host1:port1,host2:port2)"
|
||||
echo " TLS cert/key and cluster.bin are generated fresh into <top-dir> every"
|
||||
echo " run (openssl self-signed cert; cluster.bin via xceadmin from"
|
||||
echo " src/configs/*.yaml) -- no pre-existing data directory needed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BINARY=$(realpath "$BINARY")
|
||||
|
||||
# Requirements 8.3/8.4/8.5: rebuild cluster.bin when --build-config is
|
||||
# passed or none exists yet; otherwise leave an existing one untouched.
|
||||
if [ "$BUILD_CONFIG" = "1" ] || [ ! -f "$DATADIR/cluster.bin" ]; then
|
||||
# xceadmin is meson-built now (src/xceadmin/meson.build), not a plain
|
||||
# `pip install -e .` venv checked out under src/xceadmin/ -- its build
|
||||
# output lives under whichever meson build dir xce-build.sh's profile
|
||||
# picked, or under a nix-installed prefix's bin/. Try each in turn.
|
||||
XCEADMIN=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/../xce.build/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../build.release/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../build.dbg/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../result/bin/xceadmin" \
|
||||
"$(command -v xceadmin 2>/dev/null || true)"
|
||||
do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||
XCEADMIN="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$XCEADMIN" ]; then
|
||||
echo "Error: xceadmin binary not found -- run 'xce-build.sh xceadmin' or 'nix build .#xcecore' first"
|
||||
exit 1
|
||||
# xceadmin is meson-built now (src/xceadmin/meson.build), not a plain
|
||||
# `pip install -e .` venv checked out under src/xceadmin/ -- its build
|
||||
# output lives under whichever meson build dir xce-build.sh's profile
|
||||
# picked, or under a nix-installed prefix's bin/. Try each in turn.
|
||||
XCEADMIN=""
|
||||
for candidate in \
|
||||
"$SCRIPT_DIR/../xce.build/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../build.release/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../build.dbg/xceadmin/.venv/bin/xceadmin" \
|
||||
"$SCRIPT_DIR/../result/bin/xceadmin" \
|
||||
"$(command -v xceadmin 2>/dev/null || true)"
|
||||
do
|
||||
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
|
||||
XCEADMIN="$candidate"
|
||||
break
|
||||
fi
|
||||
"$XCEADMIN" config build "$SCRIPT_DIR/../configs" -o "$DATADIR/cluster.bin"
|
||||
done
|
||||
if [ -z "$XCEADMIN" ]; then
|
||||
echo "Error: xceadmin binary not found -- run 'xce-build.sh xceadmin' or 'nix build .#xcecore' first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v openssl >/dev/null 2>&1; then
|
||||
echo "Error: openssl not found -- run under 'nix develop' (provides openssl on PATH)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IFS=',' read -ra PEER_ARRAY <<< "$PEERS"
|
||||
|
|
@ -68,13 +57,22 @@ for i in $(seq 1 $N); do
|
|||
mkdir -p "$TOP/controller/ctrl-$i/data"
|
||||
done
|
||||
|
||||
# Copy certificates
|
||||
cp "$DATADIR/xce.crt" "$TOP/certs/"
|
||||
cp "$DATADIR/xce.key" "$TOP/certs/"
|
||||
# Self-signed dev/test TLS cert+key, generated fresh directly into the
|
||||
# deployment dir every run -- nothing checked into git, nothing shared
|
||||
# across deploys. Long-lived (10y) since it's regenerated per deploy
|
||||
# anyway; TLS verification is SSL_VERIFY_NONE everywhere in this codebase
|
||||
# (no hostname/identity check), so a fresh self-signed pair per run loses
|
||||
# no real security property versus a long-lived shared one.
|
||||
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 \
|
||||
-keyout "$TOP/certs/xce.key" -out "$TOP/certs/xce.crt" \
|
||||
-subj "/CN=xce-dev-test" 2>/dev/null
|
||||
|
||||
# Tag and copy cluster.bin
|
||||
TAG=$(sha256sum "$DATADIR/cluster.bin" | cut -c1-12)
|
||||
cp "$DATADIR/cluster.bin" "$TOP/controller/ctrl-1/conf/cluster.bin.$TAG"
|
||||
# Build cluster.bin from src/configs/*.yaml via xceadmin, straight into
|
||||
# this deploy's own ctrl-1/conf/ (no intermediate data-dir copy).
|
||||
CLUSTER_BIN_TMP="$TOP/.cluster.bin.tmp"
|
||||
"$XCEADMIN" config build "$SCRIPT_DIR/../configs" -o "$CLUSTER_BIN_TMP"
|
||||
TAG=$(sha256sum "$CLUSTER_BIN_TMP" | cut -c1-12)
|
||||
mv "$CLUSTER_BIN_TMP" "$TOP/controller/ctrl-1/conf/cluster.bin.$TAG"
|
||||
echo "cluster.bin.$TAG" > "$TOP/controller/ctrl-1/conf/current"
|
||||
|
||||
# Build peers string: ctrl-1=host1:port1,ctrl-2=host2:port2,...
|
||||
|
|
|
|||
|
|
@ -95,7 +95,6 @@ fi
|
|||
WORKTREE=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
BINARY="$WORKTREE/src/xce.build/controller/xce_controller"
|
||||
XCE_TEST="$WORKTREE/src/xce.build/controller/xce_test"
|
||||
DATADIR="$WORKTREE/ha/data"
|
||||
DEPLOY_DIR="${1:-/tmp/xce-ha-deploy}"
|
||||
ROUNDS="${2:-8}"
|
||||
PEERS="localhost:9991,localhost:9992,localhost:9993"
|
||||
|
|
@ -151,7 +150,7 @@ deploy_cluster() {
|
|||
exit 2
|
||||
fi
|
||||
yellow "Deploying 3-node cluster to $DEPLOY_DIR ..."
|
||||
bash "$WORKTREE/src/tools/deploy-ha.sh" "$BINARY" "$DATADIR" "$DEPLOY_DIR" "$PEERS"
|
||||
bash "$WORKTREE/src/tools/deploy-ha.sh" "$BINARY" "$DEPLOY_DIR" "$PEERS"
|
||||
}
|
||||
|
||||
start_node() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ set -e
|
|||
|
||||
WORKTREE=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
BINARY="$WORKTREE/src/xce.build/controller/xce_controller"
|
||||
DATADIR="$WORKTREE/ha/data"
|
||||
DEPLOY_DIR="${1:-/tmp/xce-deploy}"
|
||||
PEERS="localhost:9991,localhost:9992,localhost:9993"
|
||||
LOGDIR="$DEPLOY_DIR/logs"
|
||||
|
|
@ -48,7 +47,7 @@ deploy_cluster() {
|
|||
fi
|
||||
yellow "Deploying 3-node cluster to $DEPLOY_DIR ..."
|
||||
bash "$WORKTREE/src/tools/deploy-ha.sh" \
|
||||
"$BINARY" "$DATADIR" "$DEPLOY_DIR" "$PEERS"
|
||||
"$BINARY" "$DEPLOY_DIR" "$PEERS"
|
||||
}
|
||||
|
||||
start_cluster() {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ from xceadmin.push import probe_config, push_config
|
|||
|
||||
_DEFAULT_OUTPUT = "cluster.bin"
|
||||
_DEFAULT_CONTROLLER = "localhost:9991"
|
||||
_DEFAULT_CERT_DIR = "ha/data"
|
||||
|
||||
|
||||
@click.group()
|
||||
|
|
@ -59,15 +58,12 @@ def config_build(configs_dir: str, output: str) -> None:
|
|||
@click.option(
|
||||
"--controller", default=_DEFAULT_CONTROLLER, help="host:port of any controller node (2.2)."
|
||||
)
|
||||
@click.option(
|
||||
"--cert-dir", default=_DEFAULT_CERT_DIR, help="Directory containing xce.crt/xce.key (2.4)."
|
||||
)
|
||||
@click.option("--dry-run", is_flag=True, help="Probe acceptance without committing (Requirement 6).")
|
||||
def config_push(cluster_bin_path: str, controller: str, cert_dir: str, dry_run: bool) -> None:
|
||||
def config_push(cluster_bin_path: str, controller: str, dry_run: bool) -> None:
|
||||
"""Push CLUSTER_BIN_PATH to the controller cluster."""
|
||||
try:
|
||||
if dry_run:
|
||||
result = probe_config(cluster_bin_path, controller, cert_dir)
|
||||
result = probe_config(cluster_bin_path, controller)
|
||||
if not result.accepted:
|
||||
raise click.ClickException(result.reason) # 6.3
|
||||
click.echo(
|
||||
|
|
@ -75,7 +71,7 @@ def config_push(cluster_bin_path: str, controller: str, cert_dir: str, dry_run:
|
|||
f"(generation {result.current_generation}→{result.would_be_generation})"
|
||||
) # 6.2
|
||||
else:
|
||||
result = push_config(cluster_bin_path, controller, cert_dir)
|
||||
result = push_config(cluster_bin_path, controller)
|
||||
click.echo(f"versionTag={result.version_tag} ackedCount={result.acked_count}") # 2.5
|
||||
if result.error or result.acked_count <= 0:
|
||||
raise click.ClickException(result.error or "push rejected (ackedCount == 0)") # 2.7
|
||||
|
|
|
|||
|
|
@ -42,8 +42,6 @@ def _redirect_target(reply_bytes: bytes) -> tuple[str, int] | None:
|
|||
def follow_redirects(
|
||||
initial_host: str,
|
||||
initial_port: int,
|
||||
cert_dir: str, # noqa: ARG001 (signature parity with design.md; all
|
||||
# I/O, including TLS setup, is delegated to send_fn's own closure)
|
||||
send_fn,
|
||||
) -> tuple[str, int, bytes]:
|
||||
"""Send via ``send_fn(host, port) -> reply_bytes``, following up to 3
|
||||
|
|
|
|||
|
|
@ -79,9 +79,9 @@ def _split_controller(controller: str) -> tuple[str, int]:
|
|||
return controller, _DEFAULT_PORT
|
||||
|
||||
|
||||
def _send_via(request_bytes: bytes, cert_dir: str):
|
||||
def _send_via(request_bytes: bytes):
|
||||
def _send(host: str, port: int) -> bytes:
|
||||
sock = wire.connect(host, port, cert_dir)
|
||||
sock = wire.connect(host, port)
|
||||
try:
|
||||
return wire.send_request(sock, request_bytes)
|
||||
finally:
|
||||
|
|
@ -90,7 +90,7 @@ def _send_via(request_bytes: bytes, cert_dir: str):
|
|||
return _send
|
||||
|
||||
|
||||
def push_config(cluster_bin_path: str, controller: str, cert_dir: str) -> PushResult:
|
||||
def push_config(cluster_bin_path: str, controller: str) -> PushResult:
|
||||
"""Push ``cluster_bin_path`` to the controller cluster (2.1). Follows
|
||||
redirects to the primary (3.1-3.4) before sending.
|
||||
|
||||
|
|
@ -111,7 +111,7 @@ def push_config(cluster_bin_path: str, controller: str, cert_dir: str) -> PushRe
|
|||
request_bytes = request.to_bytes()
|
||||
|
||||
host, port = _split_controller(controller)
|
||||
_, _, reply_bytes = follow_redirects(host, port, cert_dir, _send_via(request_bytes, cert_dir))
|
||||
_, _, reply_bytes = follow_redirects(host, port, _send_via(request_bytes))
|
||||
|
||||
with mod.RequestReply.from_bytes(reply_bytes) as reply:
|
||||
if reply.status != 0:
|
||||
|
|
@ -129,7 +129,7 @@ def push_config(cluster_bin_path: str, controller: str, cert_dir: str) -> PushRe
|
|||
)
|
||||
|
||||
|
||||
def probe_config(cluster_bin_path: str, controller: str, cert_dir: str) -> DryRunResult:
|
||||
def probe_config(cluster_bin_path: str, controller: str) -> DryRunResult:
|
||||
"""Probe the controller's current config generation without pushing
|
||||
anything (6.1). ``cluster_bin_path`` is read only to confirm it
|
||||
exists and is readable before spending a network round-trip -- its
|
||||
|
|
@ -149,7 +149,7 @@ def probe_config(cluster_bin_path: str, controller: str, cert_dir: str) -> DryRu
|
|||
request_bytes = request.to_bytes()
|
||||
|
||||
host, port = _split_controller(controller)
|
||||
reply_bytes = _send_via(request_bytes, cert_dir)(host, port)
|
||||
reply_bytes = _send_via(request_bytes)(host, port)
|
||||
|
||||
with mod.ProbeConfigTagReply.from_bytes(reply_bytes) as reply:
|
||||
current_generation = reply.configTag
|
||||
|
|
|
|||
|
|
@ -12,9 +12,14 @@ Wire compatibility (verified against the real C implementation, not
|
|||
assumed):
|
||||
|
||||
- TLS posture mirrors the controller's own outbound client context exactly
|
||||
(``myapp.c:103-109``): ``SSL_CTX_new(TLS_client_method())`` +
|
||||
``SSL_CTX_set_verify(..., SSL_VERIFY_NONE, NULL)`` -- no CN/peer-cert
|
||||
validation anywhere in this trust model to diverge from.
|
||||
(``netio.c``'s ``create_ssl_context()``: when acting as a client, cert/key
|
||||
are both ``NULL`` -- ``TLS_client_method()`` with no
|
||||
``SSL_CTX_use_certificate_chain_file``/``SSL_CTX_use_PrivateKey_file``
|
||||
call at all, i.e. the client presents **no** certificate) plus
|
||||
``SSL_VERIFY_NONE``/``check_hostname=False`` -- no CN/peer-cert
|
||||
validation anywhere in this trust model to diverge from, and no client
|
||||
cert to load. Only the server side (controller, and this module's own
|
||||
test mock servers) loads a cert/key pair.
|
||||
- Framing matches ``request.c``'s ``xce_replconfig_req_ssl``
|
||||
(``src/lib/request.c:925-934``): an 8-byte ``uint64_t`` length header
|
||||
written via a raw ``SSL_write(ssl, &hdr, sizeof(hdr))`` -- host/native
|
||||
|
|
@ -46,9 +51,6 @@ _HEADER = struct.Struct("=Q")
|
|||
#: claiming to be larger than this is corrupt/hostile, not merely large.
|
||||
_MAX_FRAME_BYTES = 64 * 1024 * 1024
|
||||
|
||||
_CERT_FILENAME = "xce.crt"
|
||||
_KEY_FILENAME = "xce.key"
|
||||
|
||||
#: Timeout values below this are almost certainly a test deliberately
|
||||
#: forcing a fast timeout, not a production caller -- kept as a constant
|
||||
#: only so ``connect``'s default is named, not to gate behavior.
|
||||
|
|
@ -63,16 +65,14 @@ def _peername_or(sock: socket.socket, default: tuple[str, int]) -> tuple[str, in
|
|||
return default
|
||||
|
||||
|
||||
def connect(
|
||||
host: str, port: int, cert_dir: str, timeout_s: float = _DEFAULT_TIMEOUT_S
|
||||
) -> ssl.SSLSocket:
|
||||
def connect(host: str, port: int, timeout_s: float = _DEFAULT_TIMEOUT_S) -> ssl.SSLSocket:
|
||||
"""Open a TLS connection to a controller node.
|
||||
|
||||
Loads ``xce.crt``/``xce.key`` from ``cert_dir`` (2.3, 2.4, 7.1, 7.6) and
|
||||
establishes trust exactly as the controller's own outbound TLS does --
|
||||
``check_hostname=False``, ``verify_mode=ssl.CERT_NONE`` (no separate
|
||||
client CA flag or cert-issuance step, matching ``xce_test`` and
|
||||
``deploy-ha.sh``'s existing trust model).
|
||||
Establishes trust exactly as the controller's own outbound TLS does --
|
||||
no client certificate (matching ``netio.c``'s ``create_ssl_context()``
|
||||
with ``cert_file``/``key_file`` both ``NULL``), ``check_hostname=False``,
|
||||
``verify_mode=ssl.CERT_NONE`` (no CA flag or cert-issuance step,
|
||||
matching ``xce_test`` and ``deploy-ha.sh``'s existing trust model).
|
||||
|
||||
Raises :class:`ConnectionTimeoutError` if the TCP connect or the TLS
|
||||
handshake does not complete within ``timeout_s``. A refused connection
|
||||
|
|
@ -83,9 +83,6 @@ def connect(
|
|||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)
|
||||
context.check_hostname = False
|
||||
context.verify_mode = ssl.CERT_NONE
|
||||
context.load_cert_chain(
|
||||
certfile=f"{cert_dir}/{_CERT_FILENAME}", keyfile=f"{cert_dir}/{_KEY_FILENAME}"
|
||||
)
|
||||
|
||||
try:
|
||||
raw_sock = socket.create_connection((host, port), timeout=timeout_s)
|
||||
|
|
|
|||
44
src/xceadmin/tests/conftest.py
Normal file
44
src/xceadmin/tests/conftest.py
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
"""Shared pytest fixtures for xceadmin's test suite."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import shutil
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TlsCertPair:
|
||||
cert_file: Path
|
||||
key_file: Path
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def tls_certs(tmp_path_factory) -> TlsCertPair:
|
||||
"""A fresh self-signed TLS cert/key pair, generated once per test
|
||||
session via openssl -- not read from any checked-in fixture (this
|
||||
repo carries none; ``ha/data/`` was removed). Matches deploy-ha.sh's
|
||||
own cert generation: TLS verification is ``SSL_VERIFY_NONE``
|
||||
everywhere in this codebase (no hostname/identity check anywhere),
|
||||
so a fresh self-signed pair is exactly as valid a test double as a
|
||||
long-lived shared one.
|
||||
"""
|
||||
if shutil.which("openssl") is None:
|
||||
pytest.skip("openssl not found on PATH -- required to generate a test TLS cert")
|
||||
cert_dir = tmp_path_factory.mktemp("xceadmin-tls")
|
||||
cert_file = cert_dir / "xce.crt"
|
||||
key_file = cert_dir / "xce.key"
|
||||
subprocess.run(
|
||||
[
|
||||
"openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
||||
"-days", "3650",
|
||||
"-keyout", str(key_file), "-out", str(cert_file),
|
||||
"-subj", "/CN=xceadmin-test",
|
||||
],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
)
|
||||
return TlsCertPair(cert_file=cert_file, key_file=key_file)
|
||||
|
|
@ -19,11 +19,6 @@ from click.testing import CliRunner
|
|||
from xcecommon.schema import load_schema
|
||||
from xceadmin.cli import cli
|
||||
|
||||
REPO_HA_DATA_DIR = Path(__file__).resolve().parents[3] / "ha" / "data"
|
||||
CERT_FILE = REPO_HA_DATA_DIR / "xce.crt"
|
||||
KEY_FILE = REPO_HA_DATA_DIR / "xce.key"
|
||||
CERT_DIR = str(REPO_HA_DATA_DIR)
|
||||
|
||||
|
||||
def test_cli_help_exits_zero() -> None:
|
||||
"""`xceadmin --help` must run cleanly and print usage text."""
|
||||
|
|
@ -138,15 +133,17 @@ def test_config_build_missing_configs_dir_does_not_raw_traceback(tmp_path):
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _server_context() -> ssl.SSLContext:
|
||||
def _server_context(cert_file: Path, key_file: Path) -> ssl.SSLContext:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
|
||||
context.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file))
|
||||
return context
|
||||
|
||||
|
||||
class _MockController:
|
||||
def __init__(self, handler):
|
||||
def __init__(self, handler, cert_file: Path, key_file: Path):
|
||||
self._handler = handler
|
||||
self._cert_file = cert_file
|
||||
self._key_file = key_file
|
||||
self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._listener.bind(("127.0.0.1", 0))
|
||||
self._listener.listen(1)
|
||||
|
|
@ -160,7 +157,9 @@ class _MockController:
|
|||
except OSError:
|
||||
return
|
||||
try:
|
||||
tls_conn = _server_context().wrap_socket(raw_conn, server_side=True)
|
||||
tls_conn = _server_context(self._cert_file, self._key_file).wrap_socket(
|
||||
raw_conn, server_side=True
|
||||
)
|
||||
except (ssl.SSLError, OSError):
|
||||
raw_conn.close()
|
||||
return
|
||||
|
|
@ -176,18 +175,12 @@ class _MockController:
|
|||
self._thread.join(timeout=5)
|
||||
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (CERT_FILE.is_file() and KEY_FILE.is_file()),
|
||||
reason="ha/data/xce.crt or ha/data/xce.key not present",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_controller():
|
||||
def mock_controller(tls_certs):
|
||||
servers: list[_MockController] = []
|
||||
|
||||
def _make(handler) -> _MockController:
|
||||
srv = _MockController(handler)
|
||||
srv = _MockController(handler, tls_certs.cert_file, tls_certs.key_file)
|
||||
servers.append(srv)
|
||||
return srv
|
||||
|
||||
|
|
@ -223,7 +216,7 @@ def test_config_push_success_exits_zero(mock_controller, cluster_bin):
|
|||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}", "--cert-dir", CERT_DIR],
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}"],
|
||||
)
|
||||
|
||||
assert result.exit_code == 0, result.output
|
||||
|
|
@ -251,7 +244,7 @@ def test_config_push_zero_acked_count_exits_nonzero(mock_controller, cluster_bin
|
|||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}", "--cert-dir", CERT_DIR],
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{srv.port}"],
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
|
|
@ -267,7 +260,7 @@ def test_config_push_connection_refused_exits_nonzero_not_raw_traceback(cluster_
|
|||
runner = CliRunner()
|
||||
result = runner.invoke(
|
||||
cli,
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{port}", "--cert-dir", CERT_DIR],
|
||||
["config", "push", cluster_bin, "--controller", f"127.0.0.1:{port}"],
|
||||
)
|
||||
|
||||
assert result.exit_code != 0
|
||||
|
|
@ -297,8 +290,6 @@ def test_config_push_dry_run_reports_generations(mock_controller, cluster_bin):
|
|||
cluster_bin,
|
||||
"--controller",
|
||||
f"127.0.0.1:{srv.port}",
|
||||
"--cert-dir",
|
||||
CERT_DIR,
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -26,20 +26,10 @@ from xcecommon.errors import ConnectionTimeoutError, NoPrimaryError, ReplyDecode
|
|||
from xcecommon.schema import load_schema
|
||||
from xceadmin import push
|
||||
|
||||
REPO_HA_DATA_DIR = Path(__file__).resolve().parents[3] / "ha" / "data"
|
||||
CERT_FILE = REPO_HA_DATA_DIR / "xce.crt"
|
||||
KEY_FILE = REPO_HA_DATA_DIR / "xce.key"
|
||||
CERT_DIR = str(REPO_HA_DATA_DIR)
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (CERT_FILE.is_file() and KEY_FILE.is_file()),
|
||||
reason="ha/data/xce.crt or ha/data/xce.key not present",
|
||||
)
|
||||
|
||||
|
||||
def _server_context() -> ssl.SSLContext:
|
||||
def _server_context(cert_file: Path, key_file: Path) -> ssl.SSLContext:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
|
||||
context.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file))
|
||||
return context
|
||||
|
||||
|
||||
|
|
@ -48,8 +38,10 @@ class _MockController:
|
|||
accepted TLS socket to ``handler``, then closes.
|
||||
"""
|
||||
|
||||
def __init__(self, handler):
|
||||
def __init__(self, handler, cert_file: Path, key_file: Path):
|
||||
self._handler = handler
|
||||
self._cert_file = cert_file
|
||||
self._key_file = key_file
|
||||
self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._listener.bind(("127.0.0.1", 0))
|
||||
self._listener.listen(1)
|
||||
|
|
@ -63,7 +55,9 @@ class _MockController:
|
|||
except OSError:
|
||||
return
|
||||
try:
|
||||
tls_conn = _server_context().wrap_socket(raw_conn, server_side=True)
|
||||
tls_conn = _server_context(self._cert_file, self._key_file).wrap_socket(
|
||||
raw_conn, server_side=True
|
||||
)
|
||||
except (ssl.SSLError, OSError):
|
||||
raw_conn.close()
|
||||
return
|
||||
|
|
@ -80,11 +74,11 @@ class _MockController:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_controller():
|
||||
def mock_controller(tls_certs):
|
||||
servers: list[_MockController] = []
|
||||
|
||||
def _make(handler) -> _MockController:
|
||||
srv = _MockController(handler)
|
||||
srv = _MockController(handler, tls_certs.cert_file, tls_certs.key_file)
|
||||
servers.append(srv)
|
||||
return srv
|
||||
|
||||
|
|
@ -127,7 +121,7 @@ def test_push_config_successful_commit(mock_controller, cluster_bin):
|
|||
|
||||
srv = mock_controller(_handler)
|
||||
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
assert result.version_tag == 7
|
||||
assert result.acked_count == 3
|
||||
|
|
@ -166,7 +160,7 @@ def test_push_config_follows_one_redirect_to_primary(mock_controller, cluster_bi
|
|||
_standby_handler.primary_port = primary_srv.port
|
||||
standby_srv = mock_controller(_standby_handler)
|
||||
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{standby_srv.port}", CERT_DIR)
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{standby_srv.port}")
|
||||
|
||||
assert result.version_tag == 9
|
||||
assert result.acked_count == 2
|
||||
|
|
@ -184,7 +178,7 @@ def test_push_config_non_zero_status_reports_error_not_exception(mock_controller
|
|||
|
||||
srv = mock_controller(_handler)
|
||||
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
result = push.push_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
assert result.acked_count == 0
|
||||
assert "status 1" in result.error
|
||||
|
|
@ -203,7 +197,7 @@ def test_push_config_wrong_reply_variant_raises_reply_decode_error(mock_controll
|
|||
srv = mock_controller(_handler)
|
||||
|
||||
with pytest.raises(ReplyDecodeError):
|
||||
push.push_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
push.push_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
|
||||
def test_push_config_default_port_used_when_omitted():
|
||||
|
|
@ -239,7 +233,7 @@ def test_probe_config_reports_current_and_would_be_generation(mock_controller, c
|
|||
|
||||
srv = mock_controller(_handler)
|
||||
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
assert result.current_generation == 41
|
||||
assert result.would_be_generation == 42
|
||||
|
|
@ -263,7 +257,7 @@ def test_probe_config_zero_generation_still_accepted(mock_controller, cluster_bi
|
|||
|
||||
srv = mock_controller(_handler)
|
||||
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
assert result.current_generation == 0
|
||||
assert result.would_be_generation == 1
|
||||
|
|
@ -296,7 +290,7 @@ def test_probe_config_has_no_wire_level_rejection_signal_by_design(
|
|||
|
||||
srv = mock_controller(_handler)
|
||||
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}", CERT_DIR)
|
||||
result = push.probe_config(cluster_bin, f"127.0.0.1:{srv.port}")
|
||||
|
||||
assert result.accepted is True
|
||||
assert result.reason == ""
|
||||
|
|
@ -337,7 +331,7 @@ def test_push_config_propagates_too_many_redirects_end_to_end(mock_controller, c
|
|||
servers["e"] = mock_controller(_make_redirect_handler(lambda: 1))
|
||||
|
||||
with pytest.raises(TooManyRedirectsError):
|
||||
push.push_config(cluster_bin, f"127.0.0.1:{servers['a'].port}", CERT_DIR)
|
||||
push.push_config(cluster_bin, f"127.0.0.1:{servers['a'].port}")
|
||||
|
||||
|
||||
def test_push_config_propagates_no_primary_error_on_connection_timeout(cluster_bin, monkeypatch):
|
||||
|
|
@ -349,10 +343,10 @@ def test_push_config_propagates_no_primary_error_on_connection_timeout(cluster_b
|
|||
eventually raise on a genuine timeout.
|
||||
"""
|
||||
|
||||
def _fake_connect(host, port, cert_dir, timeout_s=30.0):
|
||||
def _fake_connect(host, port, timeout_s=30.0):
|
||||
raise ConnectionTimeoutError(f"timeout connecting to {host}:{port}", host, port)
|
||||
|
||||
monkeypatch.setattr(wire, "connect", _fake_connect)
|
||||
|
||||
with pytest.raises(NoPrimaryError):
|
||||
push.push_config(cluster_bin, "127.0.0.1:9991", CERT_DIR)
|
||||
push.push_config(cluster_bin, "127.0.0.1:9991")
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ def test_follow_redirects_no_redirect_returns_first_reply_unchanged():
|
|||
return reply
|
||||
|
||||
final_host, final_port, reply_bytes = discovery.follow_redirects(
|
||||
"node1", 9991, "ha/data", send_fn
|
||||
"node1", 9991, send_fn
|
||||
)
|
||||
|
||||
assert (final_host, final_port) == ("node1", 9991)
|
||||
|
|
@ -49,7 +49,7 @@ def test_follow_redirects_follows_one_redirect(capsys):
|
|||
return responses[(host, port)]
|
||||
|
||||
final_host, final_port, reply_bytes = discovery.follow_redirects(
|
||||
"node1", 9991, "ha/data", send_fn
|
||||
"node1", 9991, send_fn
|
||||
)
|
||||
|
||||
assert (final_host, final_port) == ("node2", 9991)
|
||||
|
|
@ -73,7 +73,7 @@ def test_follow_redirects_follows_exactly_three_hops(capsys):
|
|||
calls.append((host, port))
|
||||
return replies[(host, port)]
|
||||
|
||||
final_host, final_port, _ = discovery.follow_redirects("a", 1, "ha/data", send_fn)
|
||||
final_host, final_port, _ = discovery.follow_redirects("a", 1, send_fn)
|
||||
|
||||
assert (final_host, final_port) == ("d", 4)
|
||||
assert calls == [("a", 1), ("b", 2), ("c", 3), ("d", 4)]
|
||||
|
|
@ -92,7 +92,7 @@ def test_follow_redirects_raises_too_many_redirects_on_fourth_hop():
|
|||
return replies[(host, port)]
|
||||
|
||||
with pytest.raises(TooManyRedirectsError) as exc_info:
|
||||
discovery.follow_redirects("a", 1, "ha/data", send_fn)
|
||||
discovery.follow_redirects("a", 1, send_fn)
|
||||
|
||||
assert exc_info.value.hops == 4
|
||||
|
||||
|
|
@ -102,7 +102,7 @@ def test_follow_redirects_raises_no_primary_error_on_connection_timeout():
|
|||
raise ConnectionTimeoutError(f"timeout connecting to {host}:{port}", host, port)
|
||||
|
||||
with pytest.raises(NoPrimaryError) as exc_info:
|
||||
discovery.follow_redirects("node1", 9991, "ha/data", send_fn)
|
||||
discovery.follow_redirects("node1", 9991, send_fn)
|
||||
|
||||
assert exc_info.value.host == "node1"
|
||||
assert exc_info.value.port == 9991
|
||||
|
|
@ -117,7 +117,7 @@ def test_follow_redirects_no_primary_error_names_last_hop_not_initial_hop():
|
|||
return replies[(host, port)]
|
||||
|
||||
with pytest.raises(NoPrimaryError) as exc_info:
|
||||
discovery.follow_redirects("a", 1, "ha/data", send_fn)
|
||||
discovery.follow_redirects("a", 1, send_fn)
|
||||
|
||||
assert exc_info.value.host == "b"
|
||||
assert exc_info.value.port == 2
|
||||
|
|
@ -133,7 +133,7 @@ def test_follow_redirects_zero_port_is_not_treated_as_a_redirect():
|
|||
return reply
|
||||
|
||||
final_host, final_port, reply_bytes = discovery.follow_redirects(
|
||||
"node1", 9991, "ha/data", send_fn
|
||||
"node1", 9991, send_fn
|
||||
)
|
||||
|
||||
assert (final_host, final_port) == ("node1", 9991)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
"""Unit tests for ``xcecommon.wire`` (design.md, ``xcecommon.wire``;
|
||||
Requirements 2.1, 2.3, 2.4, 5.2, 6.1, 7.1, 7.3, 7.6).
|
||||
|
||||
Uses a real local TLS loopback server (threading) wrapped with this
|
||||
repo's existing ``ha/data/xce.crt``/``xce.key`` -- the same cert pair
|
||||
``deploy-ha.sh`` already copies for controller-to-controller TLS -- so
|
||||
these tests exercise the real trust model (no CA, ``SSL_VERIFY_NONE``),
|
||||
not a synthetic substitute.
|
||||
Uses a real local TLS loopback server (threading) wrapped with a
|
||||
self-signed cert/key generated fresh per test session (see conftest.py's
|
||||
``tls_certs`` fixture, same generation approach ``deploy-ha.sh`` uses) --
|
||||
so these tests exercise the real trust model (no CA, ``SSL_VERIFY_NONE``,
|
||||
no client certificate -- matching ``netio.c``'s client-side
|
||||
``create_ssl_context(NULL, NULL)``), not a synthetic substitute.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
|
@ -21,19 +22,10 @@ import pytest
|
|||
from xcecommon import wire
|
||||
from xcecommon.errors import ConnectionTimeoutError, ReplyDecodeError
|
||||
|
||||
REPO_HA_DATA_DIR = Path(__file__).resolve().parents[3] / "ha" / "data"
|
||||
CERT_FILE = REPO_HA_DATA_DIR / "xce.crt"
|
||||
KEY_FILE = REPO_HA_DATA_DIR / "xce.key"
|
||||
|
||||
pytestmark = pytest.mark.skipif(
|
||||
not (CERT_FILE.is_file() and KEY_FILE.is_file()),
|
||||
reason="ha/data/xce.crt or ha/data/xce.key not present",
|
||||
)
|
||||
|
||||
|
||||
def _server_context() -> ssl.SSLContext:
|
||||
def _server_context(cert_file: Path, key_file: Path) -> ssl.SSLContext:
|
||||
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||
context.load_cert_chain(certfile=str(CERT_FILE), keyfile=str(KEY_FILE))
|
||||
context.load_cert_chain(certfile=str(cert_file), keyfile=str(key_file))
|
||||
return context
|
||||
|
||||
|
||||
|
|
@ -43,8 +35,10 @@ class _TlsEchoServer:
|
|||
receives the wrapped :class:`ssl.SSLSocket`.
|
||||
"""
|
||||
|
||||
def __init__(self, handler):
|
||||
def __init__(self, handler, cert_file: Path, key_file: Path):
|
||||
self._handler = handler
|
||||
self._cert_file = cert_file
|
||||
self._key_file = key_file
|
||||
self._listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self._listener.bind(("127.0.0.1", 0))
|
||||
self._listener.listen(1)
|
||||
|
|
@ -58,7 +52,9 @@ class _TlsEchoServer:
|
|||
except OSError:
|
||||
return
|
||||
try:
|
||||
tls_conn = _server_context().wrap_socket(raw_conn, server_side=True)
|
||||
tls_conn = _server_context(self._cert_file, self._key_file).wrap_socket(
|
||||
raw_conn, server_side=True
|
||||
)
|
||||
except (ssl.SSLError, OSError):
|
||||
raw_conn.close()
|
||||
return
|
||||
|
|
@ -75,7 +71,7 @@ class _TlsEchoServer:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def echo_server():
|
||||
def echo_server(tls_certs):
|
||||
"""A server that reads exactly one frame and writes it straight back."""
|
||||
servers: list[_TlsEchoServer] = []
|
||||
|
||||
|
|
@ -91,7 +87,7 @@ def echo_server():
|
|||
wire.send_frame(conn, payload)
|
||||
|
||||
def _make() -> _TlsEchoServer:
|
||||
srv = _TlsEchoServer(_handler)
|
||||
srv = _TlsEchoServer(_handler, tls_certs.cert_file, tls_certs.key_file)
|
||||
servers.append(srv)
|
||||
return srv
|
||||
|
||||
|
|
@ -100,9 +96,6 @@ def echo_server():
|
|||
srv.close()
|
||||
|
||||
|
||||
CERT_DIR = str(REPO_HA_DATA_DIR)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# connect()
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
@ -110,7 +103,7 @@ CERT_DIR = str(REPO_HA_DATA_DIR)
|
|||
|
||||
def test_connect_establishes_real_tls_session(echo_server):
|
||||
srv = echo_server()
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
assert isinstance(sock, ssl.SSLSocket)
|
||||
assert sock.getpeername()[1] == srv.port
|
||||
|
|
@ -125,7 +118,7 @@ def test_connect_refused_propagates_unwrapped():
|
|||
listener.close() # nothing listens on this port now
|
||||
|
||||
with pytest.raises(ConnectionRefusedError):
|
||||
wire.connect("127.0.0.1", port, CERT_DIR, timeout_s=5.0)
|
||||
wire.connect("127.0.0.1", port, timeout_s=5.0)
|
||||
|
||||
|
||||
def test_connect_handshake_timeout_raises_connection_timeout_error_and_closes_cleanly():
|
||||
|
|
@ -151,7 +144,7 @@ def test_connect_handshake_timeout_raises_connection_timeout_error_and_closes_cl
|
|||
thread.start()
|
||||
try:
|
||||
with pytest.raises(ConnectionTimeoutError) as exc_info:
|
||||
wire.connect("127.0.0.1", port, CERT_DIR, timeout_s=0.3)
|
||||
wire.connect("127.0.0.1", port, timeout_s=0.3)
|
||||
assert exc_info.value.host == "127.0.0.1"
|
||||
assert exc_info.value.port == port
|
||||
|
||||
|
|
@ -175,7 +168,7 @@ def test_connect_handshake_timeout_raises_connection_timeout_error_and_closes_cl
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_send_frame_writes_exact_known_good_byte_sequence(echo_server):
|
||||
def test_send_frame_writes_exact_known_good_byte_sequence(tls_certs):
|
||||
"""Requirement: framing matches the controller's own byte-for-byte,
|
||||
verified against a known-good byte sequence (not just a round-trip).
|
||||
"""
|
||||
|
|
@ -191,9 +184,9 @@ def test_send_frame_writes_exact_known_good_byte_sequence(echo_server):
|
|||
break
|
||||
captured.extend(chunk)
|
||||
|
||||
srv = _TlsEchoServer(_capture_handler)
|
||||
srv = _TlsEchoServer(_capture_handler, tls_certs.cert_file, tls_certs.key_file)
|
||||
try:
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
wire.send_frame(sock, b"xce!")
|
||||
finally:
|
||||
|
|
@ -209,7 +202,7 @@ def test_send_frame_writes_exact_known_good_byte_sequence(echo_server):
|
|||
|
||||
def test_recv_frame_round_trips_arbitrary_payload(echo_server):
|
||||
srv = echo_server()
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
payload = b"\x00\x01\xffcapnp-shaped-bytes" * 100
|
||||
wire.send_frame(sock, payload)
|
||||
|
|
@ -220,7 +213,7 @@ def test_recv_frame_round_trips_arbitrary_payload(echo_server):
|
|||
|
||||
def test_recv_frame_round_trips_empty_payload(echo_server):
|
||||
srv = echo_server()
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
wire.send_frame(sock, b"")
|
||||
assert wire.recv_frame(sock) == b""
|
||||
|
|
@ -228,13 +221,13 @@ def test_recv_frame_round_trips_empty_payload(echo_server):
|
|||
sock.close()
|
||||
|
||||
|
||||
def test_recv_frame_raises_reply_decode_error_on_truncated_header():
|
||||
def test_recv_frame_raises_reply_decode_error_on_truncated_header(tls_certs):
|
||||
def _close_after_partial_header(conn: ssl.SSLSocket) -> None:
|
||||
conn.sendall(b"\x01\x02\x03") # 3 of 8 header bytes, then hang up
|
||||
|
||||
srv = _TlsEchoServer(_close_after_partial_header)
|
||||
srv = _TlsEchoServer(_close_after_partial_header, tls_certs.cert_file, tls_certs.key_file)
|
||||
try:
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
with pytest.raises(ReplyDecodeError):
|
||||
wire.recv_frame(sock)
|
||||
|
|
@ -244,14 +237,14 @@ def test_recv_frame_raises_reply_decode_error_on_truncated_header():
|
|||
srv.close()
|
||||
|
||||
|
||||
def test_recv_frame_raises_reply_decode_error_on_truncated_payload():
|
||||
def test_recv_frame_raises_reply_decode_error_on_truncated_payload(tls_certs):
|
||||
def _promise_more_than_sent(conn: ssl.SSLSocket) -> None:
|
||||
conn.sendall(struct.pack("=Q", 100))
|
||||
conn.sendall(b"only ten!") # 9 bytes, not 100 -- then hang up
|
||||
|
||||
srv = _TlsEchoServer(_promise_more_than_sent)
|
||||
srv = _TlsEchoServer(_promise_more_than_sent, tls_certs.cert_file, tls_certs.key_file)
|
||||
try:
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
with pytest.raises(ReplyDecodeError):
|
||||
wire.recv_frame(sock)
|
||||
|
|
@ -261,13 +254,13 @@ def test_recv_frame_raises_reply_decode_error_on_truncated_payload():
|
|||
srv.close()
|
||||
|
||||
|
||||
def test_recv_frame_raises_reply_decode_error_on_implausible_length():
|
||||
def test_recv_frame_raises_reply_decode_error_on_implausible_length(tls_certs):
|
||||
def _claim_huge_length(conn: ssl.SSLSocket) -> None:
|
||||
conn.sendall(struct.pack("=Q", wire._MAX_FRAME_BYTES + 1))
|
||||
|
||||
srv = _TlsEchoServer(_claim_huge_length)
|
||||
srv = _TlsEchoServer(_claim_huge_length, tls_certs.cert_file, tls_certs.key_file)
|
||||
try:
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
with pytest.raises(ReplyDecodeError):
|
||||
wire.recv_frame(sock)
|
||||
|
|
@ -284,7 +277,7 @@ def test_recv_frame_raises_reply_decode_error_on_implausible_length():
|
|||
|
||||
def test_send_request_returns_exact_reply_bytes(echo_server):
|
||||
srv = echo_server()
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
reply = wire.send_request(sock, b"pretend-capnp-request-bytes")
|
||||
assert reply == b"pretend-capnp-request-bytes"
|
||||
|
|
@ -298,7 +291,7 @@ def test_send_request_is_opcode_agnostic_works_for_any_byte_shape(echo_server):
|
|||
identically, since both are just bytes to this module.
|
||||
"""
|
||||
srv = echo_server()
|
||||
sock = wire.connect("127.0.0.1", srv.port, CERT_DIR, timeout_s=5.0)
|
||||
sock = wire.connect("127.0.0.1", srv.port, timeout_s=5.0)
|
||||
try:
|
||||
for payload in (b"", b"\x00", b"short", b"x" * 10_000):
|
||||
reply = wire.send_request(sock, payload)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue