Files
ITWorx_MCP_Hub-Public/tests/e2e/harness.py
T

199 lines
7.3 KiB
Python

"""WP-230D real-process E2E harness.
Starts the actual production topology entirely offline so Playwright can
drive a real browser through the real PKCE/BFF login flow, real per-role
authorization, and the real policy engine -- not a mocked frontend:
browser -> web reverse-proxy (services.runtime.web, real HTTPS)
-> control-api (services.runtime.control_api, real routes/RBAC)
-> fake OIDC provider (real HTTPS, for the /authorize redirect
the browser itself must navigate)
-> fake ContextForge (in-process MockTransport; nothing the
browser talks to directly, so no real HTTP needed here)
The worker readiness dependency is an explicit offline fixture; worker reconciliation
is validated by separate integration tests. Each subcommand below blocks running
one of these processes; Playwright's
`webServer` config (apps/web/playwright.config.ts) starts all four and
waits for each to answer its readiness probe before running any test.
Persona switching: `OIDCSettings`/the real `/auth/login` route were left
untouched (no test-only hint parameter was added to production auth code).
Instead the fake OIDC provider's real /authorize endpoint reads an
optional ?persona= query param (see fake_provider.py) to choose which
group claim to issue; Playwright supplies it by rewriting the outbound
request in the browser context (page.route), never by modifying the
server the app under test talks to.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import httpx # noqa: E402
import uvicorn # noqa: E402
from fastapi import FastAPI # noqa: E402
from services.control_api.auth.config import OIDCSettings # noqa: E402
from services.control_api.auth.fake_provider import FakeOIDCProvider # noqa: E402
from services.control_api.auth.models import ManagementRole # noqa: E402
from services.control_api.contextforge.fake import FakeContextForge # noqa: E402
from services.control_api.contextforge.models import ContextForgeSettings # noqa: E402
from services.control_api.persistence import ( # noqa: E402
Base,
create_engine_from_url,
session_factory,
)
from services.runtime import web as web_runtime # noqa: E402
from services.runtime.control_api import build_app as build_control_app # noqa: E402
from services.runtime.state_sync import sync_runtime_state # noqa: E402
from tests.e2e.tls import generate_local_tls_material # noqa: E402
STATE_DIR = ROOT / ".wp230e2e"
CATALOG_ROOT = ROOT / "catalog"
ROLE_GROUPS: dict[ManagementRole, str] = {
ManagementRole.VIEWER: "hub-viewer",
ManagementRole.AUDITOR: "hub-auditor",
ManagementRole.OPERATOR: "hub-operator",
ManagementRole.ADMIN: "hub-admin",
}
def _origin(port: int) -> str:
return f"https://127.0.0.1:{port}"
def run_oidc(port: int) -> None:
tls = generate_local_tls_material(STATE_DIR / "tls-oidc")
provider = FakeOIDCProvider(
issuer=_origin(port),
user_groups=(ROLE_GROUPS[ManagementRole.VIEWER],),
persona_groups={group: (group,) for group in ROLE_GROUPS.values()},
)
uvicorn.run(
provider.asgi_app(),
host="127.0.0.1",
port=port,
log_level="warning",
ssl_certfile=str(tls.cert_path),
ssl_keyfile=str(tls.key_path),
)
def run_control_api(port: int, oidc_port: int, web_port: int) -> None:
os.environ["WP230E2E_CONTEXTFORGE_TOKEN"] = "fake-token" # noqa: S105
os.environ.setdefault("MCP_CLIENT_BINDINGS_JSON", "[]")
STATE_DIR.mkdir(parents=True, exist_ok=True)
db_path = STATE_DIR / "control-api.db"
db_path.unlink(missing_ok=True)
engine = create_engine_from_url(f"sqlite:///{db_path}")
Base.metadata.create_all(engine)
sessions = session_factory(engine)
issuer = _origin(oidc_port)
with sessions() as session, session.begin():
sync_runtime_state(session, CATALOG_ROOT, issuer)
oidc_tls = STATE_DIR / "tls-oidc"
provider_transport = httpx.HTTPTransport(verify=str(oidc_tls / "ca.pem"))
oidc = OIDCSettings(
issuer=issuer,
audience="itworx-mcp-hub",
client_id="hub-bff",
redirect_uri=f"{_origin(web_port)}/auth/callback",
secure_cookie=True,
same_site="lax",
role_group_map={group: role for role, group in ROLE_GROUPS.items()},
)
app = build_control_app(
oidc=oidc,
provider_transport=provider_transport,
contextforge_settings=ContextForgeSettings(
base_url="https://contextforge.e2e.test",
auth_token_env="WP230E2E_CONTEXTFORGE_TOKEN", # noqa: S106
),
contextforge_transport=FakeContextForge().transport(),
engine=engine,
catalog_root=CATALOG_ROOT,
)
uvicorn.run(app, host="127.0.0.1", port=port, log_level="warning")
def worker_readiness_fixture() -> FastAPI:
"""Offline readiness dependency; worker reconciliation has separate integration tests."""
app = FastAPI()
@app.get("/health/ready")
def ready() -> dict[str, str]:
return {"status": "ready", "component": "e2e-worker-fixture"}
return app
def run_worker(port: int) -> None:
uvicorn.run(worker_readiness_fixture(), host="127.0.0.1", port=port, log_level="warning")
def run_web(port: int, control_port: int, worker_port: int, dist_root: Path) -> None:
if not (dist_root / "index.html").is_file():
raise SystemExit(
f"{dist_root} has no built frontend; "
"run `pnpm --filter @itworx-mcp-hub/web build` first"
)
os.environ["HUB_CONTROL_URL"] = f"http://127.0.0.1:{control_port}"
os.environ["HUB_WORKER_URL"] = f"http://127.0.0.1:{worker_port}"
os.environ["HUB_WEB_ROOT"] = str(dist_root)
tls = generate_local_tls_material(STATE_DIR / "tls-web")
uvicorn.run(
web_runtime.build_app(),
host="127.0.0.1",
port=port,
log_level="warning",
ssl_certfile=str(tls.cert_path),
ssl_keyfile=str(tls.key_path),
)
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
sub = parser.add_subparsers(dest="command", required=True)
oidc_parser = sub.add_parser("oidc")
oidc_parser.add_argument("--port", type=int, required=True)
control_parser = sub.add_parser("control-api")
control_parser.add_argument("--port", type=int, required=True)
control_parser.add_argument("--oidc-port", type=int, required=True)
control_parser.add_argument("--web-port", type=int, required=True)
web_parser = sub.add_parser("web")
web_parser.add_argument("--port", type=int, required=True)
web_parser.add_argument("--control-port", type=int, required=True)
web_parser.add_argument("--worker-port", type=int, required=True)
web_parser.add_argument("--dist", type=Path, required=True)
worker_parser = sub.add_parser("worker")
worker_parser.add_argument("--port", type=int, required=True)
args = parser.parse_args()
if args.command == "oidc":
run_oidc(args.port)
elif args.command == "control-api":
run_control_api(args.port, args.oidc_port, args.web_port)
elif args.command == "web":
run_web(args.port, args.control_port, args.worker_port, args.dist)
elif args.command == "worker":
run_worker(args.port)
if __name__ == "__main__":
main()