vibe/tests/backend/test_ssl_integration.py
Mathias Gesbert 3f8487f761
v2.14.0 (#743)
Co-authored-by: Alexis Tacnet <alexis@mistral.ai>
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai>
Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com>
Co-authored-by: Lucas Marandat <31749711+lucasmrdt@users.noreply.github.com>
Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai>
Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai>
Co-authored-by: Quentin <quentin.torroba@mistral.ai>
Co-authored-by: Val <102326092+vdeva@users.noreply.github.com>
Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com>
Co-authored-by: p.vezia <166131032+le-codeur-rapide@users.noreply.github.com>
Co-authored-by: Hiba Chaabnia <Hiba-Chaabnia@users.noreply.github.com>
Co-authored-by: Nikhil Bhima <nikhilbhima@users.noreply.github.com>
Co-authored-by: Nkipohcs <Nkipohcs@users.noreply.github.com>
Co-authored-by: Mistral Vibe <vibe@mistral.ai>
2026-06-04 18:26:35 +02:00

142 lines
5 KiB
Python

from __future__ import annotations
from collections.abc import Iterator
from dataclasses import dataclass
from datetime import UTC, datetime, timedelta
from ipaddress import IPv4Address
from pathlib import Path
import ssl
from cryptography import x509
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID
import pytest
from tests.e2e.mock_server import StreamingMockServer
from vibe.core.config import ModelConfig, ProviderConfig
from vibe.core.llm.backend.generic import GenericBackend
from vibe.core.types import Backend, LLMMessage, Role
from vibe.core.utils import build_ssl_context, configure_ssl_context
@dataclass(frozen=True)
class _TLSMaterial:
ca_file: str
cert_file: str
key_file: str
@dataclass(frozen=True)
class _HttpsStreamingMockServer:
server: StreamingMockServer
ca_file: str
@pytest.fixture
def https_streaming_mock_server(tmp_path: Path) -> Iterator[_HttpsStreamingMockServer]:
tls = _write_tls_material(tmp_path)
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ssl_context.load_cert_chain(certfile=tls.cert_file, keyfile=tls.key_file)
server = StreamingMockServer(ssl_context=ssl_context)
server.start()
try:
yield _HttpsStreamingMockServer(server=server, ca_file=tls.ca_file)
finally:
server.stop()
@pytest.mark.asyncio
async def test_generic_backend_streaming_uses_ssl_cert_file(
monkeypatch: pytest.MonkeyPatch,
https_streaming_mock_server: _HttpsStreamingMockServer,
) -> None:
monkeypatch.setenv("SSL_CERT_FILE", https_streaming_mock_server.ca_file)
monkeypatch.delenv("SSL_CERT_DIR", raising=False)
configure_ssl_context(enable_system_trust_store=False)
build_ssl_context.cache_clear()
chunks = []
try:
provider = ProviderConfig(
name="mock-provider",
api_base=https_streaming_mock_server.server.api_base,
api_key_env_var="MISTRAL_API_KEY",
backend=Backend.GENERIC,
)
model = ModelConfig(
name="mock-model", provider="mock-provider", alias="mock-model"
)
async with GenericBackend(provider=provider, timeout=5.0) as backend:
chunks = [
chunk
async for chunk in backend.complete_streaming(
model=model, messages=[LLMMessage(role=Role.user, content="Greet")]
)
]
finally:
configure_ssl_context(enable_system_trust_store=False)
build_ssl_context.cache_clear()
content = "".join(chunk.message.content or "" for chunk in chunks)
request_payload = https_streaming_mock_server.server.requests[-1]
assert content == "Hello from mock server"
assert request_payload.get("stream") is True
assert request_payload.get("model") == "mock-model"
def _write_tls_material(tmp_path: Path) -> _TLSMaterial:
now = datetime.now(UTC)
ca_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
ca_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "Vibe test CA")])
ca_cert = (
x509
.CertificateBuilder()
.subject_name(ca_name)
.issuer_name(ca_name)
.public_key(ca_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(minutes=1))
.not_valid_after(now + timedelta(days=1))
.add_extension(x509.BasicConstraints(ca=True, path_length=None), critical=True)
.sign(ca_key, hashes.SHA256())
)
server_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
server_name = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, "127.0.0.1")])
server_cert = (
x509
.CertificateBuilder()
.subject_name(server_name)
.issuer_name(ca_cert.subject)
.public_key(server_key.public_key())
.serial_number(x509.random_serial_number())
.not_valid_before(now - timedelta(minutes=1))
.not_valid_after(now + timedelta(days=1))
.add_extension(
x509.SubjectAlternativeName([x509.IPAddress(IPv4Address("127.0.0.1"))]),
critical=False,
)
.add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True)
.add_extension(
x509.ExtendedKeyUsage([ExtendedKeyUsageOID.SERVER_AUTH]), critical=False
)
.sign(ca_key, hashes.SHA256())
)
ca_file = tmp_path / "ca.pem"
cert_file = tmp_path / "server.pem"
key_file = tmp_path / "server.key"
ca_file.write_bytes(ca_cert.public_bytes(serialization.Encoding.PEM))
cert_file.write_bytes(server_cert.public_bytes(serialization.Encoding.PEM))
key_file.write_bytes(
server_key.private_bytes(
encoding=serialization.Encoding.PEM,
format=serialization.PrivateFormat.TraditionalOpenSSL,
encryption_algorithm=serialization.NoEncryption(),
)
)
return _TLSMaterial(
ca_file=str(ca_file), cert_file=str(cert_file), key_file=str(key_file)
)