M0 + M1 done

This commit is contained in:
marcos.lohmann_at_orcl
2026-09-11 15:04:34 -03:00
parent 5ebec725ec
commit 439f60598e
51 changed files with 2991 additions and 0 deletions

273
RI/tests/e2e/conftest.py Normal file
View File

@@ -0,0 +1,273 @@
"""Shared e2e fixtures: a real OPA process, a real JWKS HTTP endpoint + RS256
token signing, and helpers to run the Gateway/Sales servers in-process
against them. No Keycloak available in this environment (no Java) -- see
RI/README.md for exactly what that does and does not validate.
"""
from __future__ import annotations
import asyncio
import json
import os
import shutil
import socket
import subprocess
import threading
import time
from collections.abc import AsyncIterator, Awaitable, Callable, Iterator
from http.server import BaseHTTPRequestHandler, HTTPServer
from pathlib import Path
import jwt
import pytest
import uvicorn
from cryptography.hazmat.primitives.asymmetric import rsa
from jwt.algorithms import RSAAlgorithm
from mcp.client.session import ClientSession
from mcp.client.streamable_http import ( # type: ignore[attr-defined]
create_mcp_http_client,
streamable_http_client,
)
from mcp.shared.exceptions import MCPError
REPO_ROOT = Path(__file__).parents[2]
POLICIES_DIR = REPO_ROOT / "config" / "policies"
ISSUER = "https://keycloak.example/realms/emcp"
AUDIENCE = "emcp-gateway"
KID = "test-key-1"
GATEWAY_PORT = 18000
SALES_PORT = 18100
def free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("127.0.0.1", 0))
return int(s.getsockname()[1])
# --- Real OPA process (EP-03) --------------------------------------------
@pytest.fixture(scope="session")
def opa_url() -> Iterator[str]:
opa_binary = shutil.which(os.environ.get("OPA_BINARY", "opa"))
if opa_binary is None:
pytest.skip(
"opa binary not found on PATH (set OPA_BINARY or install it -- see RI/README.md)"
)
port = free_port()
process = subprocess.Popen(
[opa_binary, "run", "--server", "--addr", f"127.0.0.1:{port}", str(POLICIES_DIR)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
base_url = f"http://127.0.0.1:{port}"
try:
import httpx
for _ in range(100):
try:
httpx.get(f"{base_url}/health", timeout=1)
break
except httpx.HTTPError:
time.sleep(0.05)
else:
raise RuntimeError("opa server did not become healthy in time")
yield base_url
finally:
process.terminate()
process.wait(timeout=5)
# --- Real JWKS HTTP endpoint + RS256 signing (EP-01-T03) ------------------
class _JWKSHandler(BaseHTTPRequestHandler):
jwks_document: dict[str, object] = {}
def do_GET(self) -> None: # noqa: N802
body = json.dumps(self.jwks_document).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(body)
def log_message(self, format: str, *args: object) -> None: # noqa: A002
pass
@pytest.fixture(scope="session")
def rsa_keypair() -> tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey]:
private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
return private_key, private_key.public_key()
@pytest.fixture(scope="session")
def jwks_url(rsa_keypair: tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey]) -> Iterator[str]:
_, public_key = rsa_keypair
jwk = RSAAlgorithm.to_jwk(public_key, as_dict=True)
jwk.update({"kid": KID, "use": "sig", "alg": "RS256"})
_JWKSHandler.jwks_document = {"keys": [jwk]}
server = HTTPServer(("127.0.0.1", 0), _JWKSHandler)
thread = threading.Thread(target=server.serve_forever, daemon=True)
thread.start()
try:
yield f"http://127.0.0.1:{server.server_port}/jwks"
finally:
server.shutdown()
thread.join(timeout=5)
@pytest.fixture
def sign_token(
rsa_keypair: tuple[rsa.RSAPrivateKey, rsa.RSAPublicKey],
) -> Callable[..., str]:
private_key, _ = rsa_keypair
def _sign(
client_id: str,
*,
issuer: str = ISSUER,
audience: str = AUDIENCE,
expires_in_seconds: int = 300,
signing_key: rsa.RSAPrivateKey | None = None,
) -> str:
now = int(time.time())
claims = {
"iss": issuer,
"aud": audience,
"sub": f"service-account-{client_id}",
"azp": client_id,
"iat": now,
"exp": now + expires_in_seconds,
}
return jwt.encode(
claims, signing_key or private_key, algorithm="RS256", headers={"kid": KID}
)
return _sign
# --- Running Gateway + Sales servers, wired to the fixtures above --------
class RunningServer:
def __init__(self, config: uvicorn.Config) -> None:
self.server = uvicorn.Server(config)
self._task: asyncio.Task[None] | None = None
async def start(self) -> None:
self._task = asyncio.create_task(self.server.serve())
while not self.server.started:
await asyncio.sleep(0.01)
# See test_walking_skeleton.py's original note: give the ASGI app's
# own lifespan (session manager task group) one extra tick.
await asyncio.sleep(0.05)
async def stop(self) -> None:
self.server.should_exit = True
assert self._task is not None
await self._task
@pytest.fixture
async def governed_stack(
monkeypatch: pytest.MonkeyPatch, opa_url: str, jwks_url: str
) -> AsyncIterator[None]:
"""Real OPA + real JWKS-backed OIDC auth, wired into fresh Gateway/Sales
instances for one test (EP-01/EP-03/EP-04/EP-05 all real, only Keycloak
itself is substituted by the JWKS server -- see RI/README.md)."""
monkeypatch.setenv("SALES_DOMAIN_URL", f"http://127.0.0.1:{SALES_PORT}/mcp")
monkeypatch.setenv("OPA_URL", opa_url)
monkeypatch.setenv("OIDC_JWKS_URL", jwks_url)
monkeypatch.setenv("OIDC_ISSUER", ISSUER)
monkeypatch.setenv("OIDC_AUDIENCE", AUDIENCE)
import example_mcp_servers.sales_domain.server as sales_module
sales_config = uvicorn.Config(
sales_module.server.streamable_http_app(host="127.0.0.1"),
host="127.0.0.1",
port=SALES_PORT,
log_level="warning",
)
sales_server = RunningServer(sales_config)
await sales_server.start()
import emcp_bus.gateway.server as gateway_module
auth_settings, token_verifier = gateway_module.build_auth()
gateway_config = uvicorn.Config(
gateway_module.server.streamable_http_app(
host="127.0.0.1", auth=auth_settings, token_verifier=token_verifier
),
host="127.0.0.1",
port=GATEWAY_PORT,
log_level="warning",
)
gateway_server = RunningServer(gateway_config)
await gateway_server.start()
try:
yield
finally:
await gateway_server.stop()
await sales_server.stop()
async def run_against_gateway[ResultT](
operation: Callable[[ClientSession], Awaitable[ResultT]],
*,
token: str | None = None,
) -> ResultT:
"""Connect to the gateway (optionally authenticated) and run `operation(session)`.
Retries on `MCPError` ("SSE stream ended without a response" has been
observed, rarely, under this in-process dual-uvicorn test setup under
scheduler contention) -- a test-harness robustness concern, not a
Gateway defect: production deployments run one process per service.
"""
headers = {"Authorization": f"Bearer {token}"} if token else None
url = f"http://127.0.0.1:{GATEWAY_PORT}/mcp"
last_error: MCPError | None = None
for attempt in range(3):
if attempt:
await asyncio.sleep(0.1 * attempt)
try:
async with (
create_mcp_http_client(headers=headers) as http_client,
streamable_http_client(url, http_client=http_client) as (read, write),
ClientSession(read, write) as session,
):
await session.initialize()
return await operation(session)
except MCPError as exc:
last_error = exc
except BaseExceptionGroup as group:
# Each nested anyio TaskGroup (`streamable_http_client`'s, then
# `ClientSession`'s) wraps the one below it in its own
# ExceptionGroup, so a handler-raised MCPError (e.g. an auth
# rejection during `initialize`) can arrive doubly-wrapped.
# Unwrap fully so callers can `pytest.raises(MCPError)`
# uniformly, same as for one raised directly.
unwrapped = _unwrap_single_mcp_error(group)
if unwrapped is not None:
last_error = unwrapped
else:
raise
assert last_error is not None
raise last_error
def _unwrap_single_mcp_error(exc: BaseException) -> MCPError | None:
"""If `exc` is an MCPError, or a (possibly nested) ExceptionGroup wrapping
exactly one, return it; otherwise `None`."""
if isinstance(exc, MCPError):
return exc
if isinstance(exc, BaseExceptionGroup) and len(exc.exceptions) == 1:
return _unwrap_single_mcp_error(exc.exceptions[0])
return None