"""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) Each subcommand below blocks running one of these processes; Playwright's `webServer` config (apps/web/playwright.config.ts) starts all three 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 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 run_web(port: int, control_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_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("--dist", type=Path, 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.dist) if __name__ == "__main__": main()