199 lines
7.3 KiB
Python
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()
|