Restore LLM_CA_CERT_URLS support for corporate PKI HTTP cert delivery. Downloaded certificates are installed into the llm/ CA category and share the same system trust + NSS import path as CERTS_PATH-mounted certs. Changes: - certs.sh: add download_llm_ca_certs with HTTP-only curl policy, PEM/DER handling, retries, llm/ storage, hash symlinks for llm+custom - backend/agent entrypoints: run download before install_all_certs - compose/env/docs: expose LLM_CA_CERT_URLS again and update ADR-0009 - integration tests: cover PEM/DER/CER, invalid certs, non-HTTP skips, query-string filenames, private/server skips, empty inputs, combined LLM_CA_CERT_URLS + CERTS_PATH channels, and NSS imports Verified: - 194 passed, 1 skipped integration tests - full backend container smoke passed with testcontainers PostgreSQL - docker bundle rebuilt for 0.5.0
596 lines
28 KiB
Python
596 lines
28 KiB
Python
# #region TestBackendContainer [C:3] [TYPE Module] [SEMANTICS test,integration,backend,container,docker,entrypoint,certs]
|
|
# @BRIEF Integration tests for the superset-tools-backend Docker image:
|
|
# verifies entrypoint + certs.sh work correctly inside the container.
|
|
# @RELATION BINDS_TO -> [docker/backend.Dockerfile]
|
|
# @RELATION BINDS_TO -> [docker/certs.sh]
|
|
# @RELATION BINDS_TO -> [docker/backend.entrypoint.sh]
|
|
# @RELATION CALLS -> [TestSupersetTlsCustomCA]
|
|
# @RATIONALE In 0.5.0 release, backend entrypoint crashed with
|
|
# "/app/entrypoint.sh: line 264: /app/certs.sh: No such file or directory"
|
|
# because docker/backend.Dockerfile was missing COPY for certs.sh.
|
|
# Existing testcontainers tests only exercise apache/superset containers,
|
|
# not the superset-tools backend container. These tests close that gap.
|
|
# @TEST_CONTRACT BackendImageCertPaths ->
|
|
# /app/certs.sh exists and is readable
|
|
# source /app/certs.sh defines install_all_certs, install_ca_to_nss, normalize_cert
|
|
# /app/entrypoint.sh exists and is executable
|
|
# @TEST_CONTRACT BackendContainerSmoke ->
|
|
# docker compose up backend starts successfully
|
|
# /api/health/summary responds 200
|
|
# Container logs do NOT contain "certs.sh: No such file or directory"
|
|
# Container logs do NOT contain "Traceback" or unrecoverable startup error
|
|
# @REQUIRES Docker daemon running. --run-integration flag.
|
|
"""Integration tests for superset-tools-backend Docker image.
|
|
|
|
Two test suites:
|
|
|
|
1. **TestBackendImageCertsSh** — fast (no DB, no compose).
|
|
Builds the backend Docker image, runs a container with --entrypoint bash,
|
|
and verifies certs.sh is present, sourceable, and all certificate
|
|
installation functions are defined.
|
|
|
|
2. **TestBackendContainerSmoke** — full stack (needs PostgreSQL).
|
|
Starts PostgreSQL via testcontainers, deploys backend via docker compose,
|
|
waits for the health endpoint, and checks container logs for startup errors.
|
|
"""
|
|
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
# ── Project paths ────────────────────────────────────────────────────────
|
|
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
|
BACKEND_IMAGE = "superset-tools-backend:test-integration"
|
|
|
|
|
|
# ── Fixtures ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def _backend_image():
|
|
"""Build superset-tools-backend:test-integration once per module."""
|
|
# Rebuild if Dockerfile, entrypoint, or cert logic changed.
|
|
dockerfile = PROJECT_ROOT / "docker" / "backend.Dockerfile"
|
|
certs_sh = PROJECT_ROOT / "docker" / "certs.sh"
|
|
entrypoint = PROJECT_ROOT / "docker" / "backend.entrypoint.sh"
|
|
image_marker = PROJECT_ROOT / "backend" / ".image-built"
|
|
|
|
watched_mtime = max(p.stat().st_mtime for p in (dockerfile, certs_sh, entrypoint))
|
|
if not image_marker.exists() or image_marker.stat().st_mtime < watched_mtime:
|
|
subprocess.run(
|
|
["docker", "build", "-f", str(dockerfile), "-t", BACKEND_IMAGE, str(PROJECT_ROOT)],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
)
|
|
image_marker.touch()
|
|
return BACKEND_IMAGE
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# TEST 1: Fast — verify certs.sh presence and basic functionality
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestBackendImageCertPaths:
|
|
"""Fast checks: certs.sh exists, is sourceable, defines all functions.
|
|
|
|
These tests run in ~10s without PostgreSQL or docker compose.
|
|
They catch the 'certs.sh: No such file or directory' failure mode.
|
|
"""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _image(self, _backend_image):
|
|
pass
|
|
|
|
# ── helpers ─────────────────────────────────────────────────────
|
|
|
|
@staticmethod
|
|
def _run_bash(command: str) -> subprocess.CompletedProcess:
|
|
return subprocess.run(
|
|
["docker", "run", "--rm", "--entrypoint", "bash", BACKEND_IMAGE, "-c", command],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=30,
|
|
)
|
|
|
|
# ── tests ───────────────────────────────────────────────────────
|
|
|
|
# #region test_certs_sh_exists [C:1] [TYPE Function] [SEMANTICS test,integration,certs]
|
|
# @BRIEF /app/certs.sh must exist and be readable.
|
|
def test_certs_sh_exists(self):
|
|
r = self._run_bash("test -f /app/certs.sh && test -r /app/certs.sh")
|
|
assert r.returncode == 0, f"File exists: {r.returncode == 0}, stderr: {r.stderr[:200]}"
|
|
|
|
# #endregion test_certs_sh_exists
|
|
|
|
# #region test_entrypoint_sh_exists [C:1] [TYPE Function] [SEMANTICS test,integration,entrypoint]
|
|
# @BRIEF /app/entrypoint.sh must exist and be executable.
|
|
def test_entrypoint_sh_exists(self):
|
|
r = self._run_bash("test -f /app/entrypoint.sh && test -x /app/entrypoint.sh")
|
|
assert r.returncode == 0, f"File: /app/entrypoint.sh, stderr: {r.stderr[:200]}"
|
|
|
|
# #endregion test_entrypoint_sh_exists
|
|
|
|
# #region test_certs_sh_source_defines_functions [C:2] [TYPE Function] [SEMANTICS test,integration,certs]
|
|
# @BRIEF sourcing /app/certs.sh defines install_all_certs, install_ca_to_nss, normalize_cert.
|
|
def test_certs_sh_source_defines_functions(self):
|
|
r = self._run_bash("source /app/certs.sh && declare -F install_all_certs >/dev/null 2>&1 && echo -n 'install_all_certs '; declare -F install_ca_to_nss >/dev/null 2>&1 && echo -n 'install_ca_to_nss '; declare -F normalize_cert >/dev/null 2>&1 && echo -n 'normalize_cert'")
|
|
assert "install_all_certs" in r.stdout, f"install_all_certs not defined:\n{r.stderr}"
|
|
assert "install_ca_to_nss" in r.stdout, f"install_ca_to_nss not defined:\n{r.stderr}"
|
|
assert "normalize_cert" in r.stdout, f"normalize_cert not defined:\n{r.stderr}"
|
|
|
|
# #endregion test_certs_sh_source_defines_functions
|
|
|
|
# #region test_entrypoint_sources_certs_sh_ok [C:2] [TYPE Function] [SEMANTICS test,integration,entrypoint]
|
|
# @BRIEF Running the real entrypoint script (without DB) must not crash on source certs.sh.
|
|
# We pass a short DB wait timeout so it fails fast instead of retrying 30 times.
|
|
# The certs.sh sourcing happens BEFORE the DB wait loop.
|
|
def test_entrypoint_sources_certs_sh_ok(self):
|
|
r = subprocess.run(
|
|
[
|
|
"docker",
|
|
"run",
|
|
"--rm",
|
|
"-e",
|
|
"POSTGRES_HOST=does-not-exist",
|
|
"-e",
|
|
"POSTGRES_USER=test",
|
|
"-e",
|
|
"POSTGRES_PASSWORD=test",
|
|
"-e",
|
|
"POSTGRES_DB=test",
|
|
# Reduce wait time — if certs.sh source succeeds, the error
|
|
# will be about DB connection, not certs.sh
|
|
"--entrypoint",
|
|
"bash",
|
|
BACKEND_IMAGE,
|
|
"-c",
|
|
"timeout 5 /app/entrypoint.sh 2>&1 || true",
|
|
],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=15,
|
|
)
|
|
# The certs.sh source comes BEFORE DB wait.
|
|
# If certs.sh is missing, the output MUST contain "No such file or directory".
|
|
# If certs.sh loads OK, the output should show DB connection error OR cert install.
|
|
assert re.search(r"No such file or directory.*certs\.sh|certs\.sh.*No such file or directory", r.stdout + r.stderr) is None, f"certs.sh not found! Entrypoint tried to source missing file:\nSTDOUT:\n{r.stdout[:2000]}\nSTDERR:\n{r.stderr[:2000]}"
|
|
|
|
# #endregion test_entrypoint_sources_certs_sh_ok
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# TEST 2: Full stack — docker compose up backend + health check
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not os.environ.get("E2E_BACKEND_CONTAINER"),
|
|
reason="Skipped by default — set E2E_BACKEND_CONTAINER=1 to run full-stack test",
|
|
)
|
|
@pytest.mark.usefixtures("_backend_image")
|
|
class TestBackendContainerSmoke:
|
|
"""Full-stack smoke test: deploy backend container via docker compose
|
|
with PostgreSQL, wait for health endpoint, verify container logs.
|
|
|
|
Requires: set E2E_BACKEND_CONTAINER=1 (takes 30-60s, needs PostgreSQL).
|
|
"""
|
|
|
|
# #region test_backend_health [C:3] [TYPE Function] [SEMANTICS test,integration,smoke]
|
|
# @BRIEF docker run (host network) backend with testcontainers PostgreSQL,
|
|
# wait for health, check container logs for startup errors.
|
|
# @PRE Docker daemon running. Port 8000 must be free on the host.
|
|
# @POST Backend health endpoint responds 200. Container logs show no certs.sh errors.
|
|
# @SIDE_EFFECT Starts/stops PostgreSQL and backend Docker containers.
|
|
def test_backend_health(self):
|
|
import time as time_module
|
|
import urllib.request
|
|
import urllib.error
|
|
|
|
# ── Use host network mode so backend can reach PG on localhost ──
|
|
from testcontainers.postgres import PostgresContainer
|
|
|
|
pg = PostgresContainer(
|
|
"postgres:16-alpine",
|
|
dbname="ss_tools",
|
|
username="postgres",
|
|
password="postgres",
|
|
)
|
|
pg.start()
|
|
pg_port = pg.get_exposed_port(5432)
|
|
|
|
# ── Start backend container (host network) ──
|
|
container_name = f"ss-tools-integration-backend-{os.urandom(4).hex()}"
|
|
backend_port = "8000"
|
|
db_url = f"postgresql://postgres:postgres@127.0.0.1:{pg_port}/ss_tools"
|
|
run_cmd = [
|
|
"docker",
|
|
"run",
|
|
"-d",
|
|
"--name",
|
|
container_name,
|
|
"--network",
|
|
"host",
|
|
"-e",
|
|
f"DATABASE_URL={db_url}",
|
|
"-e",
|
|
"INITIAL_ADMIN_CREATE=true",
|
|
"-e",
|
|
"INITIAL_ADMIN_USERNAME=admin",
|
|
"-e",
|
|
"INITIAL_ADMIN_PASSWORD=admin123",
|
|
"-e",
|
|
"INITIAL_ADMIN_EMAIL=admin@example.com",
|
|
"-e",
|
|
"ENABLE_BELIEF_STATE_LOGGING=true",
|
|
"-e",
|
|
"TASK_LOG_LEVEL=ERROR",
|
|
"-e",
|
|
"CERTS_PATH=/opt/certs",
|
|
BACKEND_IMAGE,
|
|
]
|
|
|
|
r = subprocess.run(run_cmd, capture_output=True, text=True, timeout=30)
|
|
if r.returncode != 0:
|
|
pg.stop()
|
|
pytest.fail(f"docker run failed:\nSTDERR: {r.stderr[:2000]}")
|
|
|
|
container_id = r.stdout.strip()
|
|
container_removed = False
|
|
|
|
def _cleanup(rm_container=True):
|
|
nonlocal container_removed
|
|
if rm_container and not container_removed:
|
|
subprocess.run(["docker", "rm", "-f", container_id], capture_output=True, timeout=10)
|
|
container_removed = True
|
|
try:
|
|
pg.stop()
|
|
except Exception:
|
|
pass
|
|
|
|
try:
|
|
# ── Wait for health ──
|
|
health_url = f"http://127.0.0.1:{backend_port}/api/health/summary"
|
|
deadline = time_module.time() + 60
|
|
last_error = ""
|
|
|
|
while time_module.time() < deadline:
|
|
try:
|
|
hr = subprocess.run(
|
|
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--connect-timeout", "2", "--max-time", "4", health_url],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
http_code = hr.stdout.strip()
|
|
if http_code in ("200", "401", "403"):
|
|
break
|
|
last_error = f"HTTP {http_code}"
|
|
except subprocess.TimeoutExpired:
|
|
last_error = "curl timeout"
|
|
except Exception as e:
|
|
last_error = str(e)
|
|
time_module.sleep(2)
|
|
else:
|
|
# Timeout — dump container logs
|
|
logs_r = subprocess.run(
|
|
["docker", "logs", container_id, "--tail", "60"],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
full_log = logs_r.stdout[:5000] + logs_r.stderr[:2000]
|
|
_cleanup(rm_container=True)
|
|
pytest.fail(f"Backend health check failed after 60s.\nLast error: {last_error}\nContainer logs:\n{full_log}")
|
|
|
|
# ── Check container logs for startup errors ──
|
|
logs_r = subprocess.run(
|
|
["docker", "logs", container_id],
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=10,
|
|
)
|
|
combined_logs = logs_r.stdout + logs_r.stderr
|
|
|
|
if re.search(r"No such file or directory.*certs\.sh|certs\.sh.*No such file", combined_logs):
|
|
_cleanup(rm_container=True)
|
|
pytest.fail(f"certs.sh not found! Entrypoint crashed:\n{combined_logs[:3000]}")
|
|
|
|
if re.search(r"Traceback|Error.*certs\.sh", combined_logs):
|
|
_cleanup(rm_container=True)
|
|
pytest.fail(f"Entrypoint crash detected:\n{combined_logs[:3000]}")
|
|
|
|
finally:
|
|
_cleanup(rm_container=True)
|
|
|
|
# #endregion test_backend_health
|
|
|
|
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
# TEST 3: cert download — download_llm_ca_certs via HTTP
|
|
# ══════════════════════════════════════════════════════════════════════════
|
|
|
|
|
|
class TestBackendImageCertDownload:
|
|
"""Verify download_llm_ca_certs downloads certs from HTTP URLs."""
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _image(self, _backend_image):
|
|
pass
|
|
|
|
# #region _make_ca_cert [C:1] [TYPE Function]
|
|
@staticmethod
|
|
def _make_ca_cert(certs_dir: str, name: str = "test-download-ca") -> tuple[str, str]:
|
|
ca_key = os.path.join(certs_dir, f"{name}.key")
|
|
ca_crt = os.path.join(certs_dir, f"{name}.crt")
|
|
subprocess.run(
|
|
[
|
|
"openssl", "req", "-x509", "-newkey", "rsa:2048", "-nodes",
|
|
"-keyout", ca_key, "-out", ca_crt,
|
|
"-days", "1",
|
|
"-subj", f"/CN={name}-for-container",
|
|
],
|
|
check=True, capture_output=True, text=True, timeout=15,
|
|
)
|
|
return ca_key, ca_crt
|
|
# #endregion _make_ca_cert
|
|
|
|
# #region _serve_dir [C:1] [TYPE Function]
|
|
@staticmethod
|
|
def _serve_dir(certs_dir: str):
|
|
import http.server
|
|
import socket
|
|
import socketserver
|
|
import threading
|
|
|
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
s.bind(("127.0.0.1", 0))
|
|
http_port = s.getsockname()[1]
|
|
|
|
class _Handler(http.server.SimpleHTTPRequestHandler):
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, directory=certs_dir, **kwargs)
|
|
|
|
httpd = socketserver.TCPServer(("127.0.0.1", http_port), _Handler)
|
|
server_thread = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
server_thread.start()
|
|
return httpd, http_port
|
|
# #endregion _serve_dir
|
|
|
|
# #region _run_download [C:1] [TYPE Function]
|
|
@staticmethod
|
|
def _run_download(urls: str, command_suffix: str = "") -> subprocess.CompletedProcess:
|
|
command = "source /app/certs.sh && download_llm_ca_certs"
|
|
if command_suffix:
|
|
command = f"{command} && {command_suffix}"
|
|
return subprocess.run(
|
|
[
|
|
"docker", "run", "--rm",
|
|
"--network", "host",
|
|
"-e", f"LLM_CA_CERT_URLS={urls}",
|
|
"--entrypoint", "bash",
|
|
BACKEND_IMAGE,
|
|
"-c", command,
|
|
],
|
|
capture_output=True, text=True, timeout=45,
|
|
)
|
|
# #endregion _run_download
|
|
|
|
# #region test_download_ca_cert_over_http [C:3] [TYPE Function] [SEMANTICS test,integration,certs,download]
|
|
# @BRIEF download_llm_ca_certs() fetches a PEM cert from an HTTP server
|
|
# and installs it into /usr/local/share/ca-certificates/llm/.
|
|
def test_download_ca_cert_over_http(self):
|
|
import tempfile
|
|
|
|
# ── Generate a test CA cert ──
|
|
with tempfile.TemporaryDirectory() as certs_dir:
|
|
self._make_ca_cert(certs_dir, "test-download-ca")
|
|
httpd, http_port = self._serve_dir(certs_dir)
|
|
|
|
cert_url = f"http://127.0.0.1:{http_port}/test-download-ca.crt"
|
|
|
|
try:
|
|
# ── Run download_llm_ca_certs in container (host network) ──
|
|
r = self._run_download(cert_url, "echo 'DOWNLOAD_OK' && ls -la /usr/local/share/ca-certificates/llm/")
|
|
|
|
assert r.returncode == 0, (
|
|
f"download_llm_ca_certs failed (exit {r.returncode}):\n"
|
|
f"STDOUT: {r.stdout[:2000]}\nSTDERR: {r.stderr[:2000]}"
|
|
)
|
|
|
|
# Verify download succeeded
|
|
assert "DOWNLOAD_OK" in r.stdout, (
|
|
f"download_llm_ca_certs did not complete:\n{r.stdout[:2000]}"
|
|
)
|
|
assert "Скачано и установлено: 1" in r.stdout, (
|
|
f"Expected 1 downloaded cert:\n{r.stdout[:2000]}"
|
|
)
|
|
assert "test-download-ca.crt" in r.stdout, (
|
|
f"Cert file not found in llm/ dir:\n{r.stdout[:2000]}"
|
|
)
|
|
|
|
finally:
|
|
httpd.shutdown()
|
|
# #endregion test_download_ca_cert_over_http
|
|
|
|
# #region test_download_der_cert_converts_to_pem [C:2] [TYPE Function] [SEMANTICS test,integration,certs,download]
|
|
# @BRIEF DER certificate downloaded over HTTP is converted to PEM .crt.
|
|
def test_download_der_cert_converts_to_pem(self):
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as certs_dir:
|
|
_, ca_crt = self._make_ca_cert(certs_dir, "test-download-der")
|
|
der_path = os.path.join(certs_dir, "test-download-der.der")
|
|
subprocess.run(
|
|
["openssl", "x509", "-in", ca_crt, "-outform", "DER", "-out", der_path],
|
|
check=True, capture_output=True, text=True, timeout=15,
|
|
)
|
|
httpd, http_port = self._serve_dir(certs_dir)
|
|
try:
|
|
url = f"http://127.0.0.1:{http_port}/test-download-der.der"
|
|
r = self._run_download(url, "grep -q 'BEGIN CERTIFICATE' /usr/local/share/ca-certificates/llm/test-download-der.crt && echo DER_OK")
|
|
assert r.returncode == 0, f"DER download failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "DER_OK" in r.stdout
|
|
assert "Формат: DER" in r.stdout
|
|
finally:
|
|
httpd.shutdown()
|
|
# #endregion test_download_der_cert_converts_to_pem
|
|
|
|
# #region test_download_skips_invalid_and_non_http [C:2] [TYPE Function] [SEMANTICS test,integration,certs,download]
|
|
# @BRIEF Invalid content and HTTPS URLs are skipped without failing valid downloads.
|
|
def test_download_skips_invalid_and_non_http(self):
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as certs_dir:
|
|
self._make_ca_cert(certs_dir, "valid-ca")
|
|
Path(certs_dir, "not-a-cert.crt").write_text("not a certificate", encoding="utf-8")
|
|
httpd, http_port = self._serve_dir(certs_dir)
|
|
try:
|
|
urls = " ".join([
|
|
f"http://127.0.0.1:{http_port}/valid-ca.crt",
|
|
f"http://127.0.0.1:{http_port}/not-a-cert.crt",
|
|
"https://example.invalid/blocked.crt",
|
|
])
|
|
r = self._run_download(urls, "ls -la /usr/local/share/ca-certificates/llm/")
|
|
assert r.returncode == 0, f"Mixed download failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "Скачано и установлено: 1" in r.stdout
|
|
assert "невалид" in r.stdout or "Неизвестный формат" in r.stdout
|
|
assert "пропускаем не-HTTP URL" in r.stdout
|
|
assert "valid-ca.crt" in r.stdout
|
|
finally:
|
|
httpd.shutdown()
|
|
# #endregion test_download_skips_invalid_and_non_http
|
|
|
|
# #region test_download_query_string_filename_and_nss_import [C:2] [TYPE Function] [SEMANTICS test,integration,certs,nss]
|
|
# @BRIEF URL query strings are stripped from filenames and downloaded llm/ certs import into NSS.
|
|
def test_download_query_string_filename_and_nss_import(self):
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as certs_dir:
|
|
self._make_ca_cert(certs_dir, "query-ca")
|
|
httpd, http_port = self._serve_dir(certs_dir)
|
|
try:
|
|
url = f"http://127.0.0.1:{http_port}/query-ca.crt?token=abc"
|
|
r = self._run_download(
|
|
url,
|
|
"install_all_certs && install_ca_to_nss && "
|
|
"test -f /usr/local/share/ca-certificates/llm/query-ca.crt && "
|
|
"certutil -L -d sql:${HOME}/.pki/nssdb | grep -q 'llm-query-ca' && echo QUERY_NSS_OK",
|
|
)
|
|
assert r.returncode == 0, f"Query/NSS flow failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "QUERY_NSS_OK" in r.stdout
|
|
finally:
|
|
httpd.shutdown()
|
|
# #endregion test_download_query_string_filename_and_nss_import
|
|
|
|
# #region test_volume_certs_matrix_pem_der_cer_invalid_private [C:2] [TYPE Function] [SEMANTICS test,integration,certs,volume]
|
|
# @BRIEF CERTS_PATH handles PEM/DER/CER and skips invalid/private/server files.
|
|
def test_volume_certs_matrix_pem_der_cer_invalid_private(self):
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as certs_dir:
|
|
_, pem_crt = self._make_ca_cert(certs_dir, "volume-pem")
|
|
_, der_src = self._make_ca_cert(certs_dir, "volume-der-src")
|
|
der_cert = os.path.join(certs_dir, "volume-der.der")
|
|
subprocess.run(
|
|
["openssl", "x509", "-in", der_src, "-outform", "DER", "-out", der_cert],
|
|
check=True, capture_output=True, text=True, timeout=15,
|
|
)
|
|
os.remove(der_src)
|
|
_, cer_src = self._make_ca_cert(certs_dir, "volume-cer-src")
|
|
os.rename(cer_src, os.path.join(certs_dir, "volume-cer.cer"))
|
|
Path(certs_dir, "invalid.crt").write_text("not a certificate", encoding="utf-8")
|
|
Path(certs_dir, "server.key").write_text("not a key", encoding="utf-8")
|
|
Path(certs_dir, "server.crt").write_text(Path(pem_crt).read_text(encoding="utf-8"), encoding="utf-8")
|
|
|
|
r = subprocess.run(
|
|
[
|
|
"docker", "run", "--rm",
|
|
"-v", f"{certs_dir}:/opt/certs:ro",
|
|
"-e", "CERTS_PATH=/opt/certs",
|
|
"--entrypoint", "bash",
|
|
BACKEND_IMAGE,
|
|
"-c",
|
|
"source /app/certs.sh && install_all_certs && "
|
|
"test -f /usr/local/share/ca-certificates/custom/volume-pem.crt && "
|
|
"test -f /usr/local/share/ca-certificates/custom/volume-der.crt && "
|
|
"test -f /usr/local/share/ca-certificates/custom/volume-cer.crt && "
|
|
"test ! -f /usr/local/share/ca-certificates/custom/server.crt && "
|
|
"grep -q 'BEGIN CERTIFICATE' /usr/local/share/ca-certificates/custom/volume-der.crt && "
|
|
"echo VOLUME_MATRIX_OK",
|
|
],
|
|
capture_output=True, text=True, timeout=45,
|
|
)
|
|
assert r.returncode == 0, f"Volume matrix failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "VOLUME_MATRIX_OK" in r.stdout
|
|
assert "Установлено: 3" in r.stdout
|
|
assert "пропускаем: server.key" in r.stdout
|
|
assert "пропускаем: server.crt" in r.stdout
|
|
assert "невалидный сертификат: invalid.crt" in r.stdout
|
|
# #endregion test_volume_certs_matrix_pem_der_cer_invalid_private
|
|
|
|
# #region test_download_and_volume_combined_channels [C:2] [TYPE Function] [SEMANTICS test,integration,certs,combined]
|
|
# @BRIEF LLM_CA_CERT_URLS and CERTS_PATH work together in one container startup flow.
|
|
def test_download_and_volume_combined_channels(self):
|
|
import tempfile
|
|
|
|
with tempfile.TemporaryDirectory() as mount_dir, tempfile.TemporaryDirectory() as serve_dir:
|
|
self._make_ca_cert(mount_dir, "mounted-ca")
|
|
self._make_ca_cert(serve_dir, "downloaded-ca")
|
|
httpd, http_port = self._serve_dir(serve_dir)
|
|
try:
|
|
url = f"http://127.0.0.1:{http_port}/downloaded-ca.crt"
|
|
r = subprocess.run(
|
|
[
|
|
"docker", "run", "--rm",
|
|
"--network", "host",
|
|
"-v", f"{mount_dir}:/opt/certs:ro",
|
|
"-e", "CERTS_PATH=/opt/certs",
|
|
"-e", f"LLM_CA_CERT_URLS={url}",
|
|
"--entrypoint", "bash",
|
|
BACKEND_IMAGE,
|
|
"-c",
|
|
"source /app/certs.sh && download_llm_ca_certs && install_all_certs && install_ca_to_nss && "
|
|
"test -f /usr/local/share/ca-certificates/custom/mounted-ca.crt && "
|
|
"test -f /usr/local/share/ca-certificates/llm/downloaded-ca.crt && "
|
|
"certutil -L -d sql:${HOME}/.pki/nssdb | grep -q 'custom-mounted-ca' && "
|
|
"certutil -L -d sql:${HOME}/.pki/nssdb | grep -q 'llm-downloaded-ca' && "
|
|
"echo COMBINED_OK",
|
|
],
|
|
capture_output=True, text=True, timeout=60,
|
|
)
|
|
assert r.returncode == 0, f"Combined channels failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "COMBINED_OK" in r.stdout
|
|
finally:
|
|
httpd.shutdown()
|
|
# #endregion test_download_and_volume_combined_channels
|
|
|
|
# #region test_empty_inputs_graceful [C:2] [TYPE Function] [SEMANTICS test,integration,certs,empty]
|
|
# @BRIEF Empty LLM_CA_CERT_URLS and missing CERTS_PATH are no-op success cases.
|
|
def test_empty_inputs_graceful(self):
|
|
r = subprocess.run(
|
|
[
|
|
"docker", "run", "--rm",
|
|
"-e", "LLM_CA_CERT_URLS=",
|
|
"-e", "CERTS_PATH=/does-not-exist",
|
|
"--entrypoint", "bash",
|
|
BACKEND_IMAGE,
|
|
"-c",
|
|
"source /app/certs.sh && download_llm_ca_certs && install_all_certs && echo EMPTY_OK",
|
|
],
|
|
capture_output=True, text=True, timeout=45,
|
|
)
|
|
assert r.returncode == 0, f"Empty input path failed:\n{r.stdout}\n{r.stderr}"
|
|
assert "EMPTY_OK" in r.stdout
|
|
assert "CERTS_PATH=/does-not-exist не существует" in r.stdout
|
|
# #endregion test_empty_inputs_graceful
|
|
|
|
|
|
# #endregion TestBackendContainer
|